You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2022/04/27 22:00:45 UTC

[camel] branch main updated: CAMEL-18008. camel-jbang deploy/undeploy command for openshift (#7510)

This is an automated email from the ASF dual-hosted git repository.

marat 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 3593a5da8f3 CAMEL-18008. camel-jbang deploy/undeploy command for openshift (#7510)
3593a5da8f3 is described below

commit 3593a5da8f394d1e766d99c03d1c92fde604f93b
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Apr 27 18:00:35 2022 -0400

    CAMEL-18008. camel-jbang deploy/undeploy command for openshift (#7510)
    
    * Deploy to OpenShift (prototype)
    
    Openshift deploy/updeploy
    
    * Default jar files
---
 dsl/camel-jbang/camel-jbang-core/pom.xml           |   6 +
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +-
 .../camel/dsl/jbang/core/commands/Deploy.java      |  76 ++++++++--
 .../camel/dsl/jbang/core/commands/Image.java       | 168 ++++++++++++++++++++-
 .../dsl/jbang/core/commands/KubernetesHelper.java  |  96 +++++++++++-
 .../camel/dsl/jbang/core/commands/Manifest.java    | 115 ++++++++++++++
 .../camel/dsl/jbang/core/commands/Resource.java    |  85 -----------
 .../camel/dsl/jbang/core/commands/Undeploy.java    |  49 ++++--
 8 files changed, 475 insertions(+), 122 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml b/dsl/camel-jbang/camel-jbang-core/pom.xml
index bf34270abab..be76c83cdf0 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -100,6 +100,12 @@
             <artifactId>kubernetes-client</artifactId>
             <version>${kubernetes-client-version}</version>
         </dependency>
+        <!-- OpenShift client -->
+        <dependency>
+            <groupId>io.fabric8</groupId>
+            <artifactId>openshift-client</artifactId>
+            <version>${kubernetes-client-version}</version>
+        </dependency>
         <!-- Docker generator -->
         <dependency>
             <groupId>com.google.cloud.tools</groupId>
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 2370ada9b05..551917ccc79 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -36,7 +36,7 @@ public class CamelJBangMain implements Callable<Integer> {
                 .addSubcommand("generate", new CommandLine(new CodeGenerator())
                         .addSubcommand("rest", new CodeRestGenerator()))
                 .addSubcommand("build", new CommandLine(new Build())
-                        .addSubcommand("resources", new Resource())
+                        .addSubcommand("manifests", new Manifest())
                         .addSubcommand("image", new Image()))
                 .addSubcommand("deploy", new CommandLine(new Deploy()))
                 .addSubcommand("undeploy", new CommandLine(new Undeploy()))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Deploy.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Deploy.java
index 2df300e86ec..bae755ee9a0 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Deploy.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Deploy.java
@@ -19,9 +19,16 @@ package org.apache.camel.dsl.jbang.core.commands;
 import java.util.concurrent.Callable;
 
 import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.Status;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.client.DefaultKubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientException;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.client.DefaultOpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftConfig;
+import io.fabric8.openshift.client.OpenShiftConfigBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine;
@@ -37,8 +44,8 @@ public class Deploy implements Callable<Integer> {
     private String namespace;
     @CommandLine.Option(names = { "--name" }, description = "Application name", required = true)
     private String name;
-    @CommandLine.Option(names = { "--version" }, description = "Application version (label)", required = true)
-    private String version;
+    @CommandLine.Option(names = { "--version" }, description = "Application version (label)")
+    private String version = "latest";
     @CommandLine.Option(names = { "--image" }, description = "Deployment container image name", required = true)
     private String image;
     @CommandLine.Option(names = { "--container-port" }, description = "Container port", defaultValue = "8080")
@@ -51,25 +58,62 @@ public class Deploy implements Callable<Integer> {
     private int replicas;
     @CommandLine.Option(names = { "--minikube" }, description = "Target is minikube")
     private boolean minikube;
+    @CommandLine.Option(names = { "--openshift" }, description = "Target is openshift")
+    private boolean openshift;
+    @CommandLine.Option(names = { "--server" }, description = "Master URL")
+    private String server;
+    @CommandLine.Option(names = { "--token" }, description = "Token")
+    private String token;
 
     @Override
     public Integer call() throws Exception {
-        LOG.info("Generating Deployment...");
-        Deployment deployment = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
-        LOG.info("Generating Service...");
-        Service service
-                = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube, nodePort);
+        if (minikube) {
+            LOG.info("Generating Deployment...");
+            Deployment deployment = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
+            LOG.info("Generating Service...");
+            Service service
+                    = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube, nodePort);
 
-        try (KubernetesClient client = new DefaultKubernetesClient()) {
-            LOG.info("Creating Deployment in " + (minikube ? "Minikube" : "Kubernetes"));
-            client.apps().deployments().inNamespace(namespace).createOrReplace(deployment);
-            client.services().inNamespace(namespace).delete(service);
-            LOG.info("Creating Service in " + (minikube ? "Minikube" : "Kubernetes"));
-            client.services().inNamespace(namespace).createOrReplace(service);
-        } catch (Exception ex) {
-            LOG.error("Error", ex.getMessage());
+            try (KubernetesClient client = new DefaultKubernetesClient()) {
+                LOG.info("Creating Deployment in " + (minikube ? "Minikube" : "Kubernetes"));
+                client.apps().deployments().inNamespace(namespace).createOrReplace(deployment);
+                client.services().inNamespace(namespace).delete(service);
+                LOG.info("Creating Service in " + (minikube ? "Minikube" : "Kubernetes"));
+                client.services().inNamespace(namespace).createOrReplace(service);
+            } catch (Exception ex) {
+                LOG.error("Error", ex.getMessage());
+            }
+        } else if (openshift) {
+            if (!image.startsWith("image-registry.openshift-image-registry.svc:5000") && image.split("/").length != 3) {
+                image = "image-registry.openshift-image-registry.svc:5000/" + image;
+            }
+            LOG.info("Generating Deployment...");
+            Deployment deployment = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
+            LOG.info("Generating Service...");
+            Service service
+                    = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube, nodePort);
+            LOG.info("Generating Route...");
+            Route route = KubernetesHelper.createRoute(namespace, name, version, containerPort);
+
+            OpenShiftConfig config = new OpenShiftConfigBuilder().withMasterUrl(server).withOauthToken(token).build();
+            try (OpenShiftClient client = new DefaultOpenShiftClient(config)) {
+                LOG.info("Creating Deployment in Openshift");
+                client.apps().deployments().inNamespace(namespace).createOrReplace(deployment);
+                client.services().inNamespace(namespace).delete(service);
+                LOG.info("Creating Service in Openshift");
+                client.services().inNamespace(namespace).createOrReplace(service);
+                LOG.info("Creating Route in Openshift");
+                client.routes().inNamespace(namespace).createOrReplace(route);
+            } catch (KubernetesClientException ex) {
+                Status status = ex.getStatus();
+                if (status != null) {
+                    LOG.error("Error: [%d %s] [%s] %s", status.getCode(), status.getStatus(), status.getReason(),
+                            status.getMessage());
+                } else {
+                    LOG.error(ex.getMessage());
+                }
+            }
         }
         return 0;
     }
-
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Image.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Image.java
index 4cf886bf55b..a56a0e1b796 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Image.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Image.java
@@ -16,10 +16,15 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 import com.google.cloud.tools.jib.api.Containerizer;
@@ -28,7 +33,16 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
 import com.google.cloud.tools.jib.api.Jib;
 import com.google.cloud.tools.jib.api.LogEvent;
 import com.google.cloud.tools.jib.api.RegistryImage;
+import com.google.cloud.tools.jib.api.TarImage;
 import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath;
+import io.fabric8.kubernetes.client.dsl.LogWatch;
+import io.fabric8.openshift.api.model.Build;
+import io.fabric8.openshift.api.model.BuildConfig;
+import io.fabric8.openshift.api.model.ImageStream;
+import io.fabric8.openshift.client.DefaultOpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftConfig;
+import io.fabric8.openshift.client.OpenShiftConfigBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine;
@@ -37,12 +51,13 @@ import picocli.CommandLine;
 public class Image implements Callable<Integer> {
 
     private static final Logger LOG = LoggerFactory.getLogger(Image.class);
+    private static final int LOG_TAIL_SIZE = 10;
 
     @CommandLine.Option(names = { "-h", "--help" }, usageHelp = true, description = "Display the help and sub-commands")
     private boolean helpRequested;
     @CommandLine.Option(names = { "-f", "--from" }, description = "Base Image", defaultValue = "gcr.io/distroless/java:11")
     private String from;
-    @CommandLine.Option(names = { "-j", "--jar" }, required = true, description = "Jar filename")
+    @CommandLine.Option(names = { "-j", "--jar" }, description = "Jar file", defaultValue = "target/camel-runner.jar")
     private String jar;
     @CommandLine.Option(names = { "-t", "--tag" }, description = "Image tag")
     private String tag;
@@ -54,15 +69,93 @@ public class Image implements Callable<Integer> {
     private String username;
     @CommandLine.Option(names = { "-p", "--password" }, description = "Registry password")
     private String password;
+    @CommandLine.Option(names = { "--tar" }, description = "Create tar")
+    private boolean tar;
+    @CommandLine.Option(names = { "--tar-name" }, description = "Tar filename")
+    private String tarname;
+    @CommandLine.Option(names = { "--openshift" }, description = "Target is openshift")
+    private boolean openshift;
+    @CommandLine.Option(names = { "--server" }, description = "Master URL")
+    private String server;
+    @CommandLine.Option(names = { "--token" }, description = "Token")
+    private String token;
+    @CommandLine.Option(names = { "--namespace" }, description = "Namespace", defaultValue = "default")
+    private String namespace;
+    @CommandLine.Option(names = { "--name" }, description = "Application name", required = true)
+    private String name;
+    @CommandLine.Option(names = { "--version" }, description = "Application version (label)", required = true)
+    private String version;
+    @CommandLine.Option(names = { "--source-image" }, description = "Source image name (for OpenShift buildConfig)",
+                        defaultValue = "java:openjdk-11-ubi8")
+    private String sourceImage;
 
     @Override
     public Integer call() throws Exception {
         File jarFile = Paths.get(jar).toFile();
-        Jib.from(from)
-                .addLayer(Arrays.asList(Paths.get(jar)), "/deployments/")
-                .setWorkingDirectory(AbsoluteUnixPath.get("/deployments"))
-                .setEntrypoint("java", "-jar", jarFile.getName())
-                .containerize(push ? getRegistry() : getDockerImage());
+        if (openshift) {
+            LOG.info("Generating resources...");
+            OpenShiftConfig config
+                    = new OpenShiftConfigBuilder().withMasterUrl(server).withOauthToken(token).withNamespace(namespace).build();
+            try (OpenShiftClient client = new DefaultOpenShiftClient(config)) {
+                ImageStream imageStream = KubernetesHelper.createImageStream(namespace, name, version);
+                BuildConfig buildConfig
+                        = KubernetesHelper.createBuildConfig(namespace, name, version, jarFile.getName(), sourceImage);
+                LOG.info("Creating ImageStream...");
+                client.imageStreams().createOrReplace(imageStream);
+                LOG.info("Creating BuildConfig...");
+                client.buildConfigs().createOrReplace(buildConfig);
+                LOG.info("Creating Build...");
+                Build build = client.buildConfigs()
+                        .inNamespace(namespace)
+                        .withName(buildConfig.getMetadata().getName())
+                        .instantiateBinary()
+                        .asFile(jarFile.getName())
+                        .withTimeout(5, TimeUnit.MINUTES)
+                        .fromFile(jarFile);
+                while (isNew(build) || isPending(build) || isRunning(build)) {
+                    final String buildName = build.getMetadata().getName();
+                    Build updated = client.builds().withName(buildName).get();
+                    if (updated == null) {
+                        throw new IllegalStateException("Build:" + build.getMetadata().getName() + " is no longer present!");
+                    } else if (updated.getStatus() == null) {
+                        throw new IllegalStateException("Build:" + build.getMetadata().getName() + " has no status!");
+                    } else if (isNew(updated) || isPending(updated) || isRunning(updated)) {
+                        build = updated;
+                        try (LogWatch w
+                                = client.builds().withName(buildName).withPrettyOutput().watchLog();
+                             Reader reader = new InputStreamReader(w.getOutput())) {
+                            display(reader);
+                        } catch (IOException e) {
+                            // This may happen if the LogWatch is closed while we are still reading.
+                            // We shouldn't let the build fail, so let's log a warning and display last few lines of the log
+                            LOG.warn("Log stream closed, redisplaying last " + LOG_TAIL_SIZE + " entries:");
+                            try {
+                                display(client.builds().withName(buildName).tailingLines(LOG_TAIL_SIZE)
+                                        .getLogReader());
+                            } catch (IOException ex) {
+                                // Let's ignore this.
+                            }
+                        }
+                    } else if (isComplete(updated)) {
+                        break;
+                    } else if (isCancelled(updated)) {
+                        throw new IllegalStateException("Build:" + buildName + " cancelled!");
+                    } else if (isFailed(updated)) {
+                        throw new IllegalStateException(
+                                "Build:" + buildName + " failed! " + updated.getStatus().getMessage());
+                    } else if (isError(updated)) {
+                        throw new IllegalStateException(
+                                "Build:" + buildName + " encountered error! " + updated.getStatus().getMessage());
+                    }
+                }
+            }
+        } else {
+            Jib.from(from)
+                    .addLayer(Arrays.asList(Paths.get(jar)), "/deployments/")
+                    .setWorkingDirectory(AbsoluteUnixPath.get("/deployments"))
+                    .setEntrypoint("java", "-jar", jarFile.getName())
+                    .containerize(push ? getRegistry() : (tar ? getTarImage() : getDockerImage()));
+        }
         return 0;
     }
 
@@ -70,9 +163,18 @@ public class Image implements Callable<Integer> {
         return Containerizer.to(DockerDaemonImage.named(tag)).addEventHandler(LogEvent.class, getEventConsumer());
     }
 
+    private Containerizer getTarImage() throws InvalidImageReferenceException {
+        if (tarname == null) {
+            String filename = jar.contains(File.separator) ? jar.substring(jar.lastIndexOf(File.separator) + 1) : jar;
+            tarname = filename.substring(0, filename.lastIndexOf('.')) + ".tar";
+        }
+        return Containerizer.to(TarImage.at(Paths.get(tarname)).named(tag)).addEventHandler(LogEvent.class, getEventConsumer());
+    }
+
     private Containerizer getRegistry() throws InvalidImageReferenceException {
         return Containerizer.to(
                 RegistryImage.named(registry).addCredential(username, password))
+                .withAdditionalTag(tag)
                 .addEventHandler(LogEvent.class, getEventConsumer());
     }
 
@@ -94,4 +196,58 @@ public class Image implements Callable<Integer> {
             }
         };
     }
+
+    private static void display(Reader logReader) throws IOException {
+        BufferedReader reader = new BufferedReader(logReader);
+        for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+            LOG.info(line);
+        }
+    }
+
+    static boolean isNew(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.New.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isPending(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Pending.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isRunning(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Running.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isComplete(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Complete.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isFailed(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Failed.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isError(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Error.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    static boolean isCancelled(Build build) {
+        return build != null && build.getStatus() != null
+                && BuildStatus.Cancelled.name().equalsIgnoreCase(build.getStatus().getPhase());
+    }
+
+    public enum BuildStatus {
+
+        New,
+        Pending,
+        Running,
+        Complete,
+        Failed,
+        Error,
+        Cancelled;
+
+    }
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/KubernetesHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/KubernetesHelper.java
index 718c6e6dc5d..d79c2e870d4 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/KubernetesHelper.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/KubernetesHelper.java
@@ -27,12 +27,85 @@ import io.fabric8.kubernetes.api.model.ServicePortBuilder;
 import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
+import io.fabric8.openshift.api.model.BinaryBuildSource;
+import io.fabric8.openshift.api.model.BuildConfig;
+import io.fabric8.openshift.api.model.BuildConfigBuilder;
+import io.fabric8.openshift.api.model.ImageStream;
+import io.fabric8.openshift.api.model.ImageStreamBuilder;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.api.model.RouteBuilder;
+import io.fabric8.openshift.api.model.RoutePortBuilder;
 
 public final class KubernetesHelper {
 
     private KubernetesHelper() {
     }
 
+    public static BuildConfig createBuildConfig(
+            String namespace, String name, String version, String filename, String sourceImage) {
+
+        ObjectMetaBuilder metadata = new ObjectMetaBuilder()
+                .withName(name)
+                .withAnnotations(Map.of("jarFileName", filename))
+                .withLabels(getLabels(name, version));
+        if (namespace != null) {
+            metadata.withNamespace(namespace);
+        }
+
+        return new BuildConfigBuilder()
+                .withMetadata(metadata.build())
+                .withNewSpec()
+                .withNewSource().withType("Binary").withBinary(new BinaryBuildSource(filename)).endSource()
+                .withNewOutput()
+                .withNewTo().withKind("ImageStreamTag").withName(name + ":" + version).endTo()
+                .endOutput()
+                .withNewStrategy().withType("Source")
+                .withNewSourceStrategy().withNewFrom().withKind("ImageStreamTag").withNamespace("openshift")
+                .withName(sourceImage).endFrom()
+                .endSourceStrategy()
+                .endStrategy()
+                .withNewSource().withType("Binary")
+                .endSource()
+                .endSpec()
+                .build();
+    }
+
+    public static ImageStream createImageStream(String namespace, String name, String version) {
+
+        ObjectMetaBuilder metadata = new ObjectMetaBuilder()
+                .withName(name)
+                .withLabels(getLabels(name, version));
+        if (namespace != null) {
+            metadata.withNamespace(namespace);
+        }
+
+        return new ImageStreamBuilder()
+                .withMetadata(metadata.build())
+                .withNewSpec()
+                .withNewLookupPolicy(false)
+                .endSpec()
+                .build();
+    }
+
+    public static Route createRoute(String namespace, String name, String version, int targetPort) {
+
+        ObjectMetaBuilder metadata = new ObjectMetaBuilder()
+                .withName(name)
+                .withLabels(getLabels(name, version));
+        if (namespace != null) {
+            metadata.withNamespace(namespace);
+        }
+
+        return new RouteBuilder()
+                .withMetadata(metadata.build())
+                .withNewSpec()
+                .withPort(new RoutePortBuilder().withNewTargetPort(targetPort).build())
+                .withNewTo().withKind("Service").withName(name)
+                .endTo()
+                .endSpec()
+                .build();
+    }
+
     public static Service createService(
             String namespace, String name, String version, int port, int targetPort, boolean minikube, int nodePort) {
 
@@ -52,6 +125,7 @@ public final class KubernetesHelper {
         }
 
         ServiceSpecBuilder spec = new ServiceSpecBuilder()
+                .withSelector(getSelector(name, version))
                 .withPorts(servicePort.build());
         if (minikube) {
             spec.withType("NodePort");
@@ -66,6 +140,10 @@ public final class KubernetesHelper {
     public static Deployment createDeployment(
             String namespace, String name, String image, String version, int containerPort, int replica) {
 
+        if (image == null) {
+            image = namespace + "/" + name + ":" + version;
+        }
+
         EnvVar envVar = new EnvVarBuilder()
                 .withName("KUBERNETES_NAMESPACE")
                 .withNewValueFrom()
@@ -87,7 +165,7 @@ public final class KubernetesHelper {
                 .withNewSpec()
                 .withReplicas(replica)
                 .withNewSelector()
-                .addToMatchLabels(getLabels(name, version))
+                .addToMatchLabels(getMatchLabels(name))
                 .endSelector()
                 .withNewTemplate()
                 .withNewMetadata()
@@ -118,8 +196,22 @@ public final class KubernetesHelper {
         return Map.of(
                 "app", name,
                 "app.kubernetes.io/name", name,
+                "app.kubernetes.io/component", name,
+                "app.kubernetes.io/instance", name,
                 "app.kubernetes.io/version", version,
                 "app.kubernetes.io/part-of", name,
-                "runtime", "camel");
+                "app.openshift.io/runtime", "camel",
+                "app.kubernetes.io/runtime", "camel");
+    }
+
+    public static Map<String, String> getMatchLabels(String name) {
+        return Map.of(
+                "app", name);
+    }
+
+    public static Map<String, String> getSelector(String name, String version) {
+        return Map.of(
+                "app.kubernetes.io/name", name,
+                "app.kubernetes.io/version", version);
     }
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Manifest.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Manifest.java
new file mode 100644
index 00000000000..a95220749a1
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Manifest.java
@@ -0,0 +1,115 @@
+/*
+ * 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 org.apache.camel.dsl.jbang.core.commands;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.client.utils.Serialization;
+import io.fabric8.openshift.api.model.BuildConfig;
+import io.fabric8.openshift.api.model.ImageStream;
+import io.fabric8.openshift.api.model.Route;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "manifests", description = "Create Kubernetes resources")
+public class Manifest implements Callable<Integer> {
+    private static final Logger LOG = LoggerFactory.getLogger(Manifest.class);
+
+    @CommandLine.Option(names = { "-h", "--help" }, usageHelp = true, description = "Display the help and sub-commands")
+    private boolean helpRequested;
+    @CommandLine.Option(names = { "--path" }, description = "Output folder path", defaultValue = "manifests")
+    private String path;
+    @CommandLine.Option(names = { "--namespace" }, description = "Namespace")
+    private String namespace;
+    @CommandLine.Option(names = { "--name" }, description = "Application name", required = true)
+    private String name;
+    @CommandLine.Option(names = { "--version" }, description = "Application version (label)", required = true)
+    private String version;
+    @CommandLine.Option(names = { "--image" }, description = "Deployment container image name", required = true)
+    private String image;
+    @CommandLine.Option(names = { "--source-image" }, description = "Source image name (for OpenShift buildConfig)",
+                        defaultValue = "java:openjdk-11-ubi8")
+    private String sourceImage;
+    @CommandLine.Option(names = { "--container-port" }, description = "Container port", defaultValue = "8080")
+    private int containerPort;
+    @CommandLine.Option(names = { "--service-port" }, description = "Service port", defaultValue = "80")
+    private int servicePort;
+    @CommandLine.Option(names = { "--node-port" }, description = "Node port (minikube)", defaultValue = "30777")
+    private int nodePort;
+    @CommandLine.Option(names = { "--replicas" }, description = "Number of replicas of the application", defaultValue = "1")
+    private int replicas;
+    @CommandLine.Option(names = { "--minikube" }, description = "Target is minikube")
+    private boolean minikube;
+    @CommandLine.Option(names = { "--openshift" }, description = "Target is openshift")
+    private boolean openshift;
+    @CommandLine.Option(names = { "-j", "--jar" }, description = "Jar file", defaultValue = "target/camel-runner.jar")
+    private String jar;
+
+    @Override
+    public Integer call() throws Exception {
+        try {
+            LOG.info("Generating resources...");
+            if (minikube) {
+                Deployment deployment
+                        = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
+                Service service = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube,
+                        nodePort);
+                write(deployment, "deployment.yaml");
+                write(service, "service.yaml");
+            } else if (openshift) {
+                Deployment deployment
+                        = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
+                Service service = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube,
+                        nodePort);
+                Route route = KubernetesHelper.createRoute(namespace, name, version, containerPort);
+                ImageStream imageStream = KubernetesHelper.createImageStream(namespace, name, version);
+                File jarFile = Paths.get(jar).toFile();
+                BuildConfig buildConfig
+                        = KubernetesHelper.createBuildConfig(namespace, name, version, jarFile.getName(), sourceImage);
+                write(deployment, "deployment.yaml");
+                write(service, "service.yaml");
+                write(route, "route.yaml");
+                write(imageStream, "image-stream.yaml");
+                write(buildConfig, "build-config.yaml");
+            }
+        } catch (Exception ex) {
+            LOG.error("Error", ex.getMessage());
+        }
+        return 0;
+    }
+
+    private void write(Object object, String filename) throws IOException {
+        Path output = Paths.get(path != null ? path : System.getProperty("user.dir"));
+        if (!Files.exists(output)) {
+            LOG.info("Creating output folder " + output);
+            Files.createDirectories(output);
+        }
+        LOG.info("Writing {}...", filename);
+        Files.write(Paths.get(output.toString(), filename),
+                Serialization.asYaml(object).getBytes(StandardCharsets.UTF_8));
+    }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Resource.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Resource.java
deleted file mode 100644
index 2903d4b7b14..00000000000
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Resource.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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 org.apache.camel.dsl.jbang.core.commands;
-
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.concurrent.Callable;
-
-import io.fabric8.kubernetes.api.model.Service;
-import io.fabric8.kubernetes.api.model.apps.Deployment;
-import io.fabric8.kubernetes.client.utils.Serialization;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import picocli.CommandLine;
-
-@CommandLine.Command(name = "resources", description = "Create Kubernetes resources")
-public class Resource implements Callable<Integer> {
-    private static final Logger LOG = LoggerFactory.getLogger(Resource.class);
-
-    @CommandLine.Option(names = { "-h", "--help" }, usageHelp = true, description = "Display the help and sub-commands")
-    private boolean helpRequested;
-    @CommandLine.Option(names = { "--path" }, description = "Output folder path")
-    private String path;
-    @CommandLine.Option(names = { "--namespace" }, description = "Namespace")
-    private String namespace;
-    @CommandLine.Option(names = { "--name" }, description = "Application name", required = true)
-    private String name;
-    @CommandLine.Option(names = { "--version" }, description = "Application version (label)", required = true)
-    private String version;
-    @CommandLine.Option(names = { "--image" }, description = "Deployment container image name", required = true)
-    private String image;
-    @CommandLine.Option(names = { "--container-port" }, description = "Container port", defaultValue = "8080")
-    private int containerPort;
-    @CommandLine.Option(names = { "--service-port" }, description = "Service port", defaultValue = "80")
-    private int servicePort;
-    @CommandLine.Option(names = { "--node-port" }, description = "Node port (minikube)", defaultValue = "30777")
-    private int nodePort;
-    @CommandLine.Option(names = { "--replicas" }, description = "Number of replicas of the application", defaultValue = "1")
-    private int replicas;
-    @CommandLine.Option(names = { "--minikube" }, description = "Target is minikube")
-    private boolean minikube;
-
-    @Override
-    public Integer call() throws Exception {
-        try {
-            LOG.info("Generating Deployment...");
-            Deployment deployment = KubernetesHelper.createDeployment(namespace, name, image, version, containerPort, replicas);
-            LOG.info("Generating Service...");
-            Service service
-                    = KubernetesHelper.createService(namespace, name, version, servicePort, containerPort, minikube, nodePort);
-            Path output = Paths.get(path != null ? path : System.getProperty("user.dir"));
-            if (!Files.exists(output)) {
-                LOG.info("Creating output folder " + output);
-                Files.createDirectories(output);
-            }
-            LOG.info("writing deployment.yaml...");
-            Files.write(Paths.get(output.toString(), "deployment.yaml"),
-                    Serialization.asYaml(deployment).getBytes(StandardCharsets.UTF_8));
-            LOG.info("writing service.yaml...");
-            Files.write(Paths.get(output.toString(), "service.yaml"),
-                    Serialization.asYaml(service).getBytes(StandardCharsets.UTF_8));
-        } catch (Exception ex) {
-            LOG.error("Error", ex.getMessage());
-        }
-
-        return 0;
-    }
-
-}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Undeploy.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Undeploy.java
index e0c46289f8e..838b9b8aefb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Undeploy.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Undeploy.java
@@ -16,12 +16,15 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.util.Map;
 import java.util.concurrent.Callable;
 
-import io.fabric8.kubernetes.api.model.Service;
-import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.client.DefaultKubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.openshift.client.DefaultOpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftClient;
+import io.fabric8.openshift.client.OpenShiftConfig;
+import io.fabric8.openshift.client.OpenShiftConfigBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine;
@@ -36,19 +39,41 @@ public class Undeploy implements Callable<Integer> {
     private String namespace;
     @CommandLine.Option(names = { "--name" }, description = "Application name", required = true)
     private String name;
+    @CommandLine.Option(names = { "--version" }, description = "Application version", required = true)
+    private String version;
+    @CommandLine.Option(names = { "--openshift" }, description = "Target is openshift")
+    private boolean openshift;
+    @CommandLine.Option(names = { "--server" }, description = "Master URL")
+    private String server;
+    @CommandLine.Option(names = { "--token" }, description = "Token")
+    private String token;
 
     @Override
     public Integer call() throws Exception {
-        Deployment deployment = KubernetesHelper.createDeployment(namespace, name, "", "", 0, 0);
-        Service service = KubernetesHelper.createService(namespace, name, "", 0, 0, false, 0);
-
-        try (KubernetesClient client = new DefaultKubernetesClient()) {
-            LOG.info("Deleting Service...");
-            client.services().inNamespace(namespace).delete(service);
-            LOG.info("Deleting Deployment...");
-            client.apps().deployments().inNamespace(namespace).delete(deployment);
-        } catch (Exception ex) {
-            LOG.error("Error", ex.getMessage());
+        Map labels = KubernetesHelper.getLabels(name, version);
+        if (openshift) {
+            OpenShiftConfig config = new OpenShiftConfigBuilder().withMasterUrl(server).withOauthToken(token).build();
+            try (OpenShiftClient client = new DefaultOpenShiftClient(config)) {
+                LOG.info("Deleting Routes...");
+                client.routes().inNamespace(namespace).withLabels(labels).delete();
+                LOG.info("Deleting Service...");
+                client.services().inNamespace(namespace).withLabels(labels).delete();
+                LOG.info("Deleting Deployment...");
+                client.apps().deployments().inNamespace(namespace).withLabels(labels).delete();
+                LOG.info("Deleting ImageStream...");
+                client.imageStreams().inNamespace(namespace).withLabels(labels).delete();
+                LOG.info("Deleting BuildConfig...");
+                client.buildConfigs().inNamespace(namespace).withLabels(labels).delete();
+            }
+        } else {
+            try (KubernetesClient client = new DefaultKubernetesClient()) {
+                LOG.info("Deleting Service...");
+                client.services().inNamespace(namespace).withLabels(labels).delete();
+                LOG.info("Deleting Deployment...");
+                client.apps().deployments().inNamespace(namespace).withLabels(labels).delete();
+            } catch (Exception ex) {
+                LOG.error("Error", ex.getMessage());
+            }
         }
         return 0;
     }