You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by cd...@apache.org on 2023/12/20 18:07:23 UTC
(camel) branch main updated: CAMEL-20251: Add Camel K commands to Camel JBang
This is an automated email from the ASF dual-hosted git repository.
cdeppisch 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 1b51e7a260d CAMEL-20251: Add Camel K commands to Camel JBang
1b51e7a260d is described below
commit 1b51e7a260d69896503bd0c0da7dc07ad63c771c
Author: Christoph Deppisch <cd...@redhat.com>
AuthorDate: Mon Dec 18 14:43:46 2023 +0100
CAMEL-20251: Add Camel K commands to Camel JBang
- Adds new subcommands to Camel JBang that allow to manage Camel K integrations
- run command to create Integrations on Kubernetes
- list command to list all Integrations on the namespace
- delete command to remove Integrations
- logs command to print log output of a running Integration
---
docs/user-manual/modules/ROOT/nav.adoc | 1 +
.../modules/ROOT/pages/camel-jbang-k.adoc | 147 +++++
.../modules/ROOT/pages/camel-jbang.adoc | 18 +-
dsl/camel-jbang/camel-jbang-core/pom.xml | 27 +
.../dsl/jbang/core/commands/CamelCommand.java | 5 +
.../dsl/jbang/core/commands/CamelJBangMain.java | 99 ++--
.../jbang/core/commands/k/CompressionHelper.java | 55 ++
.../jbang/core/commands/k/IntegrationDelete.java | 66 +++
.../dsl/jbang/core/commands/k/IntegrationGet.java | 96 ++++
.../dsl/jbang/core/commands/k/IntegrationLogs.java | 107 ++++
.../dsl/jbang/core/commands/k/IntegrationRun.java | 571 ++++++++++++++++++++
.../dsl/jbang/core/commands/k/KubeBaseCommand.java | 108 ++++
.../dsl/jbang/core/commands/k/KubeCommand.java | 40 ++
.../jbang/core/commands/k/KubernetesHelper.java | 151 ++++++
.../dsl/jbang/core/commands/k/SourceScheme.java | 78 +++
.../dsl/jbang/core/commands/k/TraitHelper.java | 125 +++++
.../dsl/jbang/core/commands/k/TraitProfile.java | 25 +
.../jbang/core/commands/version/VersionGet.java | 4 +
.../camel/dsl/jbang/core/common/Printer.java | 39 ++
.../dsl/jbang/core/commands/StringPrinter.java | 68 +++
.../core/commands/k/IntegrationDeleteTest.java | 79 +++
.../jbang/core/commands/k/IntegrationGetTest.java | 110 ++++
.../jbang/core/commands/k/IntegrationLogsTest.java | 69 +++
.../jbang/core/commands/k/IntegrationRunTest.java | 594 +++++++++++++++++++++
.../dsl/jbang/core/commands/k/KubeBaseTest.java | 87 +++
.../jbang/core/commands/k/KubeCommandMainTest.java | 119 +++++
.../dsl/jbang/core/commands/k/integration.yaml | 35 ++
.../camel-jbang-core/src/test/resources/pod.yaml | 28 +
.../camel-jbang-core/src/test/resources/route.yaml | 23 +
parent/pom.xml | 1 +
30 files changed, 2926 insertions(+), 49 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/nav.adoc b/docs/user-manual/modules/ROOT/nav.adoc
index 01f22584ca4..b7c39a2e0ec 100644
--- a/docs/user-manual/modules/ROOT/nav.adoc
+++ b/docs/user-manual/modules/ROOT/nav.adoc
@@ -6,6 +6,7 @@
** xref:building.adoc[Building]
** xref:camel-console.adoc[Camel Developer Console]
** xref:camel-jbang.adoc[Camel JBang]
+*** xref:camel-jbang-k.adoc[Camel Integration with Kubernetes]
** xref:camel-maven-plugin.adoc[Camel Maven Plugin]
** xref:camel-component-maven-plugin.adoc[Camel Component Maven Plugin]
** xref:camel-report-maven-plugin.adoc[Camel Maven Report Plugin]
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-k.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-k.adoc
new file mode 100644
index 00000000000..d202b44c724
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-k.adoc
@@ -0,0 +1,147 @@
+= Camel Integration with Kubernetes
+
+Please make sure to meet these prerequisites for running Camel integrations on Kubernetes:
+
+* Connect to namespace on a Kubernetes cluster where you want to run the integration
+* Camel K operator must be installed on the Kubernetes cluster (either installed on the same namespace or as global operator for the whole cluster)
+
+Running Camel routes on Kubernetes is quite simple with Camel JBang.
+In fact, you can develop and test your Camel route locally with Camel JBang and then promote the same source to running it as an integration on Kubernetes.
+Simply run the integration using the `k` subcommand in Camel JBang.
+
+[source,bash]
+----
+camel k run route.yaml
+----
+
+The command runs the Camel integration on Kubernetes.
+More precisely it creates a Camel K Integration custom resource in the current namespace.
+The Camel K operator makes sure to create a proper runtime image and run the integration (usually as a Pod).
+
+The Camel K operator will automatically manage and configure this integration.
+In particular the operator takes care on exposing services, configuring health endpoints, providing metrics, updating image streams and much more.
+
+By default, the run command will not wait for the integration to in state running.
+You need to add `-w` or `--wait` option in order to wait for the integration to become ready.
+
+The `--logs` option makes the command also print the integration output once the integration Pod is running.
+
+The run command offers a lot more options that you may use to configure the Camel K integration.
+
+[width="100%",cols="1m,3",options="header",]
+|=======================================================================
+|Option |Description
+
+|--name
+|The integration name. Use this when the name should not get derived from the source file name.
+
+|--image
+|An image built externally (for instance via CI/CD). Enabling it will skip the integration build phase.
+
+|--kit, -k
+|The kit used to run the integration.
+
+|--profile
+|The trait profile to use for the deployment.
+
+|--service-account
+|The service account used to run this Integration.
+
+|--pod-template
+|The path of the YAML file containing a PodSpec template to be used for the integration pods.
+
+|--operator-id
+|Operator id selected to manage this integration. (default=camel-k)
+
+|--dependency, -d
+|Adds dependency that should be included, use "camel:" prefix for a Camel component, "mvn:org.my:app:1.0" for a Maven dependency.
+
+|--property, -p
+|Add a runtime property or properties file from a path, a config map or a secret (syntax: [my-key=my-value,file:/path/to/my-conf.properties,[configmap,secret]:name]).
+
+|--build-property
+|Add a build time property or properties file from a path, a config map or a secret (syntax: [my-key=my-value,file:/path/to/my-conf.properties,[configmap,secret]:name]]).
+
+|--config
+|Add a runtime configuration from a ConfigMap or a Secret (syntax: [configmap,secret]:name[/key], where name represents the configmap/secret name and key optionally represents the configmap/secret key to be filtered).
+
+|--resource
+|Add a runtime resource from a Configmap or a Secret (syntax: [configmap,secret]:name[/key][@path], where name represents the configmap/secret name, key optionally represents the configmap/secret key to be filtered and path represents the destination path).
+
+|--open-api
+|Add an OpenAPI spec (syntax: [configmap,file]:name).
+
+|--env, -e
+|Set an environment variable in the integration container, for instance "-e MY_VAR=my-value".
+
+|--volume, -v
+|Mount a volume into the integration container, for instance "-v pvcname:/container/path".
+
+|--connect, -c
+|A Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name.
+
+|--source
+|Add source file to your integration, this is added to the list of files listed as arguments of the command.
+
+|--maven-repository
+|Add a maven repository used to resolve dependencies.
+
+|--annotation
+|Add an annotation to the integration. Use name values pairs like "--annotation my.company=hello".
+
+|--label
+|Add a label to the integration. Use name values pairs like "--label my.company=hello".
+
+|--traits, -t
+|Add a label to the integration. Use name values pairs like "--label my.company=hello".
+
+|--use-flows
+|Write yaml sources as Flow objects in the integration custom resource (default=true).
+
+|--compression
+|Enable storage of sources and resources as a compressed binary blobs.
+
+|--wait, -w
+|Wait for the integration to become ready.
+
+|--logs, -l
+|Print logs after integration has been started.
+
+|--output, -o
+|Just output the generated integration custom resource (supports: yaml or json).
+|=======================================================================
+
+You can list the available integration resources with the following command.
+
+[source,bash]
+----
+camel k get
+NAME PHASE KIT READY
+my-route Running kit-123456789 1/1
+----
+
+This looks for all integrations in the current namespace and lists their individual status.
+
+To inspect the log output of a running integration call:
+
+[source,bash]
+----
+camel k logs my-route
+----
+
+The command connects to the running integration Pod and prints the log output.
+Just terminate the process to stop printing the logs.
+
+Of course, you may also delete an integration resource from the cluster.
+
+[source,bash]
+----
+camel k delete my-route
+----
+
+To remove all available integrations on the current namespace use the `--all` option.
+
+[source,bash]
+----
+camel k delete --all
+----
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
index d87d829c0af..657a8c9aa3b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
@@ -70,7 +70,7 @@ So running a simple route will be as easy as doing the following:
docker run apache/camel-jbang:3.20.5 run example.yaml
----
-or
+or
[source,bash]
----
@@ -1082,7 +1082,6 @@ camel run clipboard.xml --dev
Then you can quickly make changes and copy to clipboard, and Camel JBang will update while running.
-
=== Sending messages via Camel
*Available since Camel 4*
@@ -1478,6 +1477,13 @@ camel cmd start-route --all
TIP: You can stop one or more route by their ids by separating using
comma such as: camel cmd start-route --id=route1,hello. Use `camel cmd start-route --help` for more details.
+==== Running Camel integrations on Kubernetes
+
+After developing the Camel routes locally with JBang you may want to run these also on the Kubernetes platform at some point.
+The Camel K commands get you started with this journey and help you to run and manage Camel integrations on Kubernetes.
+
+Read about it in xref:camel-jbang-k.adoc[Camel Integration with Kubernetes].
+
==== Configuring logging levels
You can see the current logging levels of the running Camel integrations by:
@@ -1561,7 +1567,6 @@ $ camel get metric
11562 MyCoolCamel gauge system.load.average.1m 3.58935546875
----
-
==== Listing state of Circuit Breakers
If your Camel integration uses xref:components:eips:circuitBreaker-eip.adoc[Circuit Breaker] then
@@ -2156,7 +2161,6 @@ This will then automatic insert or update the JBang depencies (`//DEPS`) in the
You may want to use this for making it easier to load the source into an IDE editor to do coding.
See previous section for more details.
-
==== Camel route debugging using VSCode or IDEA editors
The Camel route debugger is available by default (the `camel-debug` component is automatically added to the classpath). By default, it can be reached through JMX at the URL `service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi/camel`.
@@ -2905,13 +2909,13 @@ camel sbom --sbom-format=spdx
You can also choose the target runtime as either _quarkus_ or _spring-boot_ as shown:
----
-camel sbom --runtime=quarkus
+camel sbom --runtime=quarkus
----
-or
+or
----
-camel sbom --runtime=spring-boot
+camel sbom --runtime=spring-boot
----
by default `camel-main` will be used
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml b/dsl/camel-jbang/camel-jbang-core/pom.xml
index 44e6a63d4d9..7c5bb588e38 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -85,6 +85,20 @@
<version>${ascii-table-version}</version>
</dependency>
+ <!-- kubernetes -->
+ <dependency>
+ <groupId>io.fabric8</groupId>
+ <artifactId>kubernetes-client</artifactId>
+ <version>${kubernetes-client-version}</version>
+ </dependency>
+
+ <!-- camel k-->
+ <dependency>
+ <groupId>org.apache.camel.k</groupId>
+ <artifactId>camel-k-crds</artifactId>
+ <version>${camel-k-version}</version>
+ </dependency>
+
<!-- jolokia -->
<dependency>
<groupId>org.jolokia</groupId>
@@ -152,6 +166,19 @@
<artifactId>jackson-databind</artifactId>
</dependency>
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-junit5</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.fabric8</groupId>
+ <artifactId>kubernetes-server-mock</artifactId>
+ <version>${kubernetes-client-version}</version>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
</project>
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
index b27ca2476ce..5ea385a69d3 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
@@ -25,6 +25,7 @@ import java.util.Stack;
import java.util.concurrent.Callable;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
import org.apache.camel.dsl.jbang.core.common.RuntimeUtil;
import org.apache.camel.util.StringHelper;
import picocli.CommandLine;
@@ -129,6 +130,10 @@ public abstract class CamelCommand implements Callable<Integer> {
return new File(camelDir, pid + "-debug.json");
}
+ protected Printer printer() {
+ return getMain().getOut();
+ }
+
protected void printConfigurationValues(String header) {
final Properties configProperties = new Properties();
CommandLineHelper.loadProperties(configProperties::putAll);
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 c5a22e8c30c..5933943ce37 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
@@ -20,24 +20,7 @@ import java.util.concurrent.Callable;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelGCAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelLogAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelReloadAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelResetStatsAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteDumpAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStartAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStopAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelSendAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceTop;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelStartupRecorderAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelStubAction;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump;
-import org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction;
-import org.apache.camel.dsl.jbang.core.commands.action.LoggerAction;
-import org.apache.camel.dsl.jbang.core.commands.action.RouteControllerAction;
-import org.apache.camel.dsl.jbang.core.commands.action.TransformMessageAction;
+import org.apache.camel.dsl.jbang.core.commands.action.*;
import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand;
import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogComponent;
import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDataFormat;
@@ -50,34 +33,18 @@ import org.apache.camel.dsl.jbang.core.commands.config.ConfigGet;
import org.apache.camel.dsl.jbang.core.commands.config.ConfigList;
import org.apache.camel.dsl.jbang.core.commands.config.ConfigSet;
import org.apache.camel.dsl.jbang.core.commands.config.ConfigUnset;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelContextStatus;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelContextTop;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelCount;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelProcessorStatus;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelProcessorTop;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelRouteStatus;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelRouteTop;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelStatus;
-import org.apache.camel.dsl.jbang.core.commands.process.CamelTop;
-import org.apache.camel.dsl.jbang.core.commands.process.Hawtio;
-import org.apache.camel.dsl.jbang.core.commands.process.Jolokia;
-import org.apache.camel.dsl.jbang.core.commands.process.ListBlocked;
-import org.apache.camel.dsl.jbang.core.commands.process.ListCircuitBreaker;
-import org.apache.camel.dsl.jbang.core.commands.process.ListConsumer;
-import org.apache.camel.dsl.jbang.core.commands.process.ListEndpoint;
-import org.apache.camel.dsl.jbang.core.commands.process.ListEvent;
-import org.apache.camel.dsl.jbang.core.commands.process.ListHealth;
-import org.apache.camel.dsl.jbang.core.commands.process.ListInflight;
-import org.apache.camel.dsl.jbang.core.commands.process.ListMetric;
-import org.apache.camel.dsl.jbang.core.commands.process.ListProcess;
-import org.apache.camel.dsl.jbang.core.commands.process.ListService;
-import org.apache.camel.dsl.jbang.core.commands.process.ListVault;
-import org.apache.camel.dsl.jbang.core.commands.process.StopProcess;
+import org.apache.camel.dsl.jbang.core.commands.k.IntegrationDelete;
+import org.apache.camel.dsl.jbang.core.commands.k.IntegrationGet;
+import org.apache.camel.dsl.jbang.core.commands.k.IntegrationLogs;
+import org.apache.camel.dsl.jbang.core.commands.k.IntegrationRun;
+import org.apache.camel.dsl.jbang.core.commands.k.KubeCommand;
+import org.apache.camel.dsl.jbang.core.commands.process.*;
import org.apache.camel.dsl.jbang.core.commands.version.VersionCommand;
import org.apache.camel.dsl.jbang.core.commands.version.VersionGet;
import org.apache.camel.dsl.jbang.core.commands.version.VersionList;
import org.apache.camel.dsl.jbang.core.commands.version.VersionSet;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@@ -85,8 +52,13 @@ import picocli.CommandLine.Command;
public class CamelJBangMain implements Callable<Integer> {
private static CommandLine commandLine;
+ private Printer out = new Printer.SystemOutPrinter();
+
public static void run(String... args) {
- CamelJBangMain main = new CamelJBangMain();
+ run(new CamelJBangMain(), args);
+ }
+
+ public static void run(CamelJBangMain main, String... args) {
commandLine = new CommandLine(main)
.addSubcommand("init", new CommandLine(new Init(main)))
.addSubcommand("run", new CommandLine(new Run(main)))
@@ -157,6 +129,11 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("get", new CommandLine(new ConfigGet(main)))
.addSubcommand("unset", new CommandLine(new ConfigUnset(main)))
.addSubcommand("set", new CommandLine(new ConfigSet(main))))
+ .addSubcommand("k", new CommandLine(new KubeCommand(main))
+ .addSubcommand("get", new CommandLine(new IntegrationGet(main)))
+ .addSubcommand("run", new CommandLine(new IntegrationRun(main)))
+ .addSubcommand("delete", new CommandLine(new IntegrationDelete(main)))
+ .addSubcommand("logs", new CommandLine(new IntegrationLogs(main))))
.addSubcommand("version", new CommandLine(new VersionCommand(main))
.addSubcommand("get", new CommandLine(new VersionGet(main)))
.addSubcommand("set", new CommandLine(new VersionSet(main)))
@@ -170,6 +147,16 @@ public class CamelJBangMain implements Callable<Integer> {
CommandLineHelper.augmentWithUserConfiguration(commandLine, args);
int exitCode = commandLine.execute(args);
+ main.quit(exitCode);
+ }
+
+ /**
+ * Finish this main with given exit code. By default, uses system exit to terminate. Subclasses may want to
+ * overwrite this exit behavior e.g. during unit tests.
+ *
+ * @param exitCode
+ */
+ protected void quit(int exitCode) {
System.exit(exitCode);
}
@@ -179,4 +166,32 @@ public class CamelJBangMain implements Callable<Integer> {
return 0;
}
+ /**
+ * Gets the main output printer to write command output.
+ *
+ * @return the printer.
+ */
+ public Printer getOut() {
+ return out;
+ }
+
+ /**
+ * Sets the main output printer.
+ *
+ * @param out the printer to use for command output.
+ */
+ public void setOut(Printer out) {
+ this.out = out;
+ }
+
+ /**
+ * Uses this printer for writing command output.
+ *
+ * @param out to use with this main.
+ */
+ public CamelJBangMain withPrinter(Printer out) {
+ this.out = out;
+ return this;
+ }
+
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/CompressionHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/CompressionHelper.java
new file mode 100644
index 00000000000..07f16123326
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/CompressionHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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.k;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+import org.apache.camel.RuntimeCamelException;
+
+/**
+ * Utility helper to handle base64 compression of sources.
+ */
+public class CompressionHelper {
+
+ private CompressionHelper() {
+ // prevent instantiation of utility class.
+ }
+
+ /**
+ * Compress given data with deflate and base64 encoding.
+ *
+ * @param data to be compressed.
+ * @return compressed base64 encoded data
+ */
+ public static String compressBase64(String data) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION, true);
+ try (DeflaterOutputStream dos = new DeflaterOutputStream(bos, compressor, true)) {
+ dos.write(data.getBytes(StandardCharsets.UTF_8));
+ dos.flush();
+ return new String(Base64.getEncoder().encode(bos.toByteArray()));
+ } catch (IOException e) {
+ throw new RuntimeCamelException("Failed to compress data", e);
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDelete.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDelete.java
new file mode 100644
index 00000000000..647963f775d
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDelete.java
@@ -0,0 +1,66 @@
+/*
+ * 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.k;
+
+import java.util.Arrays;
+import java.util.List;
+
+import io.fabric8.kubernetes.api.model.StatusDetails;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "delete", description = "Delete integrations deployed on Kubernetes", sortOptions = false)
+public class IntegrationDelete extends KubeBaseCommand {
+
+ @CommandLine.Parameters(description = "Integration names to delete.",
+ arity = "0..*", paramLabel = "<names>")
+ String[] names;
+
+ @CommandLine.Option(names = { "--all" },
+ description = "Delete all integrations in current namespace.")
+ boolean all;
+
+ public IntegrationDelete(CamelJBangMain main) {
+ super(main);
+ }
+
+ public Integer doCall() throws Exception {
+ if (all) {
+ client(Integration.class).delete();
+ printer().println("Integrations deleted");
+ } else {
+ if (names == null) {
+ throw new RuntimeCamelException("Missing integration name as argument or --all option.");
+ }
+
+ for (String name : Arrays.stream(names).map(KubernetesHelper::sanitize).toList()) {
+ List<StatusDetails> status = client(Integration.class).withName(name).delete();
+ if (status.isEmpty()) {
+ printer().printf("Integration %s deletion skipped - not found%n", name);
+ } else {
+ printer().printf("Integration %s deleted%n", name);
+ }
+ }
+ }
+
+ return 0;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGet.java
new file mode 100644
index 00000000000..55975e13302
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGet.java
@@ -0,0 +1,96 @@
+/*
+ * 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.k;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "get", description = "List Camel integrations deployed on Kubernetes", sortOptions = false)
+public class IntegrationGet extends KubeBaseCommand {
+
+ @CommandLine.Option(names = { "--name" },
+ description = "List only given integration name in the output")
+ boolean name;
+
+ public IntegrationGet(CamelJBangMain main) {
+ super(main);
+ }
+
+ public Integer doCall() throws Exception {
+ List<Row> rows = new ArrayList<>();
+
+ List<Integration> integrations = client(Integration.class).list().getItems();
+ integrations
+ .forEach(integration -> {
+ Row row = new Row();
+ row.name = integration.getMetadata().getName();
+
+ row.ready = "0/1";
+ if (integration.getStatus() != null) {
+ row.phase = integration.getStatus().getPhase();
+
+ if (integration.getStatus().getConditions() != null) {
+ row.ready
+ = integration.getStatus().getConditions().stream().filter(c -> c.getType().equals("Ready"))
+ .anyMatch(c -> c.getStatus().equals("True")) ? "1/1" : "0/1";
+ }
+
+ row.kit = integration.getStatus().getIntegrationKit() != null
+ ? integration.getStatus().getIntegrationKit().getName() : "";
+ } else {
+ row.phase = "Unknown";
+ }
+
+ rows.add(row);
+ });
+
+ if (!rows.isEmpty()) {
+ if (name) {
+ rows.forEach(r -> printer().println(r.name));
+ } else {
+ printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+ new Column().header("NAME").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(40, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.name),
+ new Column().header("PHASE").headerAlign(HorizontalAlign.LEFT)
+ .with(r -> r.phase),
+ new Column().header("KIT").headerAlign(HorizontalAlign.LEFT).with(r -> r.kit),
+ new Column().header("READY").dataAlign(HorizontalAlign.CENTER).with(r -> r.ready))));
+ }
+ }
+
+ return 0;
+ }
+
+ private static class Row {
+ String name;
+ String ready;
+ String phase;
+ String kit;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogs.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogs.java
new file mode 100644
index 00000000000..faf96f0c27c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogs.java
@@ -0,0 +1,107 @@
+/*
+ * 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.k;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodList;
+import io.fabric8.kubernetes.client.dsl.LogWatch;
+import io.fabric8.kubernetes.client.dsl.PodResource;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "logs", description = "Print the logs of an integration", sortOptions = false)
+public class IntegrationLogs extends KubeBaseCommand {
+
+ @CommandLine.Parameters(description = "Integration name to grab logs from.",
+ paramLabel = "<name>")
+ String name;
+
+ @CommandLine.Option(names = { "--tail", "-t" },
+ defaultValue = "-1",
+ description = "The number of lines from the end of the logs to show. Defaults to -1 to show all the lines.")
+ int tail = -1;
+
+ public IntegrationLogs(CamelJBangMain main) {
+ super(main);
+ }
+
+ public Integer doCall() throws Exception {
+ String integrationName = KubernetesHelper.sanitize(name);
+ Integration integration = client(Integration.class).withName(integrationName).get();
+
+ if (integration == null) {
+ printer().printf("Integration %s not found%n", integrationName);
+ return 0;
+ }
+
+ watchLogs(integration);
+
+ return 0;
+ }
+
+ void watchLogs(Integration integration) {
+ PodList pods = pods().withLabel(KubeCommand.INTEGRATION_LABEL, integration.getMetadata().getName()).list();
+
+ Pod pod = pods.getItems().stream()
+ .filter(p -> p.getStatus().getPhase() != null && !"Terminated".equals(p.getStatus().getPhase()))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeCamelException("Failed to find integration pod"));
+
+ String containerName = null;
+ if (pod.getSpec() != null && pod.getSpec().getContainers() != null) {
+ if (pod.getSpec().getContainers().stream()
+ .anyMatch(container -> KubeCommand.INTEGRATION_CONTAINER_NAME.equals(container.getName()))) {
+ containerName = KubeCommand.INTEGRATION_CONTAINER_NAME;
+ } else if (pod.getSpec().getContainers().size() > 0) {
+ containerName = pod.getSpec().getContainers().get(0).getName();
+ }
+ }
+
+ PodResource podRes = pods().withName(pod.getMetadata().getName());
+
+ LogWatch logs;
+ if (tail < 0) {
+ if (containerName != null) {
+ logs = podRes.inContainer(containerName).watchLog();
+ } else {
+ logs = podRes.watchLog();
+ }
+ } else {
+ if (containerName != null) {
+ logs = podRes.inContainer(containerName).tailingLines(tail).watchLog();
+ } else {
+ logs = podRes.tailingLines(tail).watchLog();
+ }
+ }
+
+ try (logs; BufferedReader reader = new BufferedReader(new InputStreamReader(logs.getOutput()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ printer().println(line);
+ }
+ } catch (IOException e) {
+ printer().println("Failed to read integration pod logs - " + e.getMessage());
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java
new file mode 100644
index 00000000000..6d5b5a267ca
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java
@@ -0,0 +1,571 @@
+/*
+ * 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.k;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.GistHelper;
+import org.apache.camel.dsl.jbang.core.common.GitHubHelper;
+import org.apache.camel.dsl.jbang.core.common.JSonHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.apache.camel.github.GistResourceResolver;
+import org.apache.camel.github.GitHubResourceResolver;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.engine.DefaultResourceResolvers;
+import org.apache.camel.spi.ResourceResolver;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.v1.Integration;
+import org.apache.camel.v1.IntegrationSpec;
+import org.apache.camel.v1.integrationspec.Flows;
+import org.apache.camel.v1.integrationspec.IntegrationKit;
+import org.apache.camel.v1.integrationspec.Sources;
+import org.apache.camel.v1.integrationspec.Template;
+import org.apache.camel.v1.integrationspec.Traits;
+import org.apache.camel.v1.integrationspec.template.Spec;
+import org.apache.camel.v1.integrationspec.traits.Builder;
+import org.apache.camel.v1.integrationspec.traits.Camel;
+import org.apache.camel.v1.integrationspec.traits.Container;
+import org.apache.camel.v1.integrationspec.traits.Environment;
+import org.apache.camel.v1.integrationspec.traits.Mount;
+import org.apache.camel.v1.integrationspec.traits.Openapi;
+import org.apache.camel.v1.integrationspec.traits.ServiceBinding;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "run", description = "Run Camel integrations on Kubernetes", sortOptions = false)
+public class IntegrationRun extends KubeBaseCommand {
+
+ @CommandLine.Parameters(description = "The Camel file(s) to run.",
+ arity = "0..9", paramLabel = "<files>")
+ String[] filePaths;
+
+ @CommandLine.Option(names = { "--name" },
+ description = "The integration name. Use this when the name should not get derived from the source file name.")
+ String name;
+
+ @CommandLine.Option(names = { "--image" },
+ description = "An image built externally (for instance via CI/CD). Enabling it will skip the integration build phase.")
+ String image;
+
+ @CommandLine.Option(names = { "--kit", "-k" }, description = "The kit used to run the integration.")
+ String kit;
+
+ @CommandLine.Option(names = { "--profile" }, description = "The trait profile to use for the deployment.")
+ String profile;
+
+ @CommandLine.Option(names = { "--service-account" }, description = "The service account used to run this Integration.")
+ String serviceAccount;
+
+ @CommandLine.Option(names = { "--pod-template" },
+ description = "The path of the YAML file containing a PodSpec template to be used for the integration pods.")
+ String podTemplate;
+
+ @CommandLine.Option(names = { "--operator-id", "-x" }, defaultValue = "camel-k",
+ description = "Operator id selected to manage this integration.")
+ String operatorId = "camel-k";
+
+ @CommandLine.Option(names = { "--dependency", "-d" },
+ description = "Adds dependency that should be included, use \"camel:\" prefix for a Camel component, \"mvn:org.my:app:1.0\" for a Maven dependency.")
+ String[] dependencies;
+
+ @CommandLine.Option(names = { "--property", "-p" },
+ description = "Add a runtime property or properties file from a path, a config map or a secret (syntax: [my-key=my-value|file:/path/to/my-conf.properties|[configmap|secret]:name]).")
+ String[] properties;
+
+ @CommandLine.Option(names = { "--build-property" },
+ description = "Add a build time property or properties file from a path, a config map or a secret (syntax: [my-key=my-value|file:/path/to/my-conf.properties|[configmap|secret]:name]]).")
+ String[] buildProperties;
+
+ @CommandLine.Option(names = { "--config" },
+ description = "Add a runtime configuration from a ConfigMap or a Secret (syntax: [configmap|secret]:name[/key], where name represents the configmap/secret name and key optionally represents the configmap/secret key to be filtered).")
+ String[] configs;
+
+ @CommandLine.Option(names = { "--resource" },
+ description = "Add a runtime resource from a Configmap or a Secret (syntax: [configmap|secret]:name[/key][@path], where name represents the configmap/secret name, key optionally represents the configmap/secret key to be filtered and path represents the destination path).")
+ String[] resources;
+
+ @CommandLine.Option(names = { "--open-api" }, description = "Add an OpenAPI spec (syntax: [configmap|file]:name).")
+ String[] openApis;
+
+ @CommandLine.Option(names = { "--env", "-e" },
+ description = "Set an environment variable in the integration container, for instance \"-e MY_VAR=my-value\".")
+ String[] envVars;
+
+ @CommandLine.Option(names = { "--volume", "-v" },
+ description = "Mount a volume into the integration container, for instance \"-v pvcname:/container/path\".")
+ String[] volumes;
+
+ @CommandLine.Option(names = { "--connect", "-c" },
+ description = "A Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name.")
+ String[] connects;
+
+ @CommandLine.Option(names = { "--source" },
+ description = "Add source file to your integration, this is added to the list of files listed as arguments of the command.")
+ String[] sources;
+
+ @CommandLine.Option(names = { "--maven-repository" }, description = "Add a maven repository used to resolve dependencies.")
+ String[] repositories;
+
+ @CommandLine.Option(names = { "--annotation" },
+ description = "Add an annotation to the integration. Use name values pairs like \"--annotation my.company=hello\".")
+ String[] annotations;
+
+ @CommandLine.Option(names = { "--label" },
+ description = "Add a label to the integration. Use name values pairs like \"--label my.company=hello\".")
+ String[] labels;
+
+ @CommandLine.Option(names = { "--traits", "-t" },
+ description = "Add a label to the integration. Use name values pairs like \"--label my.company=hello\".")
+ String[] traits;
+
+ @CommandLine.Option(names = { "--use-flows" }, defaultValue = "true",
+ description = "Write yaml sources as Flow objects in the integration custom resource.")
+ boolean useFlows = true;
+
+ @CommandLine.Option(names = { "--compression" },
+ description = "Enable storage of sources and resources as a compressed binary blobs.")
+ boolean compression;
+
+ @CommandLine.Option(names = { "--wait", "-w" }, description = "Wait for the integration to become ready.")
+ boolean wait;
+
+ @CommandLine.Option(names = { "--logs", "-l" }, description = "Print logs after integration has been started.")
+ boolean logs;
+
+ @CommandLine.Option(names = { "--output", "-o" },
+ description = "Just output the generated integration custom resource (supports: yaml or json).")
+ String output;
+
+ public IntegrationRun(CamelJBangMain main) {
+ super(main);
+ }
+
+ public Integer doCall() throws Exception {
+ List<String> integrationSources
+ = Stream.concat(Arrays.stream(Optional.ofNullable(filePaths).orElseGet(() -> new String[] {})),
+ Arrays.stream(Optional.ofNullable(sources).orElseGet(() -> new String[] {}))).toList();
+
+ Integration integration = new Integration();
+ integration.setSpec(new IntegrationSpec());
+ integration.getMetadata()
+ .setName(getIntegrationName(integrationSources));
+
+ if (dependencies != null && dependencies.length > 0) {
+ List<String> deps = new ArrayList<>();
+ for (String dependency : dependencies) {
+ String normalized = normalizeDependency(dependency);
+ validateDependency(normalized, printer());
+ deps.add(normalized);
+ }
+
+ integration.getSpec().setDependencies(deps);
+ }
+
+ if (kit != null) {
+ IntegrationKit integrationKit = new IntegrationKit();
+ integrationKit.setName(kit);
+ integration.getSpec().setIntegrationKit(integrationKit);
+ }
+
+ if (profile != null) {
+ TraitProfile p = TraitProfile.valueOf(profile.toUpperCase(Locale.US));
+ integration.getSpec().setProfile(p.name().toLowerCase(Locale.US));
+ }
+
+ if (repositories != null && repositories.length > 0) {
+ integration.getSpec().setRepositories(List.of(repositories));
+ }
+
+ if (annotations != null && annotations.length > 0) {
+ integration.getMetadata().setAnnotations(Arrays.stream(annotations)
+ .filter(it -> it.contains("="))
+ .map(it -> it.split("="))
+ .filter(it -> it.length == 2)
+ .collect(Collectors.toMap(it -> it[0].trim(), it -> it[1].trim())));
+ }
+
+ if (operatorId != null) {
+ if (integration.getMetadata().getAnnotations() == null) {
+ integration.getMetadata().setAnnotations(new HashMap<>());
+ }
+
+ integration.getMetadata().getAnnotations().put(KubeCommand.OPERATOR_ID_LABEL, operatorId);
+ }
+
+ if (labels != null && labels.length > 0) {
+ integration.getMetadata().setLabels(Arrays.stream(labels)
+ .filter(it -> it.contains("="))
+ .map(it -> it.split("="))
+ .filter(it -> it.length == 2)
+ .collect(Collectors.toMap(it -> it[0].trim(), it -> it[1].trim())));
+ }
+
+ Traits traitsSpec;
+ if (traits != null && traits.length > 0) {
+ traitsSpec = TraitHelper.parseTraits(traits);
+ } else {
+ traitsSpec = new Traits();
+ }
+
+ if (image != null) {
+ Container containerTrait = new Container();
+ containerTrait.setImage(image);
+ traitsSpec.setContainer(containerTrait);
+ } else {
+ List<Source> resolvedSources = resolveSources(integrationSources);
+
+ List<Flows> flows = new ArrayList<>();
+ List<Sources> sources = new ArrayList<>();
+ for (Source source : resolvedSources) {
+ if (useFlows && source.isYaml() && !source.compressed()) {
+ JsonNode json = KubernetesHelper.json().convertValue(
+ KubernetesHelper.yaml().load(source.content()), JsonNode.class);
+ if (json.isArray()) {
+ for (JsonNode item : json) {
+ Flows flowSpec = new Flows();
+ flowSpec.setAdditionalProperties(KubernetesHelper.json().readerFor(Map.class).readValue(item));
+ flows.add(flowSpec);
+ }
+ } else {
+ Flows flowSpec = new Flows();
+ flowSpec.setAdditionalProperties(KubernetesHelper.json().readerFor(Map.class).readValue(json));
+ flows.add(flowSpec);
+ }
+ } else {
+ Sources sourceSpec = new Sources();
+ sourceSpec.setName(source.name());
+ sourceSpec.setLanguage(source.language());
+ sourceSpec.setContent(source.content());
+ sourceSpec.setCompression(source.compressed());
+ sources.add(sourceSpec);
+ }
+ }
+
+ if (!flows.isEmpty()) {
+ integration.getSpec().setFlows(flows);
+ }
+
+ if (!sources.isEmpty()) {
+ integration.getSpec().setSources(sources);
+ }
+ }
+
+ if (podTemplate != null) {
+ Source templateSource = resolveSource(podTemplate);
+ if (!templateSource.isYaml()) {
+ throw new RuntimeCamelException(
+ ("Unsupported pod template %s - " +
+ "please use proper YAML source").formatted(templateSource.extension()));
+ }
+
+ Spec podSpec = KubernetesHelper.yaml().loadAs(templateSource.content(), Spec.class);
+ Template template = new Template();
+ template.setSpec(podSpec);
+ integration.getSpec().setTemplate(template);
+ }
+
+ convertOptionsToTraits(traitsSpec);
+ integration.getSpec().setTraits(traitsSpec);
+
+ if (serviceAccount != null) {
+ integration.getSpec().setServiceAccountName(serviceAccount);
+ }
+
+ if (output != null) {
+ switch (output) {
+ case "yaml" -> printer().println(KubernetesHelper.yaml().dumpAsMap(integration));
+ case "json" -> printer().println(
+ JSonHelper.prettyPrint(KubernetesHelper.json().writer().writeValueAsString(integration), 2));
+ default -> printer().printf("Unsupported output format %s%n", output);
+ }
+
+ return 0;
+ }
+
+ final AtomicBoolean updated = new AtomicBoolean(false);
+ client(Integration.class).resource(integration).createOr(it -> {
+ updated.set(true);
+ return it.update();
+ });
+
+ if (updated.get()) {
+ printer().printf("Integration %s updated%n", integration.getMetadata().getName());
+ } else {
+ printer().printf("Integration %s created%n", integration.getMetadata().getName());
+ }
+
+ if (wait || logs) {
+ client(Integration.class).withName(integration.getMetadata().getName())
+ .waitUntilCondition(it -> "Running".equals(it.getStatus().getPhase()), 10, TimeUnit.MINUTES);
+ }
+
+ if (logs) {
+ new IntegrationLogs(getMain()).watchLogs(integration);
+ }
+
+ return 0;
+ }
+
+ private void convertOptionsToTraits(Traits traitsSpec) {
+ Mount mountTrait = null;
+
+ if (configs != null && configs.length > 0) {
+ mountTrait = new Mount();
+ mountTrait.setConfigs(List.of(configs));
+ }
+
+ if (resources != null && resources.length > 0) {
+ if (mountTrait == null) {
+ mountTrait = new Mount();
+ }
+ mountTrait.setResources(List.of(resources));
+ }
+
+ if (volumes != null && volumes.length > 0) {
+ if (mountTrait == null) {
+ mountTrait = new Mount();
+ }
+ mountTrait.setVolumes(List.of(volumes));
+ }
+
+ if (mountTrait != null) {
+ traitsSpec.setMount(mountTrait);
+ }
+
+ if (openApis != null && openApis.length > 0) {
+ Openapi openapiTrait = new Openapi();
+ openapiTrait.setConfigmaps(List.of(openApis));
+ traitsSpec.setOpenapi(openapiTrait);
+ }
+
+ if (properties != null && properties.length > 0) {
+ Camel camelTrait = new Camel();
+ camelTrait.setProperties(List.of(properties));
+ traitsSpec.setCamel(camelTrait);
+ }
+
+ if (buildProperties != null && buildProperties.length > 0) {
+ Builder builderTrait = new Builder();
+ builderTrait.setProperties(List.of(buildProperties));
+ traitsSpec.setBuilder(builderTrait);
+ }
+
+ if (envVars != null && envVars.length > 0) {
+ Environment environmentTrait = new Environment();
+ environmentTrait.setVars(List.of(envVars));
+ traitsSpec.setEnvironment(environmentTrait);
+ }
+
+ if (connects != null && connects.length > 0) {
+ ServiceBinding serviceBindingTrait = new ServiceBinding();
+ serviceBindingTrait.setServices(List.of(connects));
+ traitsSpec.setServiceBinding(serviceBindingTrait);
+ }
+ }
+
+ private Source resolveSource(String source) {
+ List<Source> resolved = resolveSources(Collections.singletonList(source));
+ if (resolved.isEmpty()) {
+ throw new RuntimeCamelException("Failed to resolve source file: " + source);
+ } else {
+ return resolved.get(0);
+ }
+ }
+
+ private List<Source> resolveSources(List<String> sources) {
+ List<Source> resolved = new ArrayList<>();
+ for (String source : sources) {
+ SourceScheme sourceScheme = SourceScheme.fromUri(source);
+ String fileExtension = FileUtil.onlyExt(source);
+ String fileName = SourceScheme.onlyName(FileUtil.onlyName(source)) + "." + fileExtension;
+ try {
+ switch (sourceScheme) {
+ case GIST -> {
+ StringJoiner all = new StringJoiner(",");
+ GistHelper.fetchGistUrls(source, all);
+
+ try (ResourceResolver resolver = new GistResourceResolver()) {
+ for (String uri : all.toString().split(",")) {
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(uri).getInputStream()),
+ fileExtension, compression, false));
+ }
+ }
+ }
+ case HTTP -> {
+ try (ResourceResolver resolver = new DefaultResourceResolvers.HttpResolver()) {
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(source).getInputStream()),
+ fileExtension, compression, false));
+ }
+ }
+ case HTTPS -> {
+ try (ResourceResolver resolver = new DefaultResourceResolvers.HttpsResolver()) {
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(source).getInputStream()),
+ fileExtension, compression, false));
+ }
+ }
+ case FILE -> {
+ try (ResourceResolver resolver = new DefaultResourceResolvers.FileResolver()) {
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(source).getInputStream()),
+ fileExtension, compression, true));
+ }
+ }
+ case CLASSPATH -> {
+ try (ResourceResolver resolver = new DefaultResourceResolvers.ClasspathResolver()) {
+ resolver.setCamelContext(new DefaultCamelContext());
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(source).getInputStream()),
+ fileExtension, compression, true));
+ }
+ }
+ case GITHUB, RAW_GITHUB -> {
+ StringJoiner all = new StringJoiner(",");
+ GitHubHelper.fetchGithubUrls(source, all);
+
+ try (ResourceResolver resolver = new GitHubResourceResolver()) {
+ for (String uri : all.toString().split(",")) {
+ resolved.add(new Source(
+ fileName,
+ IOHelper.loadText(resolver.resolve(uri).getInputStream()),
+ fileExtension, compression, false));
+ }
+ }
+ }
+ case UNKNOWN -> {
+ try (FileInputStream fis = new FileInputStream(source)) {
+ resolved.add(new Source(fileName, IOHelper.loadText(fis), fileExtension, compression, true));
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeCamelException("Failed to resolve sources", e);
+ }
+ }
+ return resolved;
+ }
+
+ private String getIntegrationName(List<String> sources) {
+ if (name != null) {
+ return KubernetesHelper.sanitize(name);
+ } else if (image != null) {
+ return KubernetesHelper.sanitize(image.replaceAll(":", "-v"));
+ } else if (ObjectHelper.isNotEmpty(sources)) {
+ return KubernetesHelper.sanitize(SourceScheme.onlyName(FileUtil.onlyName(sources.get(0))));
+ }
+
+ throw new RuntimeCamelException(
+ "Failed to resolve integration name - please give an image, an explicit name option or a single source file");
+ }
+
+ /**
+ * Normalize dependency expression. Basically replaces "camel-" based artifact names to use proper "camel:" prefix.
+ *
+ * @param dependency to normalize.
+ * @return normalized dependency.
+ */
+ private static String normalizeDependency(String dependency) {
+ if (dependency.startsWith("camel-quarkus-")) {
+ return "camel:" + dependency.substring("camel-quarkus-".length());
+ }
+
+ if (dependency.startsWith("camel-quarkus:")) {
+ return "camel:" + dependency.substring("camel-quarkus:".length());
+ }
+
+ if (dependency.startsWith("camel-k-")) {
+ return "camel-k:" + dependency.substring("camel-k-".length());
+ }
+
+ if (dependency.startsWith("camel-")) {
+ return "camel:" + dependency.substring("camel-".length());
+ }
+
+ return dependency;
+ }
+
+ /**
+ * Validates given dependency expression.
+ *
+ * @param dependency to validate.
+ * @param printer to output potential warnings.
+ */
+ private static void validateDependency(String dependency, Printer printer) {
+ if (dependency.startsWith("mvn:org.apache.camel:")) {
+ String suggested = normalizeDependency(dependency.split(":")[2]);
+ printer.printf("Warning: do not use '%s' as a dependency. Please use '%s' instead%n", dependency, suggested);
+ }
+ if (dependency.startsWith("mvn:org.apache.camel.quarkus:")) {
+ String suggested = normalizeDependency(dependency.split(":")[2]);
+ printer.printf("Warning: do not use '%s' as a dependency. Please use '%s' instead%n", dependency, suggested);
+ }
+ }
+
+ private record Source(String name, String content, String extension, boolean compressed, boolean local) {
+
+ /**
+ * Provides source contant and automatically handles compression of content when enabled.
+ *
+ * @return the content, maybe compressed.
+ */
+ public String content() {
+ if (compressed()) {
+ return CompressionHelper.compressBase64(content);
+ }
+
+ return content;
+ }
+
+ public String language() {
+ if ("yml".equals(extension)) {
+ return "yaml";
+ }
+
+ return extension;
+ }
+
+ public boolean isYaml() {
+ return "yaml".equals(language());
+ }
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseCommand.java
new file mode 100644
index 00000000000..381cc544612
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseCommand.java
@@ -0,0 +1,108 @@
+/*
+ * 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.k;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.KubernetesResourceList;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodList;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
+import io.fabric8.kubernetes.client.dsl.PodResource;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import picocli.CommandLine;
+
+/**
+ * Bas command supports Kubernetes client related options such as namespace or custom kube config option. Automatically
+ * applies the options to the Kubernetes client instance that is being used to run commands.
+ */
+abstract class KubeBaseCommand extends CamelCommand {
+
+ @CommandLine.Option(names = { "--kube-config" },
+ description = "Path to the kube config file to initialize Kubernetes client")
+ String kubeConfig;
+
+ @CommandLine.Option(names = { "--namespace", "-n" }, description = "Namespace to use for all operations")
+ String namespace;
+
+ private KubernetesClient kubernetesClient;
+
+ public KubeBaseCommand(CamelJBangMain main) {
+ super(main);
+ }
+
+ /**
+ * Provides access to the Kubernetes client and automatically sets current namespace if option is given.
+ *
+ * @param resourceType the Kubernetes resource this client will operate with.
+ * @return namespaced client if applicable.
+ * @param <T> resource type parameter.
+ */
+ protected <T extends HasMetadata> NonNamespaceOperation<T, KubernetesResourceList<T>, Resource<T>> client(
+ Class<T> resourceType) {
+ if (namespace != null) {
+ return client().resources(resourceType).inNamespace(namespace);
+ }
+
+ return client().resources(resourceType);
+ }
+
+ /**
+ * Provides access to Pod resources using the Kubernetes client with current namespace automatically set if option
+ * is given.
+ *
+ * @return namespaced client if applicable.
+ */
+ protected NonNamespaceOperation<Pod, PodList, PodResource> pods() {
+ if (namespace != null) {
+ return client().pods().inNamespace(namespace);
+ }
+
+ return client().pods();
+ }
+
+ /**
+ * Gets Kubernetes client. In case custom kubeConfig option is set initializes the client with the config otherwise
+ * uses default client.
+ *
+ * @return
+ */
+ protected KubernetesClient client() {
+ if (kubernetesClient == null) {
+ if (kubeConfig != null) {
+ kubernetesClient = KubernetesHelper.getKubernetesClient(kubeConfig);
+ }
+
+ kubernetesClient = KubernetesHelper.getKubernetesClient();
+ }
+
+ return kubernetesClient;
+ }
+
+ /**
+ * Sets the Kubernetes client.
+ *
+ * @param kubernetesClient
+ */
+ KubeBaseCommand withClient(KubernetesClient kubernetesClient) {
+ this.kubernetesClient = kubernetesClient;
+ return this;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommand.java
new file mode 100644
index 00000000000..8bc5105ceaa
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommand.java
@@ -0,0 +1,40 @@
+/*
+ * 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.k;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "k",
+ description = "Manage Camel integrations on Kubernetes (use config --help to see sub commands)")
+public class KubeCommand extends KubeBaseCommand {
+
+ public static final String OPERATOR_ID_LABEL = "camel.apache.org/operator.id";
+ public static final String INTEGRATION_LABEL = "camel.apache.org/integration";
+ public static final String INTEGRATION_CONTAINER_NAME = "integration";
+
+ public KubeCommand(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doCall() throws Exception {
+ // defaults to list integrations deployed on Kubernetes
+ new CommandLine(new IntegrationGet(getMain())).execute();
+ return 0;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
new file mode 100644
index 00000000000..85dff123daa
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
@@ -0,0 +1,151 @@
+/*
+ * 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.k;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.StringHelper;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.introspector.Property;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.representer.Representer;
+
+/**
+ * Helper class provides access to cached Kubernetes client. Also provides access to generic Json and Yaml mappers.
+ */
+public final class KubernetesHelper {
+
+ private static KubernetesClient kubernetesClient;
+
+ /** Clients with custom config */
+ private static final Map<String, KubernetesClient> clients = new HashMap<>();
+
+ private static final ObjectMapper OBJECT_MAPPER;
+
+ static {
+ OBJECT_MAPPER = JsonMapper.builder()
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
+ .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
+ .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE)
+ .enable(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES)
+ .build()
+ .setDefaultPropertyInclusion(
+ JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
+ }
+
+ private KubernetesHelper() {
+ //prevent instantiation of utility class.
+ }
+
+ /**
+ * Gets the default Kubernetes client.
+ *
+ * @return
+ */
+ public static KubernetesClient getKubernetesClient() {
+ if (kubernetesClient == null) {
+ kubernetesClient = new KubernetesClientBuilder().build();
+ }
+
+ return kubernetesClient;
+ }
+
+ /**
+ * Create or get Kubernetes client with given config.
+ *
+ * @param config
+ * @return
+ */
+ public static KubernetesClient getKubernetesClient(String config) {
+ if (clients.containsKey(config)) {
+ return clients.get(config);
+ }
+
+ return clients.put(config, new KubernetesClientBuilder().withConfig(config).build());
+ }
+
+ /**
+ * 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.
+ *
+ * @return
+ */
+ public static Yaml yaml() {
+ Representer representer = new Representer(new DumperOptions()) {
+ @Override
+ protected NodeTuple representJavaBeanProperty(
+ Object javaBean, Property property, Object propertyValue, Tag customTag) {
+ // if value of property is null, ignore it.
+ if (propertyValue == null || (propertyValue instanceof Collection && ((Collection<?>) propertyValue).isEmpty())
+ ||
+ (propertyValue instanceof Map && ((Map<?, ?>) propertyValue).isEmpty())) {
+ return null;
+ } else {
+ return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
+ }
+ }
+ };
+ representer.getPropertyUtils().setSkipMissingProperties(true);
+ return new Yaml(representer);
+ }
+
+ public static ObjectMapper json() {
+ return OBJECT_MAPPER;
+ }
+
+ /**
+ * Sanitize given name to meet Kubernetes resource naming requirements.
+ *
+ * @param name to sanitize.
+ * @return sanitized name ready to be used as a Kubernetes resource name.
+ */
+ public static String sanitize(String name) {
+ name = FileUtil.onlyName(name);
+ name = StringHelper.sanitize(name);
+ name = StringHelper.camelCaseToDash(name);
+ name = name.toLowerCase(Locale.US);
+ name = name.replaceAll("[^a-z0-9-]", "");
+ name = name.trim();
+ return name;
+ }
+
+ /**
+ * Overwrites the kubernetes client. Typically used by unit tests.
+ *
+ * @param kubernetesClient
+ */
+ static void setKubernetesClient(KubernetesClient kubernetesClient) {
+ KubernetesHelper.kubernetesClient = kubernetesClient;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/SourceScheme.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/SourceScheme.java
new file mode 100644
index 00000000000..90fdbfdfabe
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/SourceScheme.java
@@ -0,0 +1,78 @@
+/*
+ * 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.k;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Supported set of file resource and URL schemes that may be used to resolve an integration resource (e.g. source
+ * file).
+ */
+public enum SourceScheme {
+
+ GIST("https://gist.github"),
+ GITHUB("https://github.com/"),
+ RAW_GITHUB("https://raw.githubusercontent.com/"),
+ FILE,
+ CLASSPATH,
+ HTTP,
+ HTTPS,
+ UNKNOWN;
+
+ private final String uri;
+
+ SourceScheme() {
+ this(null);
+ }
+
+ SourceScheme(String uri) {
+ this.uri = uri;
+ }
+
+ /**
+ * Try to resolve source scheme from given file path URL. Checks for special GIST and GITHUB endpoint URLs. By
+ * default, uses unknown scheme usually leads to loading resource from file system.
+ *
+ * @param path
+ * @return
+ */
+ public static SourceScheme fromUri(String path) {
+ return Arrays.stream(values())
+ .filter(scheme -> path.startsWith(scheme.name().toLowerCase(Locale.US) + ":") ||
+ (scheme.uri != null && path.startsWith(scheme.uri)))
+ .findFirst()
+ .orElse(UNKNOWN); // use file as default scheme
+ }
+
+ /**
+ * If any strip scheme prefix from given name.
+ *
+ * @param name
+ * @return
+ */
+ public static String onlyName(String name) {
+ for (SourceScheme scheme : values()) {
+ if (name.startsWith(scheme.name().toLowerCase(Locale.US) + ":")) {
+ return name.substring(scheme.name().length() + 1);
+ }
+ }
+
+ return name;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitHelper.java
new file mode 100644
index 00000000000..6af102f09ca
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitHelper.java
@@ -0,0 +1,125 @@
+/*
+ * 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.k;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.v1.integrationspec.Traits;
+
+/**
+ * Utility class manages trait expressions and its conversion to proper trait model.
+ */
+public final class TraitHelper {
+
+ private TraitHelper() {
+ //prevent instantiation of utility class.
+ }
+
+ /**
+ * Parses given list of trait expressions to proper trait model object.
+ *
+ * @param traits
+ * @return
+ */
+ public static Traits parseTraits(String[] traits) {
+ try {
+ Map<String, Map<String, Object>> traitJson = new HashMap<>();
+
+ for (String traitExpression : traits) {
+ //traitName.key=value
+ final String[] trait = traitExpression.split("\\.", 2);
+ final String[] traitConfig = trait[1].split("=", 2);
+
+ final String traitKey = traitConfig[0];
+ final Object traitValue = resolveTraitValue(traitKey, traitConfig[1].trim());
+ if (traitJson.containsKey(trait[0])) {
+ Map<String, Object> config = traitJson.get(trait[0]);
+
+ if (config.containsKey(traitKey)) {
+ Object existingValue = config.get(traitKey);
+
+ if (existingValue instanceof List) {
+ List<String> values = (List<String>) existingValue;
+ values.add(traitValue.toString());
+ } else {
+ config.put(traitKey, Arrays.asList(existingValue.toString(), traitValue));
+ }
+ } else {
+ config.put(traitKey, initializeTraitValue(traitValue));
+ }
+ } else {
+ Map<String, Object> props = new HashMap<>();
+ props.put(traitKey, initializeTraitValue(traitValue));
+ traitJson.put(trait[0], props);
+ }
+ }
+
+ return KubernetesHelper.json().readerFor(Traits.class).readValue(
+ KubernetesHelper.json().writeValueAsString(traitJson));
+ } catch (IOException e) {
+ throw new RuntimeCamelException("Failed to parse trait options", e);
+ }
+ }
+
+ /**
+ * Resolve trait value with automatic type conversion. Some trait keys (like enabled, verbose) need to be converted
+ * to boolean type.
+ *
+ * @param traitKey
+ * @param value
+ * @return
+ */
+ private static Object resolveTraitValue(String traitKey, String value) {
+ if (value.startsWith("\"") && value.endsWith("\"")) {
+ return value.substring(1, value.length() - 1);
+ }
+
+ if (value.startsWith("'") && value.endsWith("'")) {
+ return value.substring(1, value.length() - 1);
+ }
+
+ if (traitKey.equalsIgnoreCase("enabled") ||
+ traitKey.equalsIgnoreCase("verbose")) {
+ return Boolean.valueOf(value);
+ }
+
+ return value;
+ }
+
+ /**
+ * Initialize trait value with support for array type values.
+ *
+ * @param value
+ * @return
+ */
+ private static Object initializeTraitValue(Object value) {
+ if (value instanceof String && value.toString().startsWith("[") && value.toString().endsWith("]")) {
+ List<String> values = new ArrayList<>();
+ values.add(resolveTraitValue("", value.toString().substring(1, value.toString().length() - 1)).toString());
+ return values;
+ }
+
+ return value;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitProfile.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitProfile.java
new file mode 100644
index 00000000000..4d5e1664b12
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/TraitProfile.java
@@ -0,0 +1,25 @@
+/*
+ * 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.k;
+
+public enum TraitProfile {
+
+ OPENSHIFT,
+ KUBERNETES,
+ KNATIVE
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
index 1ab33c625d7..49cf8195932 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
@@ -44,6 +44,7 @@ public class VersionGet extends CamelCommand {
CommandLineHelper.loadProperties(properties -> {
String uv = properties.getProperty("camel-version");
+ String kv = properties.getProperty("kamelets-version");
String repos = properties.getProperty("repos");
String runtime = properties.getProperty("runtime");
if (uv != null || repos != null || runtime != null) {
@@ -51,6 +52,9 @@ public class VersionGet extends CamelCommand {
if (uv != null) {
System.out.println(" camel-version = " + uv);
}
+ if (kv != null) {
+ System.out.println(" kamelets-version = " + uv);
+ }
if (runtime != null) {
System.out.println(" runtime = " + runtime);
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Printer.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Printer.java
new file mode 100644
index 00000000000..65cc79ee986
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Printer.java
@@ -0,0 +1,39 @@
+/*
+ * 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.common;
+
+/**
+ * Printer interface used by commands to write output to given print stream. By default, uses System out print stream,
+ * but unit tests for instance may use a different print stream.
+ */
+public interface Printer {
+
+ default void println(String line) {
+ System.out.println(line);
+ }
+
+ default void printf(String format, Object... args) {
+ System.out.printf(format, args);
+ }
+
+ /**
+ * Default printer uses System out print stream.
+ */
+ class SystemOutPrinter implements Printer {
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/StringPrinter.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/StringPrinter.java
new file mode 100644
index 00000000000..3370e0c5991
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/StringPrinter.java
@@ -0,0 +1,68 @@
+/*
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.Printer;
+
+public class StringPrinter implements Printer {
+
+ private final StringWriter writer = new StringWriter();
+
+ @Override
+ public void println(String line) {
+ writer.write(line + "\n");
+ }
+
+ @Override
+ public void printf(String format, Object... args) {
+ writer.write(format.formatted(args));
+ }
+
+ /**
+ * Provides access to the cached output.
+ *
+ * @return
+ */
+ public String getOutput() {
+ return writer.toString().trim();
+ }
+
+ /**
+ * Provides access to all lines of the cached output.
+ *
+ * @return
+ * @throws IOException
+ */
+ public List<String> getLines() throws IOException {
+ BufferedReader buf = new BufferedReader(new StringReader(getOutput()));
+ List<String> lines = new ArrayList<>();
+ String line;
+ while ((line = buf.readLine()) != null) {
+ lines.add(line.trim());
+ }
+
+ return lines;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDeleteTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDeleteTest.java
new file mode 100644
index 00000000000..2d4d8a8c1fa
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationDeleteTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.k;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class IntegrationDeleteTest extends KubeBaseTest {
+
+ @Test
+ public void shouldVerifyArguments() throws Exception {
+ Assertions.assertThrows(RuntimeCamelException.class, createCommand()::doCall,
+ "Missing integration name as argument or --all option.");
+ }
+
+ @Test
+ public void shouldDeleteIntegration() throws Exception {
+ Integration integration = createIntegration();
+ kubernetesClient.resources(Integration.class).resource(integration).create();
+
+ IntegrationDelete command = createCommand();
+ command.names = new String[] { integration.getMetadata().getName() };
+ command.doCall();
+
+ Assertions.assertEquals("Integration routes deleted", printer.getOutput());
+
+ Assertions.assertEquals(0, kubernetesClient.resources(Integration.class).list().getItems().size());
+ }
+
+ @Test
+ public void shouldHandleIntegrationNotFound() throws Exception {
+ IntegrationDelete command = createCommand();
+ command.names = new String[] { "mickey-mouse" };
+ command.doCall();
+
+ Assertions.assertEquals("Integration mickey-mouse deletion skipped - not found", printer.getOutput());
+ }
+
+ @Test
+ public void shouldDeleteAll() throws Exception {
+ Integration integration1 = createIntegration("foo");
+ Integration integration2 = createIntegration("bar");
+
+ kubernetesClient.resources(Integration.class).resource(integration1).create();
+ kubernetesClient.resources(Integration.class).resource(integration2).create();
+
+ IntegrationDelete command = createCommand();
+ command.all = true;
+ command.doCall();
+
+ Assertions.assertEquals("Integrations deleted", printer.getOutput());
+ Assertions.assertEquals(0, kubernetesClient.resources(Integration.class).list().getItems().size());
+ }
+
+ private IntegrationDelete createCommand() {
+ IntegrationDelete command = new IntegrationDelete(new CamelJBangMain().withPrinter(printer));
+ command.withClient(kubernetesClient);
+ return command;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGetTest.java
new file mode 100644
index 00000000000..5bdcbe72192
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationGetTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.k;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import org.apache.camel.v1.IntegrationStatus;
+import org.apache.camel.v1.integrationstatus.Conditions;
+import org.apache.camel.v1.integrationstatus.IntegrationKit;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class IntegrationGetTest extends KubeBaseTest {
+
+ @Test
+ public void shouldListIntegrationsEmpty() throws Exception {
+ createCommand().doCall();
+
+ Assertions.assertEquals("", printer.getOutput());
+ }
+
+ @Test
+ public void shouldListReadyIntegration() throws Exception {
+ Integration integration = createIntegration();
+
+ IntegrationStatus status = new IntegrationStatus();
+
+ IntegrationKit kit = new IntegrationKit();
+ kit.setName("kit-123456789");
+ status.setIntegrationKit(kit);
+
+ status.setPhase("Running");
+ status.setConditions(new ArrayList<>());
+ Conditions readyCondition = new Conditions();
+ readyCondition.setType("Ready");
+ readyCondition.setStatus("True");
+ status.getConditions().add(readyCondition);
+ integration.setStatus(status);
+
+ kubernetesClient.resources(Integration.class).resource(integration).create();
+
+ createCommand().doCall();
+
+ List<String> output = printer.getLines();
+ Assertions.assertEquals("NAME PHASE KIT READY", output.get(0));
+ Assertions.assertEquals("routes Running kit-123456789 1/1", output.get(1));
+ }
+
+ @Test
+ public void shouldListPendingIntegration() throws Exception {
+ Integration integration = createIntegration("building");
+ IntegrationStatus status = new IntegrationStatus();
+
+ status.setPhase("Building Kit");
+ status.setConditions(new ArrayList<>());
+ Conditions readyCondition = new Conditions();
+ readyCondition.setType("Ready");
+ readyCondition.setStatus("False");
+ status.getConditions().add(readyCondition);
+ integration.setStatus(status);
+
+ kubernetesClient.resources(Integration.class).resource(integration).create();
+
+ createCommand().doCall();
+
+ List<String> output = printer.getLines();
+ Assertions.assertEquals("NAME PHASE KIT READY", output.get(0));
+ Assertions.assertEquals("building Building Kit 0/1", output.get(1));
+ }
+
+ @Test
+ public void shouldListIntegrationNames() throws Exception {
+ Integration integration1 = createIntegration("foo");
+ Integration integration2 = createIntegration("bar");
+
+ kubernetesClient.resources(Integration.class).resource(integration1).create();
+ kubernetesClient.resources(Integration.class).resource(integration2).create();
+
+ IntegrationGet command = createCommand();
+ command.name = true;
+ command.doCall();
+
+ Assertions.assertEquals("foo\nbar", printer.getOutput());
+ }
+
+ private IntegrationGet createCommand() {
+ IntegrationGet command = new IntegrationGet(new CamelJBangMain().withPrinter(printer));
+ command.withClient(kubernetesClient);
+ return command;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogsTest.java
new file mode 100644
index 00000000000..de80a26bf29
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationLogsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.k;
+
+import java.util.Collections;
+
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodBuilder;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class IntegrationLogsTest extends KubeBaseTest {
+
+ @Test
+ public void shouldHandleIntegrationsNotFound() throws Exception {
+ IntegrationLogs command = createCommand();
+ command.name = "mickey-mouse";
+ command.doCall();
+
+ Assertions.assertEquals("Integration mickey-mouse not found", printer.getOutput());
+ }
+
+ @Test
+ public void shouldGetIntegrationLogs() throws Exception {
+ Integration integration = createIntegration();
+ kubernetesClient.resources(Integration.class).resource(integration).create();
+
+ Pod pod = new PodBuilder()
+ .withNewMetadata()
+ .withName(integration.getMetadata().getName())
+ .withLabels(Collections.singletonMap(KubeCommand.INTEGRATION_LABEL, integration.getMetadata().getName()))
+ .endMetadata()
+ .withNewStatus()
+ .withPhase("Running")
+ .endStatus()
+ .build();
+
+ kubernetesClient.pods().resource(pod).create();
+
+ IntegrationLogs command = createCommand();
+
+ command.name = "routes";
+ command.doCall();
+ }
+
+ private IntegrationLogs createCommand() {
+ IntegrationLogs command = new IntegrationLogs(new CamelJBangMain().withPrinter(printer));
+ command.withClient(kubernetesClient);
+ return command;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java
new file mode 100644
index 00000000000..9a5913900ab
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java
@@ -0,0 +1,594 @@
+/*
+ * 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.k;
+
+import java.util.regex.Pattern;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class IntegrationRunTest extends KubeBaseTest {
+
+ @Test
+ public void shouldHandleMissingSourceFile() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "mickey-mouse.groovy" };
+ Assertions.assertThrows(RuntimeCamelException.class, command::doCall, "Failed to resolve sources");
+ }
+
+ @Test
+ public void shouldRunIntegration() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.doCall();
+
+ Assertions.assertEquals("Integration route created", printer.getOutput());
+
+ Integration created = kubernetesClient.resources(Integration.class).withName("route").get();
+ Assertions.assertEquals("camel-k", created.getMetadata().getAnnotations().get(KubeCommand.OPERATOR_ID_LABEL));
+ }
+
+ @Test
+ public void shouldUpdateIntegration() throws Exception {
+ Integration integration = createIntegration("route");
+ kubernetesClient.resources(Integration.class).resource(integration).create();
+
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.doCall();
+
+ Assertions.assertEquals("Integration route updated", printer.getOutput());
+
+ Integration created = kubernetesClient.resources(Integration.class).withName("route").get();
+ Assertions.assertEquals("camel-k", created.getMetadata().getAnnotations().get(KubeCommand.OPERATOR_ID_LABEL));
+ }
+
+ @Test
+ public void shouldAddTraits() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.traits = new String[] { "logging.level=DEBUG", "container.imagePullPolicy=ALWAYS" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ container:
+ imagePullPolicy: ALWAYS
+ logging:
+ level: DEBUG""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldSpecFromOptions() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.name = "custom";
+ command.operatorId = "custom-operator";
+ command.serviceAccount = "service-account-name";
+ command.labels = new String[] { "custom-label=enabled" };
+ command.annotations = new String[] { "custom-annotation=enabled" };
+ command.repositories = new String[] { "http://custom-repository" };
+ command.profile = "knative";
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ custom-annotation: enabled
+ camel.apache.org/operator.id: custom-operator
+ labels:
+ custom-label: enabled
+ name: custom
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ profile: knative
+ repositories:
+ - http://custom-repository
+ serviceAccountName: service-account-name
+ traits: {}""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddVolumes() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.volumes = new String[] { "/foo", "/bar" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ mount:
+ volumes:
+ - /foo
+ - /bar""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddDependencies() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.dependencies = new String[] { "camel-jackson", "camel-quarkus-jms", "mvn:foo:bar:1.0" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ dependencies:
+ - camel:jackson
+ - camel:jms
+ - mvn:foo:bar:1.0
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits: {}""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddEnvVars() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.envVars = new String[] { "CAMEL_FOO=bar" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ environment:
+ vars:
+ - CAMEL_FOO=bar""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddProperties() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.properties = new String[] { "camel.foo=bar" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ camel:
+ properties:
+ - camel.foo=bar""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddBuildProperties() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.buildProperties = new String[] { "camel.foo=bar" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ builder:
+ properties:
+ - camel.foo=bar""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldUseKit() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.kit = "kit-123456789";
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ integrationKit:
+ name: kit-123456789
+ traits: {}""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddSources() throws Exception {
+ IntegrationRun command = createCommand();
+ command.sources = new String[] { "classpath:route.yaml" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits: {}""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddConnects() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.connects = new String[] { "serving.knative.dev/v1:Service:foo" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ serviceBinding:
+ services:
+ - serving.knative.dev/v1:Service:foo""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldUsePodTemplate() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.podTemplate = "classpath:pod.yaml";
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ template:
+ spec:
+ containers:
+ - env:
+ - name: TEST
+ value: TEST
+ name: integration
+ volumeMounts:
+ - mountPath: /var/log
+ name: var-logs
+ volumes:
+ - emptyDir: {}
+ name: var-logs
+ traits: {}""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddConfigs() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.configs = new String[] { "secret:foo", "configmap:bar" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ mount:
+ configs:
+ - secret:foo
+ - configmap:bar""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddResources() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.resources = new String[] { "configmap:foo/file.txt" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ mount:
+ resources:
+ - configmap:foo/file.txt""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldAddOpenApis() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.openApis = new String[] { "configmap:openapi/spec.yaml" };
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ openapi:
+ configmaps:
+ - configmap:openapi/spec.yaml""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldUseImage() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.image = "quay.io/camel/demo-app:1.0";
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: demo-app-v1
+ spec:
+ traits:
+ container:
+ image: quay.io/camel/demo-app:1.0""", printer.getOutput());
+ }
+
+ @Test
+ public void shouldUseCompression() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.compression = true;
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals(
+ """
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ sources:
+ - compression: true
+ content: ZFNNb6NADL3zK5zk0kr5WO2RPbFpoqKtiBRoqxwnYMAqzLAzZmn+/XoI2UbauSDP2M/vPZtFsIAXylE7LIANcI0QdSqXT2pKHpRF2JteF4rJaHiI0v0jSIgWjEYwFlpjUUByo9nSuWe5aq6AoCqL2KJmtwZIEUf05JDF2x2U1CAU5K5F0nwgrgWHa3IwGPsBpSCpoiDfWDVAWi7aKw2LlbIF6UradhdLVc1gBo3W1dStBSXzMtL9jYm7wo49ReTJ9JOGO7mTC0t4Exjf5Pv6myA9+JT59Dh//AEXKW7VBbRh6B3eIeNnjh0LUWHVdg0pneOXrH8dxIvThGHOrCRdjTLAlPdpoDhYSOV4auYu3GyGYVirke7a2GpzU7d5EUeTdLcaKUvNq27QObHpd09WvD1fQHXCKFdn4dmowQ9unM44dKEwWPFZV0tw09QF5X46X3bd6In [...]
+ language: yaml
+ name: route.yaml
+ traits: {}""",
+ removeLicenseHeader(printer.getOutput()));
+ }
+
+ private final Pattern comments = Pattern.compile("^\\s*#.*$", Pattern.MULTILINE);
+ private final Pattern emptyLine = Pattern.compile("^[\\r?\\n]$", Pattern.MULTILINE);
+
+ private String removeLicenseHeader(String yaml) {
+ return emptyLine.matcher(comments.matcher(yaml).replaceAll("")).replaceAll("");
+ }
+
+ @Test
+ public void shouldHandleUseFlowsDisabledOption() throws Exception {
+ IntegrationRun command = createCommand();
+ command.filePaths = new String[] { "classpath:route.yaml" };
+ command.useFlows = false;
+ command.output = "yaml";
+ command.doCall();
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ sources:
+ - compression: false
+ content: |
+
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ language: yaml
+ name: route.yaml
+ traits: {}""", removeLicenseHeader(printer.getOutput()));
+ }
+
+ private IntegrationRun createCommand() {
+ IntegrationRun command = new IntegrationRun(new CamelJBangMain().withPrinter(printer));
+ command.withClient(kubernetesClient);
+ return command;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseTest.java
new file mode 100644
index 00000000000..86856f6a31c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeBaseTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.k;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.server.mock.KubernetesCrudDispatcher;
+import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
+import io.fabric8.mockwebserver.Context;
+import okhttp3.mockwebserver.MockWebServer;
+import org.apache.camel.dsl.jbang.core.commands.StringPrinter;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.v1.Integration;
+import org.apache.camel.v1.IntegrationSpec;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInstance;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class KubeBaseTest {
+
+ protected static Integration integration;
+
+ private KubernetesMockServer k8sServer;
+
+ protected KubernetesClient kubernetesClient;
+
+ protected StringPrinter printer;
+
+ @BeforeAll
+ public void setupFixtures() {
+ k8sServer = new KubernetesMockServer(
+ new Context(), new MockWebServer(),
+ new HashMap<>(), new KubernetesCrudDispatcher(), false);
+
+ kubernetesClient = k8sServer.createClient();
+ }
+
+ @BeforeEach
+ public void setup() {
+ printer = new StringPrinter();
+ k8sServer.reset();
+ }
+
+ @AfterAll
+ public void cleanup() {
+ k8sServer.destroy();
+ }
+
+ protected Integration createIntegration() throws IOException {
+ return createIntegration("routes");
+ }
+
+ protected Integration createIntegration(String name) throws IOException {
+ if (integration == null) {
+ integration = KubernetesHelper.yaml().loadAs(
+ IOHelper.loadText(KubeBaseTest.class.getResourceAsStream("integration.yaml")), Integration.class);
+ }
+
+ Integration created = new Integration();
+ created.getMetadata().setName(name);
+ created.setSpec(new IntegrationSpec());
+ created.getSpec().setTraits(integration.getSpec().getTraits());
+ created.getSpec().setFlows(integration.getSpec().getFlows());
+
+ return created;
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommandMainTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommandMainTest.java
new file mode 100644
index 00000000000..8333dc0b278
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/KubeCommandMainTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.k;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodBuilder;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.v1.Integration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class KubeCommandMainTest extends KubeBaseTest {
+
+ @Test
+ public void shouldDeleteIntegration() throws IOException {
+ KubernetesHelper.setKubernetesClient(kubernetesClient);
+ CamelJBangMain.run(createMain(), "k", "delete", "--all");
+
+ Assertions.assertEquals("Integrations deleted", printer.getOutput());
+ }
+
+ @Test
+ public void shouldListIntegration() throws IOException {
+ KubernetesHelper.setKubernetesClient(kubernetesClient);
+
+ kubernetesClient.resources(Integration.class).resource(createIntegration()).create();
+
+ CamelJBangMain.run(createMain(), "k", "get");
+
+ List<String> output = printer.getLines();
+ Assertions.assertEquals("NAME PHASE KIT READY", output.get(0));
+ Assertions.assertEquals("routes Unknown 0/1", output.get(1));
+ }
+
+ @Test
+ public void shouldPrintIntegrationLogs() throws IOException {
+ KubernetesHelper.setKubernetesClient(kubernetesClient);
+
+ kubernetesClient.resources(Integration.class).resource(createIntegration()).create();
+
+ Pod pod = new PodBuilder()
+ .withNewMetadata()
+ .withName(integration.getMetadata().getName())
+ .withLabels(Collections.singletonMap(KubeCommand.INTEGRATION_LABEL, integration.getMetadata().getName()))
+ .endMetadata()
+ .withNewStatus()
+ .withPhase("Running")
+ .endStatus()
+ .build();
+
+ kubernetesClient.pods().resource(pod).create();
+
+ CamelJBangMain.run(createMain(), "k", "logs", "routes");
+ }
+
+ @Test
+ public void shouldRunIntegration() {
+ KubernetesHelper.setKubernetesClient(kubernetesClient);
+ CamelJBangMain.run(createMain(), "k", "run", "classpath:route.yaml");
+
+ Integration integration = kubernetesClient.resources(Integration.class).withName("route").get();
+ Assertions.assertNotNull(integration);
+ Assertions.assertEquals(integration.getMetadata().getAnnotations().get(KubeCommand.OPERATOR_ID_LABEL), "camel-k");
+ }
+
+ @Test
+ public void shouldPrintIntegration() {
+ CamelJBangMain.run(createMain(), "k", "run", "classpath:route.yaml", "-o", "yaml");
+
+ Assertions.assertEquals("""
+ apiVersion: camel.apache.org/v1
+ kind: Integration
+ metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: route
+ spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits: {}""", printer.getOutput());
+ }
+
+ private CamelJBangMain createMain() {
+ return new CamelJBangMain() {
+ @Override
+ protected void quit(int exitCode) {
+ if (exitCode != 0) {
+ Assertions.fail("Main finished with exit code %d".formatted(exitCode));
+ }
+ }
+ }.withPrinter(printer);
+ }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/k/integration.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/k/integration.yaml
new file mode 100644
index 00000000000..6b85aa4c7a5
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/k/integration.yaml
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ name: routes
+spec:
+ flows:
+ - additionalProperties:
+ from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
+ traits:
+ logging:
+ level: DEBUG
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/pod.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/pod.yaml
new file mode 100644
index 00000000000..5c62a01c52b
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/pod.yaml
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+
+containers:
+ - name: integration
+ env:
+ - name: TEST
+ value: TEST
+ volumeMounts:
+ - name: var-logs
+ mountPath: /var/log
+volumes:
+ - name: var-logs
+ emptyDir: { }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/route.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/route.yaml
new file mode 100644
index 00000000000..4485c1f5379
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/route.yaml
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+
+from:
+ uri: timer:tick
+ steps:
+ - set-body:
+ constant: Hello Camel !!!
+ - to: log:info
diff --git a/parent/pom.xml b/parent/pom.xml
index b6cd62eb182..718da83cd06 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -82,6 +82,7 @@
<californium-version>2.8.0</californium-version>
<californium-scandium-version>2.8.0</californium-scandium-version>
<camunda-version>7.20.0</camunda-version>
+ <camel-k-version>2.1.0</camel-k-version>
<cassandra-driver-version>4.17.0</cassandra-driver-version>
<jta-api-1.2-version>1.2</jta-api-1.2-version>
<cglib-version>3.3.0</cglib-version>