Skip to content

Commit

Permalink
Edit a CustomResource should result in a patch (#2923)
Browse files Browse the repository at this point in the history
* CustomResource edit as PATCH

* fix the test

* patchutils object mapper
  • Loading branch information
andreaTP authored Apr 7, 2021
1 parent 49c7255 commit 07f77b1
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.fabric8.kubernetes.api.model.DeleteOptions;
Expand All @@ -48,11 +49,12 @@
import io.fabric8.kubernetes.client.dsl.Namespaceable;
import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext;
import io.fabric8.kubernetes.client.dsl.base.OperationSupport;
import io.fabric8.kubernetes.client.internal.PatchUtils;
import io.fabric8.kubernetes.client.utils.HttpClientUtils;
import io.fabric8.kubernetes.client.utils.IOHelpers;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;
import io.fabric8.kubernetes.client.utils.WatcherToggle;
import io.fabric8.zjsonpatch.JsonDiff;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -88,12 +90,12 @@ public class RawCustomResourceOperationsImpl extends OperationSupport implements
private final String deletionPropagation;
private final boolean cascading;

private enum HttpCallMethod { GET, POST, PUT, DELETE }
private enum HttpCallMethod { GET, POST, PUT, PATCH, DELETE }

private RawCustomResourceOperationsImpl(OkHttpClient client, Config config, CustomResourceDefinitionContext crdContext, String namespace, String name, long gracePeriodInSeconds, boolean cascading, String deletionPropagation, ListOptions listOptions, boolean dryRun) {
super(client, config);
this.customResourceDefinition = crdContext;
this.objectMapper = Serialization.jsonMapper();
this.objectMapper = PatchUtils.patchMapper();
this.namespace = namespace;
this.name = name;
this.gracePeriodInSeconds = gracePeriodInSeconds;
Expand Down Expand Up @@ -298,6 +300,18 @@ public Map<String, Object> createOrReplace(String namespace, InputStream objectA
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, namespace, null, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).createOrReplace(objectAsStream);
}

private Map<String, Object> replace(String namespace, String name, Map<String, Object> object) throws IOException {
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, namespace, name, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).replace(object);
}

private Map<String, Object> replace(String name, Map<String, Object> object) throws IOException {
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, null, name, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).replace(object);
}

private Map<String, Object> replace(Map<String, Object> object) throws IOException {
return validateAndSubmitRequest(objectMapper.writeValueAsString(object), HttpCallMethod.PUT);
}

/**
* Edit a custom resource object which is a non-namespaced object.
*
Expand Down Expand Up @@ -332,7 +346,6 @@ public Map<String, Object> edit(String name, String objectAsString) throws IOExc
* @throws IOException in case of network/serialization failures or failures from Kubernetes API
*/
public Map<String, Object> edit(String namespace, String name, Map<String, Object> object) throws IOException {
object = appendResourceVersionInObject(namespace, name, object);
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, namespace, name, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).edit(object);
}

Expand All @@ -346,9 +359,6 @@ public Map<String, Object> edit(String namespace, String name, Map<String, Objec
* @throws IOException in case of network/serialization failures or failures from Kubernetes API
*/
public Map<String, Object> edit(String namespace, String name, String objectAsString) throws IOException {
// Append resourceVersion in object metadata in order to
// avoid : https://github.com/fabric8io/kubernetes-client/issues/1724
objectAsString = appendResourceVersionInObject(namespace, name, objectAsString);
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, namespace, name, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).edit(objectAsString);
}

Expand All @@ -360,7 +370,8 @@ public Map<String, Object> edit(String namespace, String name, String objectAsSt
* @throws IOException in case of network/serializatino failures or failures from Kubernetes API
*/
public Map<String, Object> edit(String objectAsString) throws IOException {
return validateAndSubmitRequest(objectAsString, HttpCallMethod.PUT);
Map<String, Object> object = convertJsonOrYamlStringToMap(objectAsString);
return new RawCustomResourceOperationsImpl(client, config, customResourceDefinition, namespace, name, gracePeriodInSeconds, cascading, deletionPropagation, listOptions, dryRun).edit(object);
}

/**
Expand All @@ -371,7 +382,8 @@ public Map<String, Object> edit(String objectAsString) throws IOException {
* @throws IOException in case of network/serializatino failures or failures from Kubernetes API
*/
public Map<String, Object> edit(Map<String, Object> object) throws IOException {
return validateAndSubmitRequest(objectMapper.writeValueAsString(object), HttpCallMethod.PUT);
String objectAsString = getPatchDiff(namespace, name, object);
return validateAndSubmitRequest(objectAsString, HttpCallMethod.PATCH);
}

/**
Expand Down Expand Up @@ -883,7 +895,7 @@ private Map<String, Object> createOrReplaceObject(Map<String, Object> objectAsMa
}
String nameFromObject = (String) metadata.get("name");
ret = this.namespace != null ?
edit(this.namespace, nameFromObject, objectAsMap) : edit(nameFromObject, objectAsMap);
replace(this.namespace, nameFromObject, objectAsMap) : replace(nameFromObject, objectAsMap);
} catch (NullPointerException nullPointerException) {
throw KubernetesClientException.launderThrowable(new IllegalStateException("Invalid object provided -- metadata.name is required."));
}
Expand Down Expand Up @@ -1057,23 +1069,22 @@ private Request getRequest(String url, String body, HttpCallMethod httpCallMetho
return requestBuilder.post(requestBody).url(url).build();
case PUT:
return requestBuilder.put(requestBody).url(url).build();
case PATCH:
return requestBuilder.patch(RequestBody.create(JSON_PATCH, body)).url(url).build();
}
return requestBuilder.build();
}

private String appendResourceVersionInObject(String namespace, String customResourceName, String customResourceAsJsonString) throws IOException {
Map<String, Object> newObject = convertJsonOrYamlStringToMap(customResourceAsJsonString);

return objectMapper.writeValueAsString(appendResourceVersionInObject(namespace, customResourceName, newObject));
}

private Map<String, Object> appendResourceVersionInObject(String namespace, String customResourceName, Map<String, Object> customResource) throws IOException {
private String getPatchDiff(String namespace, String customResourceName, Map<String, Object> customResource) throws IOException {
Map<String, Object> oldObject = get(namespace, customResourceName);
String resourceVersion = ((Map<String, Object>)oldObject.get(METADATA)).get(RESOURCE_VERSION).toString();

((Map<String, Object>)customResource.get(METADATA)).put(RESOURCE_VERSION, resourceVersion);
// Exclude changes to the status
oldObject.put("status", null);
customResource.put("status", null);

JsonNode newone = JsonDiff.asJson(PatchUtils.patchMapper().valueToTree(oldObject), PatchUtils.patchMapper().valueToTree(customResource));

return customResource;
return objectMapper.writeValueAsString(newone);
}

private DeleteOptions fetchDeleteOptions(boolean cascading, String propagationPolicy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
*/
package io.fabric8.kubernetes.client.dsl.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.io.IOException;
Expand All @@ -30,6 +27,8 @@

import io.fabric8.kubernetes.api.model.DeleteOptionsBuilder;
import io.fabric8.kubernetes.api.model.DeletionPropagation;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
Expand Down Expand Up @@ -657,6 +656,19 @@ void testGetConfigmap() {
assertRequestCaptured(captor, "/api/v1/namespaces/default/configmaps/game-config", "GET");
}

@Test
void testEditCR() throws IOException {
// Given
RawCustomResourceOperationsImpl rawCustomResourceOperations = new RawCustomResourceOperationsImpl(mockClient, config, clusterCustomResourceDefinitionContext);
String jsonString = "{ \"metadata\": " + Serialization.jsonMapper().writeValueAsString(new ObjectMetaBuilder().withName("myresource").withNamespace("mynamespace").build()) + "}";

// When
Map<String, Object> res = rawCustomResourceOperations.edit(jsonString);

// Then
assertNotEquals(null, res);
}

private void assertRequestCaptured(ArgumentCaptor<Request> captor, String url, String method) {
verify(mockClient).newCall(captor.capture());
assertEquals(url, captor.getValue().url().encodedPath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,12 @@ void testGet() {

@Test
void testEdit() throws IOException {
String jsonObject = "{\"apiVersion\": \"test.fabric8.io/v1alpha1\",\"kind\": \"Hello\"," +
"\"metadata\": {\"resourceVersion\": \"1\", \"name\": \"example-hello\"},\"spec\": {\"size\": 3}}";
String jsonObjectNew = "{\"apiVersion\": \"test.fabric8.io/v1alpha1\",\"kind\": \"Hello\"," +
"\"metadata\": {\"resourceVersion\": \"1\", \"name\": \"example-hello\"},\"spec\": {\"size\": 4}}";
server.expect().put().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObjectNew).once();
server.expect().patch().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObjectNew).once();
server.expect().get().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObject).once();
server.expect().get().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObjectNew).once();

KubernetesClient client = server.getClient();
Expand Down

0 comments on commit 07f77b1

Please sign in to comment.