This is an automated email from the ASF dual-hosted git repository. claudio4j pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new e5885b85167 camel-jbang-plugin-kubernetes: make the delete op to delete from the k8s resources (#17335) e5885b85167 is described below commit e5885b851672e4231c7ff35367c8be9d114b2d2c Author: Claudio Miranda <clau...@claudius.com.br> AuthorDate: Sun Mar 9 09:35:35 2025 -0300 camel-jbang-plugin-kubernetes: make the delete op to delete from the k8s resources (#17335) Do not use the camel-jbang temporary directory The "run" op sets labels used in the "del" op Use a single KubernetesClient instance in the KubernetesHelper --- .../camel-jbang-plugin-kubernetes/pom.xml | 11 +- .../commands/kubernetes/KubernetesBaseCommand.java | 14 +- .../core/commands/kubernetes/KubernetesDelete.java | 111 ++++------ .../core/commands/kubernetes/KubernetesHelper.java | 22 +- .../core/commands/kubernetes/KubernetesRun.java | 12 +- .../commands/kubernetes/KubernetesCommandTest.java | 2 +- .../commands/kubernetes/KubernetesDeleteTest.java | 232 +++++++++++++++------ 7 files changed, 245 insertions(+), 159 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml index 5ade3566b5c..eb4ff51fdac 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml @@ -65,6 +65,11 @@ <artifactId>openshift-model</artifactId> <version>${kubernetes-client-version}</version> </dependency> + <dependency> + <groupId>io.fabric8</groupId> + <artifactId>openshift-client</artifactId> + <version>${kubernetes-client-version}</version> + </dependency> <!-- Knative model --> <dependency> @@ -99,12 +104,6 @@ <version>${kubernetes-client-version}</version> <scope>test</scope> </dependency> - <dependency> - <groupId>io.fabric8</groupId> - <artifactId>openshift-client</artifactId> - <version>${kubernetes-client-version}</version> - <scope>test</scope> - </dependency> </dependencies> <build> diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseCommand.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseCommand.java index c77146be48c..43eebadccc5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseCommand.java @@ -60,8 +60,6 @@ public abstract class KubernetesBaseCommand extends CamelCommand { List<Supplier<String>> projectNameSuppliers = new ArrayList<>(); - private KubernetesClient kubernetesClient; - public KubernetesBaseCommand(CamelJBangMain main) { super(main); projectNameSuppliers.add(() -> name); @@ -130,22 +128,14 @@ public abstract class KubernetesBaseCommand extends CamelCommand { * uses default client. */ protected KubernetesClient client() { - if (kubernetesClient == null) { - if (kubeConfig != null) { - kubernetesClient = KubernetesHelper.getKubernetesClient(kubeConfig); - } else { - kubernetesClient = KubernetesHelper.getKubernetesClient(); - } - } - - return kubernetesClient; + return KubernetesHelper.getKubernetesClient(); } /** * Sets the Kubernetes client. */ public KubernetesBaseCommand withClient(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; + KubernetesHelper.setKubernetesClient(kubernetesClient); return this; } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDelete.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDelete.java index 7a73fe6f221..f60790bc02f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDelete.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDelete.java @@ -17,93 +17,68 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes; -import java.io.File; -import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.fabric8.kubernetes.api.model.StatusDetails; +import io.fabric8.openshift.client.OpenShiftClient; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; -import org.apache.camel.util.ObjectHelper; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait; import org.apache.camel.util.StringHelper; +import org.codehaus.plexus.util.ExceptionUtils; import picocli.CommandLine; -@CommandLine.Command(name = "delete", description = "Delete Camel application from Kubernetes", sortOptions = false) -public class KubernetesDelete extends KubernetesBaseCommand { - - @CommandLine.Parameters(description = "The Camel file to delete. Integration name is derived from the file name.", - arity = "0..1", paramLabel = "<file>") - String filePath; +import static org.apache.camel.dsl.jbang.core.commands.kubernetes.KubernetesHelper.getKubernetesClient; - @CommandLine.Option(names = { "--working-dir" }, - description = "The working directory where to find exported project sources.") - String workingDir; +@CommandLine.Command(name = "delete", + description = "Delete Camel application from Kubernetes. This operation will delete all resources associated to this app, such as: Deployment, Routes, Services, etc. filtering by labels \"app.kubernetes.io/managed-by=camel-jbang\" and \"app=<app name>\".", + sortOptions = false) +public class KubernetesDelete extends KubernetesBaseCommand { - @CommandLine.Option(names = { "--cluster-type" }, - description = "The target cluster type. Special configurations may be applied to different cluster types such as Kind or Minikube or Openshift." - + - " If a target cluster type was set to create the project (run/export command), it needs to be set.") - protected String clusterType; + @CommandLine.Parameters(description = "The deployed application name", arity = "1", paramLabel = "<app name>") + String appName; public KubernetesDelete(CamelJBangMain main) { super(main); - projectNameSuppliers.add(() -> projectNameFromFilePath(() -> filePath)); } public Integer doCall() throws Exception { - - // First, try the explicit workingDir - File resolvedManifestDir = null; - if (workingDir != null) { - File resolvedWorkingDir = new File(workingDir); - File candidateDir = new File(resolvedWorkingDir, "target/kubernetes"); - if (candidateDir.isDirectory()) { - resolvedManifestDir = candidateDir; - } - } - - String projectName = getProjectName(); - - // Next, try the project name in the run dir - if (resolvedManifestDir == null) { - File resolvedWorkingDir = new File(RUN_PLATFORM_DIR + "/" + projectName); - File candidateDir = new File(resolvedWorkingDir, "target/kubernetes"); - if (candidateDir.isDirectory()) { - resolvedManifestDir = candidateDir; + printer().printf("Deleting all resources from app: %s%n", appName); + Map<String, String> labels = new HashMap<>(); + // this label is set in KubernetesRun command + labels.put(BaseTrait.KUBERNETES_LABEL_MANAGED_BY, "camel-jbang"); + labels.put("app", appName); + List<StatusDetails> deleteStatuses = new ArrayList<>(); + try { + // delete the most common resources + // delete Deployment cascades to pod + deleteStatuses.addAll(getKubernetesClient().apps().deployments().withLabels(labels).delete()); + // delete service + deleteStatuses.addAll(getKubernetesClient().services().withLabels(labels).delete()); + ClusterType clusterType = KubernetesHelper.discoverClusterType(); + if (ClusterType.OPENSHIFT == clusterType) { + // openshift specific: BuildConfig, ImageStreams, Route - BuildConfig casacade delete to Build and ConfigMap + OpenShiftClient ocpClient = getKubernetesClient().adapt(OpenShiftClient.class); + // BuildConfig + deleteStatuses.addAll(ocpClient.buildConfigs().withLabels(labels).delete()); + // ImageStreams + deleteStatuses.addAll(ocpClient.imageStreams().withLabels(labels).delete()); + // Route + deleteStatuses.addAll(ocpClient.routes().withLabels(labels).delete()); } - } - - // Next, try the project name in the current dir - if (resolvedManifestDir == null) { - File candidateDir = new File("./target/kubernetes"); - if (candidateDir.isDirectory()) { - resolvedManifestDir = candidateDir; - } - } - - if (resolvedManifestDir == null) { - printer().printErr("Failed to resolve exported project: %s".formatted(projectName)); - return 1; - } - - File manifest = KubernetesHelper.resolveKubernetesManifest(clusterType, resolvedManifestDir); - printer().printf("Deleting resources from manifest: %s%n", manifest); - - try (FileInputStream fis = new FileInputStream(manifest)) { - List<StatusDetails> status; - var loadedResources = client().load(fis); - if (!ObjectHelper.isEmpty(namespace)) { - status = loadedResources.inNamespace(namespace).delete(); + if (deleteStatuses.size() > 0) { + deleteStatuses.forEach( + s -> printer().printf("Deleted: %s '%s'%n", StringHelper.capitalize(s.getKind()), s.getName())); } else { - // First, let the client choose the default namespace - status = loadedResources.delete(); - // Next, explicitly name the default namespace - if (status.isEmpty()) { - status = loadedResources.inNamespace("default").delete(); - } + printer().println("No deployment found with name: " + appName); } - status.forEach(s -> printer().printf("Deleted: %s '%s'%n", StringHelper.capitalize(s.getKind()), s.getName())); + } catch (Exception ex) { + // there could be various chained exceptions, so we want to get the root cause + printer().println("Error trying to delete the app: " + ExceptionUtils.getRootCause(ex)); + return 1; } - return 0; } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java index caaee6e85f6..71fbf2349a5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java @@ -80,7 +80,7 @@ public final class KubernetesHelper { if (kubernetesClient == null) { kubernetesClient = new KubernetesClientBuilder().build(); } - + setKubernetesClientProperties(); return kubernetesClient; } @@ -91,11 +91,25 @@ public final class KubernetesHelper { if (clients.containsKey(config)) { return clients.get(config); } - + setKubernetesClientProperties(); var client = new KubernetesClientBuilder().withConfig(config).build(); return clients.put(config, client); } + // set short timeouts to fail fast in case it's not connected to a cluster and don't waste time + // the user can override these values by setting the property in the cli + private static void setKubernetesClientProperties() { + if (System.getProperty("kubernetes.connection.timeout") == null) { + System.setProperty("kubernetes.connection.timeout", "2000"); + } + if (System.getProperty("kubernetes.request.timeout") == null) { + System.setProperty("kubernetes.request.timeout", "2000"); + } + if (System.getProperty("kubernetes.request.retry.backoffLimit") == null) { + System.setProperty("kubernetes.request.retry.backoffLimit", "1"); + } + } + /** * Creates new Yaml instance. The implementation provided by Snakeyaml is not thread-safe. It is better to create a * fresh instance for every YAML stream. @@ -125,10 +139,6 @@ public final class KubernetesHelper { */ static ClusterType discoverClusterType() { ClusterType cluster = ClusterType.KUBERNETES; - // in case it's not connected to a cluster, don't waste time waiting for a response. - System.setProperty("kubernetes.connection.timeout", "2000"); - System.setProperty("kubernetes.request.timeout", "2000"); - System.setProperty("kubernetes.request.retry.backoffLimit", "1"); if (isConnectedToOpenshift()) { cluster = ClusterType.OPENSHIFT; } else if (isConnectedToMinikube()) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java index c5246b6a723..0fa05b85375 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java @@ -354,6 +354,9 @@ public class KubernetesRun extends KubernetesBaseCommand { private KubernetesExport configureExport(String workingDir) { detectCluster(); + // jkube automatically sets the "app.kubernetes.io/managed-by: jkube" label + // given this is a deployment managed by this plugin, we will replace the value for "camel-jbang" + buildProperties.add("jkube.enricher.jkube-well-known-labels.managedBy=camel-jbang"); KubernetesExport.ExportConfigurer configurer = new KubernetesExport.ExportConfigurer( runtime, quarkusVersion, @@ -438,7 +441,6 @@ public class KubernetesRun extends KubernetesBaseCommand { printer().printf("Reloading project due to file change: %s%n", FileUtil.stripPath(name)); - String currentWorkingDir = getIndexedWorkingDir(projectName); devModeReloadCount += 1; String reloadWorkingDir = getIndexedWorkingDir(projectName); @@ -459,8 +461,6 @@ public class KubernetesRun extends KubernetesBaseCommand { // Undeploy/Delete current project // KubernetesDelete deleteCommand = new KubernetesDelete(getMain()); - deleteCommand.workingDir = currentWorkingDir; - deleteCommand.clusterType = clusterType; deleteCommand.name = projectName; deleteCommand.doCall(); @@ -530,13 +530,11 @@ public class KubernetesRun extends KubernetesBaseCommand { private void installShutdownHook(String projectName, String workingDir) { devModeShutdownTask = new Thread(() -> { KubernetesDelete deleteCommand = new KubernetesDelete(getMain()); - deleteCommand.clusterType = clusterType; - deleteCommand.workingDir = workingDir; deleteCommand.name = projectName; try (var client = createKubernetesClientForShutdownHook()) { KubernetesHelper.setKubernetesClient(client); deleteCommand.doCall(); - CommandHelper.cleanExportDir(deleteCommand.workingDir, false); + CommandHelper.cleanExportDir(workingDir, false); } catch (Exception e) { throw new RuntimeException(e); } @@ -585,6 +583,8 @@ public class KubernetesRun extends KubernetesBaseCommand { // skip image build and push because we only want to build the Kubernetes manifest args.add("-Djkube.skip.build=true"); args.add("-Djkube.skip.push=true"); + // suppress maven transfer progress + args.add("-ntp"); args.add("package"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java index d20df61591f..c76ecad1a4a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java @@ -64,7 +64,7 @@ class KubernetesCommandTest extends KubernetesBaseTest { Assertions.assertEquals("route", deployment.getMetadata().getLabels().get(BaseTrait.KUBERNETES_LABEL_NAME)); Assertions.assertEquals("route", deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getName()); Assertions.assertEquals("route", matchLabels.get(BaseTrait.KUBERNETES_LABEL_NAME)); - Assertions.assertEquals("jkube", matchLabels.get(BaseTrait.KUBERNETES_LABEL_MANAGED_BY)); + Assertions.assertEquals("camel-jbang", matchLabels.get(BaseTrait.KUBERNETES_LABEL_MANAGED_BY)); Assertions.assertEquals("camel-test/route:1.0-SNAPSHOT", deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); Assertions.assertEquals("IfNotPresent", diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDeleteTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDeleteTest.java index c4717e4cf96..159ee6307e9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDeleteTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesDeleteTest.java @@ -17,86 +17,198 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes; -import io.fabric8.kubernetes.api.model.ContainerBuilder; -import io.fabric8.kubernetes.api.model.IntOrString; -import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.fabric8.kubernetes.api.model.ServicePortBuilder; -import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import java.net.HttpURLConnection; +import java.util.Collections; + +import io.fabric8.kubernetes.api.model.Node; +import io.fabric8.kubernetes.api.model.NodeBuilder; +import io.fabric8.kubernetes.api.model.NodeList; +import io.fabric8.kubernetes.api.model.NodeListBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import io.fabric8.openshift.api.model.config.v1.ClusterVersion; +import io.fabric8.openshift.api.model.config.v1.ClusterVersionBuilder; +import io.fabric8.openshift.client.OpenShiftClient; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; -import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait; +import org.apache.camel.dsl.jbang.core.common.StringPrinter; +import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; -import org.junit.jupiter.api.condition.EnabledIf; +import org.junitpioneer.jupiter.SetEnvironmentVariable; @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", disabledReason = "Requires too much network resources") -@EnabledIf("isDockerAvailable") -class KubernetesDeleteTest extends KubernetesBaseTest { +@EnableKubernetesMockClient +class KubernetesDeleteTest { + + protected KubernetesMockServer server; + protected KubernetesClient client; + protected StringPrinter printer; + + @BeforeEach + public void setup() { + // Set Camel version with system property value, usually set via Maven surefire plugin + // In case you run this test via local Java IDE you need to provide the system property or a default value here + VersionHelper.setCamelVersion(System.getProperty("camel.version", "")); + printer = new StringPrinter(); + } @Test - public void shouldDeleteKubernetesResources() throws Exception { - kubernetesClient.apps().deployments().resource(new DeploymentBuilder() - .withNewMetadata() - .withName("route") - .addToLabels(BaseTrait.KUBERNETES_LABEL_NAME, "route") - .endMetadata() - .withNewSpec() - .withNewTemplate() - .withNewSpec() - .addToContainers(new ContainerBuilder() - .withName("route") - .withImage("quay.io/camel-test/route:1.0-SNAPSHOT") - .build()) - .endSpec() - .endTemplate() - .endSpec() - .build()).create(); - - kubernetesClient.services().resource(new ServiceBuilder() + public void shouldDeleteNonExistentApp() throws Exception { + KubernetesDelete command = new KubernetesDelete(new CamelJBangMain().withPrinter(printer)); + command.withClient(client); + command.appName = "does-not-exist"; + int exit = command.doCall(); + boolean errorOutput = printer.getOutput().contains("Error trying to delete the app"); + if (errorOutput) { + Assertions.assertEquals(1, exit, printer.getOutput()); + } else { + Assertions.assertEquals(0, exit, printer.getOutput()); + Assertions.assertTrue(printer.getOutput().contains("No deployment found with name"), printer.getOutput()); + } + } + + @Test + @SetEnvironmentVariable(key = "MINIKUBE_ACTIVE_DOCKERD", value = "foo") + @SetEnvironmentVariable(key = "DOCKER_TLS_VERIFY", value = "foo") + public void shouldDeleteNonExistentAppInMinikube() throws Exception { + Node nodeCR = new NodeBuilder() .withNewMetadata() - .withName("route") + .withName("minikube") + .withLabels(Collections.singletonMap("minikube.k8s.io/name", "minikube")) .endMetadata() - .withNewSpec() - .withPorts(new ServicePortBuilder() - .withPort(80) - .withProtocol("TCP") - .withName("http") - .withTargetPort(new IntOrString(8080)) - .build()) - .endSpec() - .build()).create(); - - KubernetesRun run = new KubernetesRun(new CamelJBangMain().withPrinter(printer)); - run.withClient(kubernetesClient); - run.imageGroup = "camel-test"; - run.imageBuild = false; - run.imagePush = false; - run.filePaths = new String[] { "classpath:route.yaml" }; - run.output = "yaml"; - int exit = run.doCall(); - Assertions.assertEquals(0, exit); + .build(); + NodeList nodeList = new NodeListBuilder().addToItems(nodeCR) + .build(); + + serverExpect("/api/v1/nodes?labelSelector=minikube.k8s.io%2Fname", nodeList); + + KubernetesDelete command = new KubernetesDelete(new CamelJBangMain().withPrinter(printer)); + command.withClient(client); + command.appName = "does-not-exist"; + int exit = command.doCall(); + boolean errorOutput = printer.getOutput().contains("Error trying to delete the app"); + if (errorOutput) { + Assertions.assertEquals(1, exit, printer.getOutput()); + } else { + Assertions.assertEquals(0, exit, printer.getOutput()); + Assertions.assertTrue(printer.getOutput().contains("No deployment found with name"), printer.getOutput()); + } + } + + @Test + public void shouldDeleteNonExistentAppInOpenshift() throws Exception { + ClusterVersion versionCR = new ClusterVersionBuilder() + .withNewMetadata().withName("version").endMetadata() + .withNewStatus() + .withNewDesired() + .withVersion("4.14.5") + .endDesired() + .endStatus() + .build(); + + serverExpect("/apis/config.openshift.io/v1/clusterversions/version", versionCR); KubernetesDelete command = new KubernetesDelete(new CamelJBangMain().withPrinter(printer)); - command.withClient(kubernetesClient); - command.name = "route"; - exit = command.doCall(); + command.withClient(client); + command.appName = "does-not-exist"; + int exit = command.doCall(); + boolean errorOutput = printer.getOutput().contains("Error trying to delete the app"); + if (errorOutput) { + Assertions.assertEquals(1, exit, printer.getOutput()); + } else { + Assertions.assertEquals(0, exit, printer.getOutput()); + Assertions.assertTrue(printer.getOutput().contains("No deployment found with name"), printer.getOutput()); + } + } + + @Test + public void shouldDeleteOnKubernetes() throws Exception { + String appName = "my-route"; + + // Mock the delete request expectation + serverDeleteExpect( + "/apis/apps/v1/namespaces/test/deployments?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + serverDeleteExpect( + "/api/v1/namespaces/test/services?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); - Assertions.assertEquals(0, exit); - Assertions.assertNull(kubernetesClient.apps().deployments().withName("route").get()); - Assertions.assertNull(kubernetesClient.services().withName("route").get()); + // Execute delete command + KubernetesDelete command = new KubernetesDelete(new CamelJBangMain().withPrinter(printer)); + command.withClient(client); + command.namespace = "test"; + command.appName = appName; + int exit = command.doCall(); + + // Verify command execution + Assertions.assertEquals(0, exit, printer.getOutput()); + + // Verify deployment was deleted + Assertions.assertNull(client.apps().deployments().withName(appName).get()); + Assertions.assertNull(client.services().withName(appName).get()); } @Test - public void shouldHandleNoExportFound() throws Exception { + public void shouldDeleteOnOpenShift() throws Exception { + ClusterVersion versionCR = new ClusterVersionBuilder() + .withNewMetadata().withName("version").endMetadata() + .withNewStatus() + .withNewDesired() + .withVersion("4.14.5") + .endDesired() + .endStatus() + .build(); + + serverExpect("/apis/config.openshift.io/v1/clusterversions/version", versionCR); + + String appName = "my-route"; + + // Mock the delete request expectations for openshift resources + serverDeleteExpect( + "/apis/apps/v1/namespaces/test/deployments?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + serverDeleteExpect( + "/api/v1/namespaces/test/services?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + serverDeleteExpect( + "/apis/build.openshift.io/v1/namespaces/test/buildconfigs?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + serverDeleteExpect( + "/apis/image.openshift.io/v1/namespaces/test/imagestreams?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + serverDeleteExpect( + "/apis/route.openshift.io/v1/namespaces/test/routes?labelSelector=app%3Dmy-route%2Capp.kubernetes.io%2Fmanaged-by%3Dcamel-jbang"); + + // Execute delete command KubernetesDelete command = new KubernetesDelete(new CamelJBangMain().withPrinter(printer)); - command.withClient(kubernetesClient); - command.name = "does-not-exist"; + command.withClient(client); + command.namespace = "test"; + command.appName = appName; int exit = command.doCall(); - Assertions.assertEquals(1, exit); - Assertions.assertEquals("ERROR: Failed to resolve exported project: does-not-exist", - printer.getOutput()); + // Verify command execution + Assertions.assertEquals(0, exit, printer.getOutput()); + + OpenShiftClient ocpClient = client.adapt(OpenShiftClient.class); + + // Verify deployment was deleted + Assertions.assertNull(client.apps().deployments().withName(appName).get()); + Assertions.assertNull(client.services().withName(appName).get()); + Assertions.assertNull(ocpClient.buildConfigs().withName(appName).get()); + Assertions.assertNull(ocpClient.imageStreams().withName(appName).get()); + Assertions.assertNull(ocpClient.routes().withName(appName).get()); + } + + private void serverDeleteExpect(String path) { + server.expect().delete() + .withPath(path) + .andReturn(HttpURLConnection.HTTP_OK, null) + .once(); + } + + private void serverExpect(String path, Object response) { + server.expect().get() + .withPath(path) + .andReturn(HttpURLConnection.HTTP_OK, response) + .once(); } }