Skip to content

Commit

Permalink
Implement kafkactl delete -f to delete resources from files (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
twobeeb authored Oct 13, 2021
1 parent d4edfa8 commit 69cbdff
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 19 deletions.
104 changes: 87 additions & 17 deletions cli/src/main/java/com/michelin/ns4kafka/cli/DeleteSubcommand.java
Original file line number Diff line number Diff line change
@@ -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<Integer> {
Expand All @@ -25,13 +32,33 @@ public class DeleteSubcommand implements Callable<Integer> {
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> 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;

Expand All @@ -52,21 +79,64 @@ public Integer call() {

String namespace = kafkactlCommand.optionalNamespace.orElse(kafkactlConfig.getCurrentNamespace());

Optional<ApiResource> optionalApiResource = apiResourcesService.getResourceDefinitionFromCommandName(resourceType);
if (optionalApiResource.isEmpty()) {
throw new CommandLine.ParameterException(commandSpec.commandLine(), "The server doesn't have resource type [" + resourceType + "]");
List<Resource> resources;

if (config.fileConfig != null && config.fileConfig.file.isPresent()) {
// 1. list all files to process
List<File> 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<ApiResource> 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<Resource> 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<Resource> 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<ApiResource> 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;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
public interface NamespacedResourceClient {

@Delete("{namespace}/{kind}/{resourcename}{?dryrun}")
void delete(
HttpResponse delete(
String namespace,
String kind,
String resourcename,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,7 +85,10 @@ public HttpResponse<Resource> 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);
Expand Down

0 comments on commit 69cbdff

Please sign in to comment.