diff --git a/README.md b/README.md index 1f54cb7..3444e94 100644 --- a/README.md +++ b/README.md @@ -588,7 +588,7 @@ Please note that the resources are deleted instantly and cannot be recovered onc with the resource is permanently lost. ```console -Usage: kafkactl delete [-hv] [--dry-run] [--force] [-n=] ([ [-V[=]]] | [[-f=] [-R]]) +Usage: kafkactl delete [-hv] [--dry-run] [--force] [--cascade] [-n=] ([ [-V[=]]] | [[-f=] [-R]]) Description: Delete a resource. Parameters: @@ -598,6 +598,7 @@ Parameters: Options: -c, --context= Override context defined in config. + --cascade Cascade delete related connectors from Ns4Kafka. Only for connect cluster. --dry-run Does not persist resources. Validate only. --execute This option is mandatory to delete resources with wildcard. -f, --file= YAML file or directory containing resources to delete. @@ -619,6 +620,8 @@ kafkactl delete -f resource.yml kafkactl delete topic myTopic kafkactl delete connector myConnector --force kafkactl delete connect-cluster myConnectCluster --force +kafkactl delete connect-cluster myConnectCluster --cascade +kafkactl delete connect-cluster myConnectCluster --cascade --force kafkactl delete topic *-test kafkactl delete schema * kafkactl delete schema mySchema -V latest diff --git a/src/main/java/com/michelin/kafkactl/client/NamespacedResourceClient.java b/src/main/java/com/michelin/kafkactl/client/NamespacedResourceClient.java index 3d0216e..bf5347d 100644 --- a/src/main/java/com/michelin/kafkactl/client/NamespacedResourceClient.java +++ b/src/main/java/com/michelin/kafkactl/client/NamespacedResourceClient.java @@ -19,6 +19,7 @@ package com.michelin.kafkactl.client; import com.michelin.kafkactl.model.Resource; +import com.michelin.kafkactl.model.request.DeleteResourceRequest; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Body; @@ -27,6 +28,7 @@ import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.QueryValue; +import io.micronaut.http.annotation.RequestBean; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.ReadTimeoutException; import io.micronaut.retry.annotation.Retryable; @@ -39,28 +41,16 @@ public interface NamespacedResourceClient { /** * Delete a given resource. * - * @param namespace The namespace - * @param kind The kind of resource - * @param name The name of the resource - * @param token The auth token - * @param version The version of the resource, for schemas only. - * @param dryrun Is dry-run mode or not? + * @param request The delete resource request * @return The delete response */ - @Delete("{namespace}/{kind}{?name,version,dryrun,force}") + @Delete("{namespace}/{kind}{?name,version,dryrun,force,cascade}") @Retryable( delay = "${kafkactl.retry.delay}", attempts = "${kafkactl.retry.attempt}", multiplier = "${kafkactl.retry.multiplier}", includes = ReadTimeoutException.class) - HttpResponse> delete( - String namespace, - String kind, - @Header("Authorization") String token, - @QueryValue String name, - @Nullable @QueryValue String version, - @QueryValue boolean dryrun, - @QueryValue boolean force); + HttpResponse> delete(@RequestBean DeleteResourceRequest request); /** * Apply a given resource. diff --git a/src/main/java/com/michelin/kafkactl/command/Delete.java b/src/main/java/com/michelin/kafkactl/command/Delete.java index a954813..218a733 100644 --- a/src/main/java/com/michelin/kafkactl/command/Delete.java +++ b/src/main/java/com/michelin/kafkactl/command/Delete.java @@ -21,6 +21,7 @@ import com.michelin.kafkactl.hook.DryRunHook; import com.michelin.kafkactl.model.ApiResource; import com.michelin.kafkactl.model.Resource; +import com.michelin.kafkactl.model.request.DeleteResourceRequest; import com.michelin.kafkactl.service.FileService; import com.michelin.kafkactl.service.FormatService; import com.michelin.kafkactl.service.ResourceService; @@ -71,6 +72,11 @@ public class Delete extends DryRunHook { description = "Force delete resource from Ns4Kafka. Only for connector and connect cluster.") public boolean force; + @Option( + names = {"--cascade"}, + description = "Cascade delete related connectors from Ns4Kafka. Only for connect cluster.") + public boolean cascade; + /** * Run the "delete" command. * @@ -101,26 +107,32 @@ public Integer onAuthSuccess() { // Process each document individually, return 0 when all succeed int errors = resources.stream() - .map(resource -> { + .mapToInt(resource -> { ApiResource apiResource = apiResourcesService .getResourceDefinitionByKind(resource.getKind()) .orElseThrow(); Map spec = resource.getSpec(); + String version = spec != null && spec.containsKey(VERSION) + ? spec.get(VERSION).toString() + : null; return resourceService.delete( - apiResource, - namespace, - resource.getMetadata().getName(), - (spec != null && spec.containsKey(VERSION) - ? spec.get(VERSION).toString() - : null), - dryRun, - force, - commandSpec); + apiResource, + new DeleteResourceRequest( + namespace, + apiResource.getPath(), + null, + resource.getMetadata().getName(), + version, + dryRun, + force, + cascade), + commandSpec) + ? 0 + : 1; }) - .mapToInt(value -> value ? 0 : 1) .sum(); - return errors > 0 ? 1 : 0; + return errors == 0 ? 0 : 1; } catch (HttpClientResponseException e) { formatService.displayError(e, commandSpec); return 1; diff --git a/src/main/java/com/michelin/kafkactl/model/request/DeleteResourceRequest.java b/src/main/java/com/michelin/kafkactl/model/request/DeleteResourceRequest.java new file mode 100644 index 0000000..fb012d3 --- /dev/null +++ b/src/main/java/com/michelin/kafkactl/model/request/DeleteResourceRequest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.michelin.kafkactl.model.request; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.QueryValue; + +/** Delete resource request. */ +@Introspected +public record DeleteResourceRequest( + @PathVariable String namespace, + @PathVariable String kind, + @Nullable @Header("Authorization") String token, + @QueryValue String name, + @Nullable @QueryValue String version, + @QueryValue boolean dryrun, + @QueryValue boolean force, + @QueryValue boolean cascade) { + public DeleteResourceRequest withToken(String token) { + return new DeleteResourceRequest(namespace, kind, token, name, version, dryrun, force, cascade); + } +} diff --git a/src/main/java/com/michelin/kafkactl/service/ResourceService.java b/src/main/java/com/michelin/kafkactl/service/ResourceService.java index f76615c..7c12176 100644 --- a/src/main/java/com/michelin/kafkactl/service/ResourceService.java +++ b/src/main/java/com/michelin/kafkactl/service/ResourceService.java @@ -33,6 +33,7 @@ import com.michelin.kafkactl.model.Output; import com.michelin.kafkactl.model.Resource; import com.michelin.kafkactl.model.SubjectCompatibility; +import com.michelin.kafkactl.model.request.DeleteResourceRequest; import io.confluent.kafka.schemaregistry.avro.AvroSchema; import io.confluent.kafka.schemaregistry.client.rest.entities.SchemaReference; import io.micronaut.core.annotation.Nullable; @@ -242,32 +243,17 @@ public HttpResponse apply( * Delete a given resource. * * @param apiResource The resource type - * @param namespace The namespace - * @param name The resource name or wildcard - * @param version The version of the resource, for schemas only - * @param dryRun Is dry run mode or not? + * @param request The delete resource request * @param commandSpec The command that triggered the action * @return true if deletion succeeded, false otherwise */ - public boolean delete( - ApiResource apiResource, - String namespace, - String name, - @Nullable String version, - boolean dryRun, - boolean force, - CommandSpec commandSpec) { + public boolean delete(ApiResource apiResource, DeleteResourceRequest request, CommandSpec commandSpec) { try { + DeleteResourceRequest authorizedRequest = request.withToken(loginService.getAuthorization()); HttpResponse> response = apiResource.isNamespaced() - ? namespacedClient.delete( - namespace, - apiResource.getPath(), - loginService.getAuthorization(), - name, - version, - dryRun, - force) - : nonNamespacedClient.delete(loginService.getAuthorization(), apiResource.getPath(), name, dryRun); + ? namespacedClient.delete(authorizedRequest) + : nonNamespacedClient.delete( + loginService.getAuthorization(), apiResource.getPath(), request.name(), request.dryrun()); // Micronaut does not throw exception on 404, so produce a 404 manually if (response.getStatus().equals(HttpStatus.NOT_FOUND)) { @@ -282,11 +268,11 @@ public boolean delete( .commandLine() .getOut() .println(formatService.prettifyKind(apiResource.getKind()) + " \"" + resourceName + "\"" - + (version != null ? " version " + version : "") + " deleted.")); + + (request.version() != null ? " version " + request.version() : "") + " deleted.")); return true; } catch (HttpClientResponseException exception) { - formatService.displayError(exception, apiResource.getKind(), name, commandSpec); + formatService.displayError(exception, apiResource.getKind(), request.name(), commandSpec); return false; } } diff --git a/src/test/java/com/michelin/kafkactl/command/DeleteTest.java b/src/test/java/com/michelin/kafkactl/command/DeleteTest.java index 05fa45f..2de072a 100644 --- a/src/test/java/com/michelin/kafkactl/command/DeleteTest.java +++ b/src/test/java/com/michelin/kafkactl/command/DeleteTest.java @@ -31,6 +31,7 @@ import com.michelin.kafkactl.model.ApiResource; import com.michelin.kafkactl.model.Resource; +import com.michelin.kafkactl.model.request.DeleteResourceRequest; import com.michelin.kafkactl.property.KafkactlProperties; import com.michelin.kafkactl.service.ApiResourcesService; import com.michelin.kafkactl.service.ConfigService; @@ -243,8 +244,7 @@ void shouldDeleteByFile() { .build(); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -282,8 +282,7 @@ void shouldDeleteOneVersionByFile() { .build(); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -308,8 +307,7 @@ void shouldDeleteByName() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -334,15 +332,17 @@ void shouldForceDeleteConnectorByName() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); int code = cmd.execute("connector", "prefix.connector", "-n", "namespace", "--force"); assertEquals(0, code); verify(resourceService) - .delete(any(), eq("namespace"), eq("prefix.connector"), eq(null), eq(false), eq(true), any()); + .delete( + any(), + eq(deleteRequest("namespace", "connectors", "prefix.connector", null, false, true, false)), + any()); } @Test @@ -360,14 +360,73 @@ void shouldForceDeleteConnectClusterByName() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); int code = cmd.execute("connect-cluster", "my-cluster", "-n", "namespace", "--force"); assertEquals(0, code); - verify(resourceService).delete(any(), eq("namespace"), eq("my-cluster"), eq(null), eq(false), eq(true), any()); + verify(resourceService) + .delete( + any(), + eq(deleteRequest("namespace", "connect-clusters", "my-cluster", null, false, true, false)), + any()); + } + + @Test + void shouldCascadeDeleteConnectClusterByName() { + when(configService.isCurrentContextValid()).thenReturn(true); + when(loginService.doAuthenticate(any(), anyBoolean())).thenReturn(true); + + ApiResource apiResource = ApiResource.builder() + .kind("ConnectCluster") + .path("connect-clusters") + .names(List.of("connect-clusters", "connect-cluster", "cc")) + .namespaced(true) + .synchronizable(true) + .build(); + + when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); + when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); + when(resourceService.delete(any(), any(), any())).thenReturn(true); + + CommandLine cmd = new CommandLine(delete); + + int code = cmd.execute("connect-cluster", "my-cluster", "-n", "namespace", "--cascade"); + assertEquals(0, code); + verify(resourceService) + .delete( + any(), + eq(deleteRequest("namespace", "connect-clusters", "my-cluster", null, false, false, true)), + any()); + } + + @Test + void shouldCascadeForceDeleteConnectClusterByName() { + when(configService.isCurrentContextValid()).thenReturn(true); + when(loginService.doAuthenticate(any(), anyBoolean())).thenReturn(true); + + ApiResource apiResource = ApiResource.builder() + .kind("ConnectCluster") + .path("connect-clusters") + .names(List.of("connect-clusters", "connect-cluster", "cc")) + .namespaced(true) + .synchronizable(true) + .build(); + + when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); + when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); + when(resourceService.delete(any(), any(), any())).thenReturn(true); + + CommandLine cmd = new CommandLine(delete); + + int code = cmd.execute("connect-cluster", "my-cluster", "-n", "namespace", "--cascade", "--force"); + assertEquals(0, code); + verify(resourceService) + .delete( + any(), + eq(deleteRequest("namespace", "connect-clusters", "my-cluster", null, false, true, true)), + any()); } @Test @@ -385,8 +444,7 @@ void shouldDeleteOneVersionByName() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -421,8 +479,7 @@ void shouldNotDeleteByFileWhenInDryRunMode() { .build(); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -461,8 +518,7 @@ void shouldNotDeleteByFileWhenFail() { .build(); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(false); + when(resourceService.delete(any(), any(), any())).thenReturn(false); CommandLine cmd = new CommandLine(delete); @@ -515,8 +571,7 @@ void shouldDeleteByNameWithWildcard() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -541,7 +596,7 @@ void shouldNotDeleteByNameWithWildcardWithoutExecute() { assertTrue(sw.toString() .contains("Rerun the command with option --dry-run to see the resources that will be deleted.")); assertTrue(sw.toString().contains("Rerun the command with option --execute to execute this operation.")); - verify(resourceService, never()).delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any()); + verify(resourceService, never()).delete(any(), any(), any()); } @Test @@ -559,7 +614,7 @@ void shouldNotDeleteByNameWithQuestionMarkWildcardWithoutExecute() { assertTrue(sw.toString() .contains("Rerun the command with option --dry-run to see the resources that will be deleted.")); assertTrue(sw.toString().contains("Rerun the command with option --execute to execute this operation.")); - verify(resourceService, never()).delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any()); + verify(resourceService, never()).delete(any(), any(), any()); } @Test @@ -577,8 +632,7 @@ void shouldNotDeleteByNameWithWildcardInDryRunMode() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -604,8 +658,7 @@ void shouldNotDeleteByNameWithWildcardWithExecuteInDryRunMode() { when(apiResourcesService.getResourceDefinitionByName(any())).thenReturn(Optional.of(apiResource)); when(apiResourcesService.getResourceDefinitionByKind(any())).thenReturn(Optional.of(apiResource)); - when(resourceService.delete(any(), any(), any(), any(), anyBoolean(), anyBoolean(), any())) - .thenReturn(true); + when(resourceService.delete(any(), any(), any())).thenReturn(true); CommandLine cmd = new CommandLine(delete); StringWriter sw = new StringWriter(); @@ -616,4 +669,15 @@ void shouldNotDeleteByNameWithWildcardWithExecuteInDryRunMode() { assertTrue(sw.toString().contains("Dry run execution.")); assertFalse(sw.toString().contains("You are about to potentially delete multiple resources with wildcard")); } + + private static DeleteResourceRequest deleteRequest( + String namespace, + String kind, + String name, + String version, + boolean dryRun, + boolean force, + boolean cascade) { + return new DeleteResourceRequest(namespace, kind, null, name, version, dryRun, force, cascade); + } } diff --git a/src/test/java/com/michelin/kafkactl/service/ResourceServiceTest.java b/src/test/java/com/michelin/kafkactl/service/ResourceServiceTest.java index 5428d0b..380acd1 100644 --- a/src/test/java/com/michelin/kafkactl/service/ResourceServiceTest.java +++ b/src/test/java/com/michelin/kafkactl/service/ResourceServiceTest.java @@ -35,7 +35,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.eq; @@ -49,6 +48,7 @@ import com.michelin.kafkactl.model.ApiResource; import com.michelin.kafkactl.model.Resource; import com.michelin.kafkactl.model.SubjectCompatibility; +import com.michelin.kafkactl.model.request.DeleteResourceRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientResponseException; @@ -800,7 +800,7 @@ void shouldDeleteNamespacedResource() { .metadata(Resource.Metadata.builder().name("name").build()) .build(); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) + when(namespacedClient.delete(any())) .thenReturn(HttpResponse.ok(List.of(deletedResource)).header("X-Ns4kafka-Result", "created")); ApiResource apiResource = ApiResource.builder() @@ -811,8 +811,8 @@ void shouldDeleteNamespacedResource() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "name", null, false, false, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "name", null, false, false, false), cmd.getCommandSpec()); assertTrue(actual); assertTrue(sw.toString().contains("Topic \"name\" deleted.")); @@ -834,7 +834,7 @@ void shouldDeleteMultipleNamespacedResources() { .metadata(Resource.Metadata.builder().name("name2").build()) .build(); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) + when(namespacedClient.delete(any())) .thenReturn(HttpResponse.ok(List.of(deletedResource1, deletedResource2)) .header("X-Ns4kafka-Result", "created")); @@ -846,8 +846,8 @@ void shouldDeleteMultipleNamespacedResources() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "name*", null, false, false, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "name*", null, false, false, false), cmd.getCommandSpec()); assertTrue(actual); assertTrue(sw.toString().contains("Topic \"name1\" deleted.")); @@ -866,7 +866,7 @@ void shouldDeleteNamespacedResourceWithVersion() { .metadata(Resource.Metadata.builder().name("name").build()) .build(); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) + when(namespacedClient.delete(any())) .thenReturn(HttpResponse.ok(List.of(deletedResource)).header("X-Ns4kafka-Result", "created")); ApiResource apiResource = ApiResource.builder() @@ -877,8 +877,8 @@ void shouldDeleteNamespacedResourceWithVersion() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "name", "latest", false, false, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "name", "latest", false, false, false), cmd.getCommandSpec()); assertTrue(actual); assertTrue(sw.toString().contains("Topic \"name\" version latest deleted.")); @@ -896,8 +896,7 @@ void shouldForceDeleteConnector() { .metadata(Resource.Metadata.builder().name("connector").build()) .build(); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) - .thenReturn(HttpResponse.ok(List.of(deletedResource))); + when(namespacedClient.delete(any())).thenReturn(HttpResponse.ok(List.of(deletedResource))); ApiResource apiResource = ApiResource.builder() .kind(CONNECTOR) @@ -907,12 +906,11 @@ void shouldForceDeleteConnector() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "connector", null, false, true, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "connector", null, false, true, false), cmd.getCommandSpec()); assertTrue(actual); - verify(namespacedClient) - .delete(eq("namespace"), eq("connectors"), any(), eq("connector"), isNull(), eq(false), eq(true)); + verify(namespacedClient).delete(eq(deleteRequest(apiResource, "connector", null, false, true, false))); } @Test @@ -927,8 +925,7 @@ void shouldForceDeleteConnectCluster() { .metadata(Resource.Metadata.builder().name("cluster").build()) .build(); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) - .thenReturn(HttpResponse.ok(List.of(deletedResource))); + when(namespacedClient.delete(any())).thenReturn(HttpResponse.ok(List.of(deletedResource))); ApiResource apiResource = ApiResource.builder() .kind(CONNECT_CLUSTER) @@ -938,12 +935,98 @@ void shouldForceDeleteConnectCluster() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "cluster", null, false, true, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "cluster", null, false, true, false), cmd.getCommandSpec()); assertTrue(actual); - verify(namespacedClient) - .delete(eq("namespace"), eq("connect-clusters"), any(), eq("cluster"), isNull(), eq(false), eq(true)); + verify(namespacedClient).delete(eq(deleteRequest(apiResource, "cluster", null, false, true, false))); + } + + @Test + void shouldCascadeDeleteConnectCluster() { + CommandLine cmd = new CommandLine(new Kafkactl()); + StringWriter sw = new StringWriter(); + cmd.setOut(new PrintWriter(sw)); + + doCallRealMethod().when(formatService).prettifyKind(any()); + + Resource deletedResource = Resource.builder() + .metadata(Resource.Metadata.builder().name("cluster").build()) + .build(); + + when(namespacedClient.delete(any())).thenReturn(HttpResponse.ok(List.of(deletedResource))); + + ApiResource apiResource = ApiResource.builder() + .kind(CONNECT_CLUSTER) + .path("connect-clusters") + .names(List.of("connect-clusters", "connect-cluster", "cc")) + .namespaced(true) + .synchronizable(true) + .build(); + + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "cluster", null, false, false, true), cmd.getCommandSpec()); + + assertTrue(actual); + verify(namespacedClient).delete(eq(deleteRequest(apiResource, "cluster", null, false, false, true))); + } + + @Test + void shouldForceCascadeDeleteConnectCluster() { + CommandLine cmd = new CommandLine(new Kafkactl()); + StringWriter sw = new StringWriter(); + cmd.setOut(new PrintWriter(sw)); + + doCallRealMethod().when(formatService).prettifyKind(any()); + + Resource deletedResource = Resource.builder() + .metadata(Resource.Metadata.builder().name("cluster").build()) + .build(); + + when(namespacedClient.delete(any())).thenReturn(HttpResponse.ok(List.of(deletedResource))); + + ApiResource apiResource = ApiResource.builder() + .kind(CONNECT_CLUSTER) + .path("connect-clusters") + .names(List.of("connect-clusters", "connect-cluster", "cc")) + .namespaced(true) + .synchronizable(true) + .build(); + + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "cluster", null, false, true, true), cmd.getCommandSpec()); + + assertTrue(actual); + verify(namespacedClient).delete(eq(deleteRequest(apiResource, "cluster", null, false, true, true))); + } + + @Test + void shouldPassForceCascadeForUnsupportedResource() { + CommandLine cmd = new CommandLine(new Kafkactl()); + StringWriter sw = new StringWriter(); + cmd.setOut(new PrintWriter(sw)); + + doCallRealMethod().when(formatService).prettifyKind(any()); + + Resource deletedResource = Resource.builder() + .metadata(Resource.Metadata.builder().name("topic").build()) + .build(); + + when(namespacedClient.delete(any())).thenReturn(HttpResponse.ok(List.of(deletedResource))); + + ApiResource apiResource = ApiResource.builder() + .kind("Topic") + .path("topics") + .names(List.of("topics", "topic", "to")) + .namespaced(true) + .synchronizable(true) + .build(); + + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "topic", null, false, true, true), cmd.getCommandSpec()); + + assertTrue(actual); + verify(namespacedClient).delete(eq(deleteRequest(apiResource, "topic", null, false, true, true))); } @Test @@ -969,8 +1052,8 @@ void shouldDeleteNonNamespacedResource() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "name", null, false, false, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "name", null, false, false, false), cmd.getCommandSpec()); assertTrue(actual); assertTrue(sw.toString().contains("Topic \"name\" deleted.")); @@ -1004,8 +1087,8 @@ void shouldDeleteMultipleNonNamespacedResources() { .synchronizable(true) .build(); - boolean actual = - resourceService.delete(apiResource, "namespace", "name*", null, false, false, cmd.getCommandSpec()); + boolean actual = resourceService.delete( + apiResource, deleteRequest(apiResource, "name*", null, false, false, false), cmd.getCommandSpec()); assertTrue(actual); assertTrue(sw.toString().contains("Topic \"name1\" deleted.")); @@ -1025,11 +1108,12 @@ void shouldDeleteNamespacedResourceAndHandleHttpResponseException() { CommandLine cmd = new CommandLine(new Kafkactl()); HttpClientResponseException exception = new HttpClientResponseException("error", HttpResponse.serverError()); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) - .thenThrow(exception); + when(namespacedClient.delete(any())).thenThrow(exception); boolean actual = resourceService.delete( - apiResource, "namespace", "prefix.topic", null, false, false, cmd.getCommandSpec()); + apiResource, + deleteRequest(apiResource, "prefix.topic", null, false, false, false), + cmd.getCommandSpec()); assertFalse(actual); verify(formatService).displayError(exception, "Topic", "prefix.topic", cmd.getCommandSpec()); @@ -1047,11 +1131,12 @@ void shouldNotDeleteWhenNamespacedResourceNotFound() { CommandLine cmd = new CommandLine(new Kafkactl()); - when(namespacedClient.delete(any(), any(), any(), any(), any(), anyBoolean(), anyBoolean())) - .thenReturn(HttpResponse.notFound()); + when(namespacedClient.delete(any())).thenReturn(HttpResponse.notFound()); boolean actual = resourceService.delete( - apiResource, "namespace", "prefix.topic", null, false, false, cmd.getCommandSpec()); + apiResource, + deleteRequest(apiResource, "prefix.topic", null, false, false, false), + cmd.getCommandSpec()); assertFalse(actual); verify(formatService) @@ -1078,7 +1163,9 @@ void shouldNotDeleteWhenNonNamespacedResourceNotFound() { when(nonNamespacedClient.delete(any(), any(), any(), anyBoolean())).thenReturn(HttpResponse.notFound()); boolean actual = resourceService.delete( - apiResource, "namespace", "prefix.topic", null, false, false, cmd.getCommandSpec()); + apiResource, + deleteRequest(apiResource, "prefix.topic", null, false, false, false), + cmd.getCommandSpec()); assertFalse(actual); verify(formatService) @@ -1090,6 +1177,12 @@ void shouldNotDeleteWhenNonNamespacedResourceNotFound() { eq(cmd.getCommandSpec())); } + private static DeleteResourceRequest deleteRequest( + ApiResource apiResource, String name, String version, boolean dryRun, boolean force, boolean cascade) { + return new DeleteResourceRequest( + "namespace", apiResource.getPath(), null, name, version, dryRun, force, cascade); + } + @Test void shouldImportAllResources() { ApiResource apiResource = ApiResource.builder()