Skip to content

Commit

Permalink
Fix #2701: Add Support for Merge Patch for Kubernetes resources
Browse files Browse the repository at this point in the history
+ Allow option to configure PatchType in patch operation
+ Use MediaType in PatchType enum + Add merge-patch+json example
  • Loading branch information
rohanKanojia authored and manusa committed Apr 28, 2021
1 parent c06d990 commit b54d41f
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* Fix #2980: Add DSL Support for `apps/v1#ControllerRevision` resource
* Fix #2981: Add DSL support for `storage.k8s.io/v1beta1` CSIDriver, CSINode and VolumeAttachment
* Fix #2912: Add DSL support for `storage.k8s.io/v1beta1` CSIStorageCapacity
* Fix #2701: Better support for patching in KuberntesClient

#### _**Note**_: Breaking changes in the API
##### DSL Changes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,35 @@
*/
package io.fabric8.kubernetes.client.dsl;

import io.fabric8.kubernetes.client.dsl.base.PatchContext;

public interface Patchable<T> {

/**
* Update field(s) of a resource using a JSON patch.
*
* @param item item to be patched with patched values
* @return returns deserialized version of api server response
*/
T patch(T item);

/**
* Update field(s) of a resource using strategic merge patch.
*
* @param patch The patch to be applied to the resource JSON file.
* @return returns deserialized version of api server response
*/
default T patch(String patch) {
return patch(null, patch);
}

/**
* Update field(s) of a resource using type specified in {@link PatchContext}(defaults to strategic merge if not specified).
*
* @param patchContext {@link PatchContext} for patch request
* @param patch The patch to be applied to the resource JSON file.
* @return The patch to be applied to the resource JSON file.
*/
T patch(PatchContext patchContext, String patch);

}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public class BaseOperation<T extends HasMetadata, L extends KubernetesResourceLi
private static final String INVOLVED_OBJECT_RESOURCE_VERSION = "involvedObject.resourceVersion";
private static final String INVOLVED_OBJECT_API_VERSION = "involvedObject.apiVersion";
private static final String INVOLVED_OBJECT_FIELD_PATH = "involvedObject.fieldPath";
private static final String READ_ONLY_UPDATE_EXCEPTION_MESSAGE = "Cannot update read-only resources";
private static final String READ_ONLY_EDIT_EXCEPTION_MESSAGE = "Cannot edit read-only resources";

private final boolean cascading;
private final T item;
Expand Down Expand Up @@ -245,12 +247,12 @@ public RootPaths getRootPaths() {

@Override
public T edit(UnaryOperator<T> function) {
throw new KubernetesClientException("Cannot edit read-only resources");
throw new KubernetesClientException(READ_ONLY_EDIT_EXCEPTION_MESSAGE);
}

@Override
public T edit(Visitor... visitors) {
throw new KubernetesClientException("Cannot edit read-only resources");
throw new KubernetesClientException(READ_ONLY_EDIT_EXCEPTION_MESSAGE);
}

@Override
Expand All @@ -270,7 +272,7 @@ public void visit(V item) {

@Override
public T accept(Consumer<T> consumer) {
throw new KubernetesClientException("Cannot edit read-only resources");
throw new KubernetesClientException(READ_ONLY_EDIT_EXCEPTION_MESSAGE);
}

@Override
Expand Down Expand Up @@ -848,12 +850,17 @@ public Watch watch(ListOptions options, final Watcher<T> watcher) {

@Override
public T replace(T item) {
throw new KubernetesClientException("Cannot update read-only resources");
throw new KubernetesClientException(READ_ONLY_UPDATE_EXCEPTION_MESSAGE);
}

@Override
public T patch(T item) {
throw new KubernetesClientException("Cannot update read-only resources");
throw new KubernetesClientException(READ_ONLY_UPDATE_EXCEPTION_MESSAGE);
}

@Override
public T patch(PatchContext patchContext, String patch) {
throw new KubernetesClientException(READ_ONLY_UPDATE_EXCEPTION_MESSAGE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.Resource;

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;

import static io.fabric8.kubernetes.client.utils.IOHelpers.convertToJson;

public class HasMetadataOperation<T extends HasMetadata, L extends KubernetesResourceList<T>, R extends Resource<T>> extends BaseOperation< T, L, R> {
public static final DeletionPropagation DEFAULT_PROPAGATION_POLICY = DeletionPropagation.BACKGROUND;
public static final long DEFAULT_GRACE_PERIOD_IN_SECONDS = -1L;
private static final String PATCH_OPERATION = "patch";

public HasMetadataOperation(OperationContext ctx) {
super(ctx);
Expand Down Expand Up @@ -119,7 +125,7 @@ public T patch(T item) {
resource.getMetadata().setResourceVersion(resourceVersion);
return handlePatch(got, resource);
} catch (Exception e) {
throw KubernetesClientException.launderThrowable(forOperationType("patch"), e);
throw KubernetesClientException.launderThrowable(forOperationType(PATCH_OPERATION), e);
}
};
return visitor.apply(item);
Expand All @@ -142,6 +148,22 @@ public T patch(T item) {
caught = e;
}
}
throw KubernetesClientException.launderThrowable(forOperationType("patch"), caught);
throw KubernetesClientException.launderThrowable(forOperationType(PATCH_OPERATION), caught);
}

@Override
public T patch(PatchContext patchContext, String patch) {
try {
final T got = fromServer().get();
if (got == null) {
return null;
}
return handlePatch(patchContext, got, convertToJson(patch), getType());
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw KubernetesClientException.launderThrowable(forOperationType(PATCH_OPERATION), interruptedException);
} catch (IOException | ExecutionException e) {
throw KubernetesClientException.launderThrowable(forOperationType(PATCH_OPERATION), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class OperationSupport {
public static final MediaType JSON = MediaType.parse("application/json");
public static final MediaType JSON_PATCH = MediaType.parse("application/json-patch+json");
public static final MediaType STRATEGIC_MERGE_JSON_PATCH = MediaType.parse("application/strategic-merge-patch+json");
public static final MediaType JSON_MERGE_PATCH = MediaType.parse("application/merge-patch+json");
protected static final ObjectMapper JSON_MAPPER = Serialization.jsonMapper();
protected static final ObjectMapper YAML_MAPPER = Serialization.yamlMapper();
private static final String CLIENT_STATUS_FLAG = "CLIENT_STATUS_FLAG";
Expand Down Expand Up @@ -174,6 +175,23 @@ public URL getResourceURLForWriteOperation(URL resourceURL) throws MalformedURLE
return resourceURL;
}

public URL getResourceURLForPatchOperation(URL resourceUrl, PatchContext patchContext) throws MalformedURLException {
if (patchContext != null) {
String url = resourceUrl.toString();
if (patchContext.getForce() != null) {
url = URLUtils.join(url, "?force=" + patchContext.getForce());
}
if ((patchContext.getDryRun() != null && !patchContext.getDryRun().isEmpty()) || dryRun) {
url = URLUtils.join(url, "?dryRun=All");
}
if (patchContext.getFieldManager() != null) {
url = URLUtils.join(url, "?fieldManager=" + patchContext.getFieldManager());
}
return new URL(url);
}
return resourceUrl;
}

protected <T> String checkNamespace(T item) {
String operationNs = getNamespace();
String itemNs = (item instanceof HasMetadata && ((HasMetadata)item).getMetadata() != null) ? ((HasMetadata) item).getMetadata().getNamespace() : null;
Expand Down Expand Up @@ -344,6 +362,26 @@ protected <T> T handlePatch(T current, Map<String, Object> patchForUpdate, Class
return handleResponse(requestBuilder, type, Collections.<String, String>emptyMap());
}

/**
* Send an http patch and handle the response.
*
* @param patchContext patch options for patch request
* @param current current object
* @param patchForUpdate Patch string
* @param type type of object
* @param <T> template argument provided
* @return returns de-serialized version of api server response
* @throws ExecutionException Execution Exception
* @throws InterruptedException Interrupted Exception
* @throws IOException IOException in case of network errors
*/
protected <T> T handlePatch(PatchContext patchContext, T current, String patchForUpdate, Class<T> type) throws ExecutionException, InterruptedException, IOException {
MediaType bodyMediaType = getMediaTypeFromPatchContextOrDefault(patchContext);
RequestBody body = RequestBody.create(bodyMediaType, patchForUpdate);
Request.Builder requestBuilder = new Request.Builder().patch(body).url(getResourceURLForPatchOperation(getResourceUrl(checkNamespace(current), checkName(current)), patchContext));
return handleResponse(requestBuilder, type, Collections.emptyMap());
}

/**
* Replace Scale of specified Kubernetes Resource
*
Expand Down Expand Up @@ -611,4 +649,11 @@ protected static <T> Map getObjectValueAsMap(T object) {
public Config getConfig() {
return config;
}

private MediaType getMediaTypeFromPatchContextOrDefault(PatchContext patchContext) {
if (patchContext != null && patchContext.getPatchType() != null) {
return patchContext.getPatchType().getMediaType();
}
return STRATEGIC_MERGE_JSON_PATCH;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.dsl.base;

import java.util.List;

public class PatchContext {
private List<String> dryRun;
private String fieldManager;
private Boolean force;
private PatchType patchType;

public List<String> getDryRun() {
return dryRun;
}

public void setDryRun(List<String> dryRun) {
this.dryRun = dryRun;
}

public String getFieldManager() {
return fieldManager;
}

public void setFieldManager(String fieldManager) {
this.fieldManager = fieldManager;
}

public Boolean getForce() {
return force;
}

public void setForce(Boolean force) {
this.force = force;
}

public PatchType getPatchType() {
return patchType;
}

public void setPatchType(PatchType patchType) {
this.patchType = patchType;
}

public static class Builder {
private final PatchContext patchContext;

public Builder() {
this.patchContext = new PatchContext();
}

public Builder withDryRun(List<String> dryRun) {
this.patchContext.setDryRun(dryRun);
return this;
}

public Builder withFieldManager(String fieldManager) {
this.patchContext.setFieldManager(fieldManager);
return this;
}

public Builder withForce(Boolean force) {
this.patchContext.setForce(force);
return this;
}

public Builder withPatchType(PatchType patchType) {
this.patchContext.setPatchType(patchType);
return this;
}

public PatchContext build() {
return this.patchContext;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.dsl.base;

import okhttp3.MediaType;

import static io.fabric8.kubernetes.client.dsl.base.OperationSupport.JSON_PATCH;
import static io.fabric8.kubernetes.client.dsl.base.OperationSupport.JSON_MERGE_PATCH;
import static io.fabric8.kubernetes.client.dsl.base.OperationSupport.STRATEGIC_MERGE_JSON_PATCH;

/**
* Enum for different Patch types supported by Patch
*/
public enum PatchType {
JSON(JSON_PATCH),
JSON_MERGE(JSON_MERGE_PATCH),
STRATEGIC_MERGE(STRATEGIC_MERGE_JSON_PATCH);

private final MediaType mediaType;

PatchType(MediaType mediaType) {
this.mediaType = mediaType;
}

public MediaType getMediaType() {
return this.mediaType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,11 @@ public static String convertYamlToJson(String yaml) throws IOException {
return jsonWriter.writeValueAsString(obj);
}

public static String convertToJson(String jsonOrYaml) throws IOException {
if (isJSONValid(jsonOrYaml)) {
return jsonOrYaml;
}
return convertYamlToJson(jsonOrYaml);
}

}
Loading

0 comments on commit b54d41f

Please sign in to comment.