Skip to content

Commit

Permalink
Fix fabric8io#1041: Support cascading delete on custom resources
Browse files Browse the repository at this point in the history
  • Loading branch information
rohanKanojia committed Nov 14, 2019
1 parent eec48b2 commit deb1164
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 11 deletions.
Expand Up @@ -16,6 +16,7 @@
package io.fabric8.kubernetes.client.dsl.internal;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.DeleteOptions;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClientException;
Expand Down Expand Up @@ -72,7 +73,7 @@ public RawCustomResourceOperationsImpl(OkHttpClient client, Config config, Custo
* @throws IOException exception in case any read operation fails.
*/
public Map<String, Object> load(InputStream fileInputStream) throws IOException {
return convertJsonStringToMap(IOHelpers.readFully(fileInputStream));
return convertJsonOrYamlStringToMap(IOHelpers.readFully(fileInputStream));
}

/**
Expand All @@ -83,7 +84,7 @@ public Map<String, Object> load(InputStream fileInputStream) throws IOException
* @throws IOException exception in case any problem in reading json.
*/
public Map<String, Object> load(String objectAsJsonString) throws IOException {
return convertJsonStringToMap(objectAsJsonString);
return convertJsonOrYamlStringToMap(objectAsJsonString);
}

/**
Expand Down Expand Up @@ -369,6 +370,32 @@ public Map<String, Object> delete(String namespace) {
return makeCall(fetchUrl(namespace, null), null, HttpCallMethod.DELETE);
}

/**
* Delete all custom resources in a specific namespace
*
* @param namespace desired namespace
* @param cascading whether dependent object need to be orphaned or not. If true/false, the "orphan"
* finalizer will be added to/removed from the object's finalizers list.
* @return deleted objects as HashMap
* @throws IOException in case of any network/parsing exception
*/
public Map<String, Object> delete(String namespace, boolean cascading) throws IOException {
return makeCall(fetchUrl(namespace, null), objectMapper.writeValueAsString(fetchDeleteOptions(cascading, null)), HttpCallMethod.DELETE);
}

/**
* Delete all custom resources in a specific namespace
*
* @param namespace desired namespace
* @param deleteOptions object provided by Kubernetes API for more fine grained control over deletion.
* For more information please see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#deleteoptions-v1-meta
* @return deleted object as HashMap
* @throws IOException in case of any network/object parse problems
*/
public Map<String, Object> delete(String namespace, DeleteOptions deleteOptions) throws IOException {
return makeCall(fetchUrl(namespace, null), objectMapper.writeValueAsString(deleteOptions), HttpCallMethod.DELETE);
}

/**
* Delete a custom resource in a specific namespace
*
Expand All @@ -380,6 +407,53 @@ public Map<String, Object> delete(String namespace, String name) {
return makeCall(fetchUrl(namespace, null) + name, null, HttpCallMethod.DELETE);
}

/**
* Delete a custom resource in a specific namespace
*
* @param namespace required namespace
* @param name required name of custom resource
* @param cascading whether dependent object need to be orphaned or not. If true/false, the "orphan"
* finalizer will be added to/removed from the object's finalizers list.
* @return deleted objects as HashMap
* @throws IOException exception related to network/object parsing
*/
public Map<String, Object> delete(String namespace, String name, boolean cascading) throws IOException {
return makeCall(fetchUrl(namespace, null) + name, objectMapper.writeValueAsString(fetchDeleteOptions(cascading, null)), HttpCallMethod.DELETE);
}

/**
* Delete a custom resource in a specific namespace
*
* @param namespace required namespace
* @param name required name of custom resource
* @param propagationPolicy Whether and how garbage collection will be performed. Either this field or OrphanDependents
* may be set, but not both. The default policy is decided by the existing finalizer set in
* the metadata.finalizers and the resource-specific default policy.
* Acceptable values are:
* 'Orphan' - orphan the dependents;
* 'Background' - allow the garbage collector to delete the dependents in the background;
* 'Foreground' - a cascading policy that deletes all dependents in the foreground.
* @return deleted object as HashMap
* @throws IOException in case of network/object parse exception
*/
public Map<String, Object> delete(String namespace, String name, String propagationPolicy) throws IOException {
return makeCall(fetchUrl(namespace, null) + name, objectMapper.writeValueAsString(fetchDeleteOptions(false, propagationPolicy)) , HttpCallMethod.DELETE);
}

/**
* Delete a custom resource in a specific namespace
*
* @param namespace required namespace
* @param name name of custom resource
* @param deleteOptions object provided by Kubernetes API for more fine grained control over deletion.
* For more information please see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#deleteoptions-v1-meta
* @return deleted object as HashMap
* @throws IOException in case of any network/object parse exception
*/
public Map<String, Object> delete(String namespace, String name, DeleteOptions deleteOptions) throws IOException {
return makeCall(fetchUrl(namespace, null) + name, objectMapper.writeValueAsString(deleteOptions), HttpCallMethod.DELETE);
}

/**
* Watch custom resources in a specific namespace. Here Watcher is provided
* for string type only. User has to deserialize object itself.
Expand Down Expand Up @@ -516,7 +590,15 @@ private Map<String, Object> createOrReplaceJsonStringObject(String namespace, St
return ret;
}

private Map<String, Object> convertJsonStringToMap(String objectAsString) throws IOException {
/**
* Converts yaml/json object as string to a HashMap.
* This method checks whether
*
* @param objectAsString JSON or Yaml object as plain string
* @return object being deserialized to a HashMap
* @throws IOException in case of any parsing error
*/
private Map<String, Object> convertJsonOrYamlStringToMap(String objectAsString) throws IOException {
HashMap<String, Object> retVal = null;
if (IOHelpers.isJSONValid(objectAsString)) {
retVal = objectMapper.readValue(objectAsString, HashMap.class);
Expand Down Expand Up @@ -577,9 +659,7 @@ private Map<String, Object> makeCall(String url, String body, HttpCallMethod cal
if (response.isSuccessful()) {
return objectMapper.readValue(response.body().string(), HashMap.class);
} else {
String message = String.format("Error while performing the call to %s. Response code: %s", url, response.code());
Status status = createStatus(response);
throw new KubernetesClientException(message, response.code(), status);
throw requestFailure(request, createStatus(response));
}
} catch(Exception e) {
throw KubernetesClientException.launderThrowable(e);
Expand Down Expand Up @@ -612,6 +692,8 @@ private Request getRequest(String url, String body, HttpCallMethod httpCallMetho
Request.Builder requestBuilder = new Request.Builder();
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), body);
switch(httpCallMethod) {
case DELETE:
return requestBuilder.delete(requestBody).url(url).build();
case POST:
return requestBuilder.post(requestBody).url(url).build();
case PUT:
Expand All @@ -624,9 +706,19 @@ private String appendResourceVersionInObject(String namespace, String customReso
Map<String, Object> oldObject = get(namespace, customResourceName);
String resourceVersion = ((Map<String, Object>)oldObject.get("metadata")).get("resourceVersion").toString();

Map<String, Object> newObject = convertJsonStringToMap(customResourceAsJsonString);
Map<String, Object> newObject = convertJsonOrYamlStringToMap(customResourceAsJsonString);
((Map<String, Object>)newObject.get("metadata")).put("resourceVersion", resourceVersion);

return objectMapper.writeValueAsString(newObject);
}

private DeleteOptions fetchDeleteOptions(boolean cascading, String propagationPolicy) {
DeleteOptions deleteOptions = new DeleteOptions();
if (propagationPolicy != null) {
deleteOptions.setPropagationPolicy(propagationPolicy);
} else {
deleteOptions.setOrphanDependents(!cascading);
}
return deleteOptions;
}
}
Expand Up @@ -57,15 +57,14 @@ private static void copy(Reader reader, Writer writer) throws IOException {
}
}

public static boolean isJSONValid(String json) throws IOException {
boolean valid = true;
public static boolean isJSONValid(String json) {
try{
ObjectMapper objectMapper = Serialization.jsonMapper();
objectMapper.readTree(json);
} catch(JsonProcessingException e){
valid = false;
return false;
}
return valid;
return true;
}

public static String convertYamlToJson(String yaml) throws IOException {
Expand Down
Expand Up @@ -25,6 +25,8 @@
import java.util.List;
import java.util.Map;

import io.fabric8.kubernetes.api.model.DeleteOptions;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.Rule;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -177,6 +179,58 @@ public void testDelete() {
assertEquals("Success", result.get("status"));
}

@Test
public void testCascadingDeletion() throws IOException, InterruptedException {
server.expect().delete().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, "{\"metadata\":{},\"apiVersion\":\"v1\",\"kind\":\"Status\",\"details\":{\"name\":\"prometheus-example-rules\",\"group\":\"monitoring.coreos.com\",\"kind\":\"prometheusrules\",\"uid\":\"b3d085bd-6a5c-11e9-8787-525400b18c1d\"},\"status\":\"Success\"}").once();

KubernetesClient client = server.getClient();
Map<String, Object> result = client.customResource(customResourceDefinitionContext)
.delete("ns1", "example-hello", true);
assertEquals("Success", result.get("status"));
DeleteOptions expectedDeleteOptions = new DeleteOptions();
expectedDeleteOptions.setPropagationPolicy("Orphan");

RecordedRequest request = server.getLastRequest();
assertEquals("DELETE", request.getMethod());
assertEquals("{\"apiVersion\":\"v1\",\"kind\":\"DeleteOptions\",\"orphanDependents\":false}",
request.getBody().readUtf8());
}

@Test
public void testPropagationPolicy() throws IOException, InterruptedException {
server.expect().delete().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, "{\"metadata\":{},\"apiVersion\":\"v1\",\"kind\":\"Status\",\"details\":{\"name\":\"prometheus-example-rules\",\"group\":\"monitoring.coreos.com\",\"kind\":\"prometheusrules\",\"uid\":\"b3d085bd-6a5c-11e9-8787-525400b18c1d\"},\"status\":\"Success\"}").once();

KubernetesClient client = server.getClient();
Map<String, Object> result = client.customResource(customResourceDefinitionContext)
.delete("ns1", "example-hello", "Orphan");
assertEquals("Success", result.get("status"));

RecordedRequest request = server.getLastRequest();
assertEquals("DELETE", request.getMethod());
assertEquals("{\"apiVersion\":\"v1\",\"kind\":\"DeleteOptions\",\"propagationPolicy\":\"Orphan\"}",
request.getBody().readUtf8());
}

@Test
public void testDeleteOptions() throws InterruptedException, IOException {
server.expect().delete().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, "{\"metadata\":{},\"apiVersion\":\"v1\",\"kind\":\"Status\",\"details\":{\"name\":\"prometheus-example-rules\",\"group\":\"monitoring.coreos.com\",\"kind\":\"prometheusrules\",\"uid\":\"b3d085bd-6a5c-11e9-8787-525400b18c1d\"},\"status\":\"Success\"}").once();

KubernetesClient client = server.getClient();

DeleteOptions deleteOptions = new DeleteOptions();
deleteOptions.setGracePeriodSeconds(0L);
deleteOptions.setPropagationPolicy("Orphan");
Map<String, Object> result = client.customResource(customResourceDefinitionContext)
.delete("ns1", "example-hello", deleteOptions);

assertEquals("Success", result.get("status"));

RecordedRequest request = server.getLastRequest();
assertEquals("DELETE", request.getMethod());
assertEquals("{\"apiVersion\":\"v1\",\"kind\":\"DeleteOptions\",\"gracePeriodSeconds\":0,\"propagationPolicy\":\"Orphan\"}",
request.getBody().readUtf8());;
}

@Test
public void testDeleteWithNamespaceMismatch() {
Assertions.assertThrows(KubernetesClientException.class, () -> {
Expand Down

0 comments on commit deb1164

Please sign in to comment.