diff --git a/cli/src/main/java/com/michelin/ns4kafka/cli/DeleteSubcommand.java b/cli/src/main/java/com/michelin/ns4kafka/cli/DeleteSubcommand.java index 1a27c936..77941c6a 100644 --- a/cli/src/main/java/com/michelin/ns4kafka/cli/DeleteSubcommand.java +++ b/cli/src/main/java/com/michelin/ns4kafka/cli/DeleteSubcommand.java @@ -1,17 +1,24 @@ package com.michelin.ns4kafka.cli; import com.michelin.ns4kafka.cli.models.ApiResource; +import com.michelin.ns4kafka.cli.models.ObjectMeta; +import com.michelin.ns4kafka.cli.models.Resource; import com.michelin.ns4kafka.cli.services.ApiResourcesService; +import com.michelin.ns4kafka.cli.services.FileService; import com.michelin.ns4kafka.cli.services.LoginService; import com.michelin.ns4kafka.cli.services.ResourceService; import picocli.CommandLine; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import javax.inject.Inject; +import java.io.File; +import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; +import java.util.stream.Collectors; @Command(name = "delete", description = "Delete a resource") public class DeleteSubcommand implements Callable { @@ -25,13 +32,33 @@ public class DeleteSubcommand implements Callable { public LoginService loginService; @Inject public ApiResourcesService apiResourcesService; + @Inject + public FileService fileService; @CommandLine.ParentCommand public KafkactlCommand kafkactlCommand; - @Parameters(index = "0", description = "Resource type", arity = "1") - public String resourceType; - @Parameters(index = "1", description = "Resource name", arity = "1") - public String name; + + @ArgGroup(exclusive = true, multiplicity = "1") + public EitherOf config; + + static class EitherOf { + @ArgGroup(exclusive = false) + public ByName nameConfig; + @ArgGroup(exclusive = false) + public ByFile fileConfig; + } + static class ByName { + @Parameters(index = "0", description = "Resource type", arity = "1") + public String resourceType; + @Parameters(index = "1", description = "Resource name", arity = "1") + public String name; + } + static class ByFile { + @Option(names = {"-f", "--file"}, description = "YAML File or Directory containing YAML resources") + public Optional file; + @Option(names = {"-R", "--recursive"}, description = "Enable recursive search in Directory") + public boolean recursive; + } @Option(names = {"--dry-run"}, description = "Does not persist operation. Validate only") public boolean dryRun; @@ -52,21 +79,64 @@ public Integer call() { String namespace = kafkactlCommand.optionalNamespace.orElse(kafkactlConfig.getCurrentNamespace()); - Optional optionalApiResource = apiResourcesService.getResourceDefinitionFromCommandName(resourceType); - if (optionalApiResource.isEmpty()) { - throw new CommandLine.ParameterException(commandSpec.commandLine(), "The server doesn't have resource type [" + resourceType + "]"); + List resources; + + if (config.fileConfig != null && config.fileConfig.file.isPresent()) { + // 1. list all files to process + List yamlFiles = fileService.computeYamlFileList(config.fileConfig.file.get(), config.fileConfig.recursive); + if (yamlFiles.isEmpty()) { + throw new CommandLine.ParameterException(commandSpec.commandLine(), "Could not find yaml/yml files in " + config.fileConfig.file.get().getName()); + } + // 2 load each files + resources = fileService.parseResourceListFromFiles(yamlFiles); + } else { + Optional optionalApiResource = apiResourcesService.getResourceDefinitionFromCommandName(config.nameConfig.resourceType); + if (optionalApiResource.isEmpty()) { + throw new CommandLine.ParameterException(commandSpec.commandLine(), "The server doesn't have resource type [" + config.nameConfig.resourceType + "]"); + } + // Generate a single resource with minimum details from input + resources = List.of(Resource.builder() + .metadata(ObjectMeta.builder() + .name(config.nameConfig.name) + .namespace(namespace) + .build()) + .kind(optionalApiResource.get().getKind()) + .build()); } - ApiResource apiResource = optionalApiResource.get(); - - boolean deleted = resourceService.delete(apiResource, namespace, name, dryRun); - if (deleted) { - System.out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,green Success |@") + resourceType + "/" + name + " (deleted)"); - - return 0; + // 3. validate resource types from resources + List invalidResources = apiResourcesService.validateResourceTypes(resources); + if (!invalidResources.isEmpty()) { + String invalid = String.join(", ", invalidResources.stream().map(Resource::getKind).distinct().collect(Collectors.toList())); + throw new CommandLine.ParameterException(commandSpec.commandLine(), "The server doesn't have resource type [" + invalid + "]"); } - return 1; + // 4. validate namespace mismatch + List nsMismatch = resources.stream() + .filter(resource -> resource.getMetadata().getNamespace() != null && !resource.getMetadata().getNamespace().equals(namespace)) + .collect(Collectors.toList()); + if (!nsMismatch.isEmpty()) { + String invalid = String.join(", ", nsMismatch.stream().map(resource -> resource.getKind() + "/" + resource.getMetadata().getName()).distinct().collect(Collectors.toList())); + throw new CommandLine.ParameterException(commandSpec.commandLine(), "Namespace mismatch between kafkactl and yaml document [" + invalid + "]"); + } + List apiResources = apiResourcesService.getListResourceDefinition(); + + // 5. process each document individually, return 0 when all succeed + int errors = resources.stream() + .map(resource -> { + ApiResource apiResource = apiResources.stream() + .filter(apiRes -> apiRes.getKind().equals(resource.getKind())) + .findFirst() + .orElseThrow(); // already validated + boolean success = resourceService.delete(apiResource, namespace, resource.getMetadata().getName(), dryRun); + if(success) { + System.out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,green Success |@") + apiResource.getKind() + "/" + resource.getMetadata().getName() + " (deleted)"); + } + return success; + }) + .mapToInt(value -> value ? 0 : 1) + .sum(); + + return errors > 0 ? 1 : 0; } - -} +} \ No newline at end of file diff --git a/cli/src/main/java/com/michelin/ns4kafka/cli/client/NamespacedResourceClient.java b/cli/src/main/java/com/michelin/ns4kafka/cli/client/NamespacedResourceClient.java index 3055f977..be257972 100644 --- a/cli/src/main/java/com/michelin/ns4kafka/cli/client/NamespacedResourceClient.java +++ b/cli/src/main/java/com/michelin/ns4kafka/cli/client/NamespacedResourceClient.java @@ -11,7 +11,7 @@ public interface NamespacedResourceClient { @Delete("{namespace}/{kind}/{resourcename}{?dryrun}") - void delete( + HttpResponse delete( String namespace, String kind, String resourcename, diff --git a/cli/src/main/java/com/michelin/ns4kafka/cli/services/ResourceService.java b/cli/src/main/java/com/michelin/ns4kafka/cli/services/ResourceService.java index e1cea187..8a6fe3a2 100644 --- a/cli/src/main/java/com/michelin/ns4kafka/cli/services/ResourceService.java +++ b/cli/src/main/java/com/michelin/ns4kafka/cli/services/ResourceService.java @@ -6,6 +6,7 @@ import com.michelin.ns4kafka.cli.models.Resource; import com.michelin.ns4kafka.cli.models.Status; import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientResponseException; import javax.inject.Inject; @@ -84,7 +85,10 @@ public HttpResponse apply(ApiResource apiResource, String namespace, R public boolean delete(ApiResource apiResource, String namespace, String resource, boolean dryRun) { try { if (apiResource.isNamespaced()) { - namespacedClient.delete(namespace, apiResource.getPath(), resource, loginService.getAuthorization(), dryRun); + HttpResponse response = namespacedClient.delete(namespace, apiResource.getPath(), resource, loginService.getAuthorization(), dryRun); + if(response.getStatus() != HttpStatus.NO_CONTENT){ + throw new HttpClientResponseException("Resource not Found", response); + } return true; } else { nonNamespacedClient.delete(loginService.getAuthorization(), apiResource.getPath(), resource, dryRun);