You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2024/03/23 06:32:32 UTC

(camel) branch main updated: CAMEL-20606: Add Camel K bind command (#13595)

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

davsclaus 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 eb36ce03bf1 CAMEL-20606: Add Camel K bind command (#13595)
eb36ce03bf1 is described below

commit eb36ce03bf1d19b447133216d8d64eb33e16734e
Author: Christoph Deppisch <cd...@redhat.com>
AuthorDate: Sat Mar 23 07:32:26 2024 +0100

    CAMEL-20606: Add Camel K bind command (#13595)
    
    - Enhance Camel JBang bind command with Kubernetes and Camel K specifics such as traits, annotations, service bindings
    - Uses arbitrary bind command as a base
    - Makes sure to use a proper Camel K operator id
---
 .../apache/camel/dsl/jbang/core/commands/Bind.java |  90 +++++++--
 .../camel/dsl/jbang/core/commands/k/Bind.java      | 184 ++++++++++++++++++
 .../dsl/jbang/core/commands/k/IntegrationRun.java  |  17 +-
 .../dsl/jbang/core/commands/k/KubePlugin.java      |   1 +
 .../templates/pipe-kamelet-kamelet.yaml.tmpl       |  21 ++
 .../resources/templates/pipe-kamelet-uri.yaml.tmpl |  18 ++
 .../resources/templates/pipe-uri-kamelet.yaml.tmpl |  18 ++
 .../resources/templates/pipe-uri-uri.yaml.tmpl     |  15 ++
 .../camel/dsl/jbang/core/commands/k/BindTest.java  | 215 +++++++++++++++++++++
 .../jbang/core/commands/k/IntegrationRunTest.java  |  14 ++
 10 files changed, 576 insertions(+), 17 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
index c5bc9e3809c..3a005645342 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
@@ -84,12 +84,50 @@ public class Bind extends CamelCommand {
                         description = "Output format generated by this command (supports: file, yaml or json).")
     String output;
 
+    private final TemplateProvider templateProvider;
+
     public Bind(CamelJBangMain main) {
+        this(main, new TemplateProvider() {
+        });
+    }
+
+    public Bind(CamelJBangMain main, TemplateProvider templateProvider) {
         super(main);
+        this.templateProvider = templateProvider;
+    }
+
+    /**
+     * Helper class provides access to the templates that construct the Pipe resource. Subclasses may overwrite the
+     * provider to inject their own templates.
+     */
+    public interface TemplateProvider {
+        default InputStream getPipeTemplate(String in, String out) {
+            return Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
+        }
+
+        default InputStream getStepTemplate(String stepType) {
+            return Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
+        }
+
+        default InputStream getErrorHandlerTemplate(String type) {
+            return Bind.class.getClassLoader()
+                    .getResourceAsStream("templates/error-handler-%s.yaml.tmpl".formatted(type));
+        }
     }
 
     @Override
     public Integer doCall() throws Exception {
+        String pipe = constructPipe();
+
+        if (pipe.isEmpty()) {
+            printer().println("Failed to construct Pipe resource");
+            return -1;
+        }
+
+        return dumpPipe(pipe);
+    }
+
+    public String constructPipe() throws Exception {
         // the pipe source and sink can either be a kamelet or an uri
         String in = "kamelet";
         String out = "kamelet";
@@ -119,7 +157,7 @@ public class Bind extends CamelCommand {
             }
         }
 
-        InputStream is = Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
+        InputStream is = templateProvider.getPipeTemplate(in, out);
         String context = IOHelper.loadText(is);
         IOHelper.close(is);
 
@@ -146,7 +184,7 @@ public class Bind extends CamelCommand {
                     stepProperties = kameletProperties(step, stepProperties);
                 }
 
-                is = Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
+                is = templateProvider.getStepTemplate(stepType);
                 text = IOHelper.loadText(is);
                 IOHelper.close(is);
                 text = text.replaceFirst("\\{\\{ \\.Name }}", step);
@@ -176,7 +214,8 @@ public class Bind extends CamelCommand {
                     if (errorHandlerTokens.length != 2) {
                         printer().println(
                                 "Invalid error handler syntax. Type 'sink' needs an endpoint configuration (ie sink:endpointUri)");
-                        return -1;
+                        // Error abort Pipe construction
+                        return "";
                     }
                     String endpoint = errorHandlerTokens[1];
 
@@ -203,8 +242,7 @@ public class Bind extends CamelCommand {
                         errorHandlerSinkProperties = kameletProperties(endpoint, errorHandlerSinkProperties);
                     }
 
-                    is = Bind.class.getClassLoader()
-                            .getResourceAsStream("templates/error-handler-sink-%s.yaml.tmpl".formatted(sinkType));
+                    is = templateProvider.getErrorHandlerTemplate("sink-" + sinkType);
                     errorHandlerSpec = IOHelper.loadText(is);
                     IOHelper.close(is);
                     errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.Name }}", endpoint);
@@ -214,7 +252,7 @@ public class Bind extends CamelCommand {
                             asErrorHandlerParameters(errorHandlerParameters));
                     break;
                 case "log":
-                    is = Bind.class.getClassLoader().getResourceAsStream("templates/error-handler-log.yaml.tmpl");
+                    is = templateProvider.getErrorHandlerTemplate("log");
                     errorHandlerSpec = IOHelper.loadText(is);
                     IOHelper.close(is);
                     errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}",
@@ -249,23 +287,26 @@ public class Bind extends CamelCommand {
             sinkProperties.putAll(sinkUriProperties);
         }
         context = context.replaceFirst("\\{\\{ \\.SinkProperties }}\n", asEndpointProperties(sinkProperties));
+        return context;
+    }
 
+    public int dumpPipe(String pipe) throws Exception {
         switch (output) {
             case "file":
                 if (file.endsWith(".yaml")) {
-                    IOHelper.writeText(context, new FileOutputStream(file, false));
+                    IOHelper.writeText(pipe, new FileOutputStream(file, false));
                 } else if (file.endsWith(".json")) {
-                    IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)),
+                    IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)),
                             new FileOutputStream(file, false));
                 } else {
-                    IOHelper.writeText(context, new FileOutputStream(file + ".yaml", false));
+                    IOHelper.writeText(pipe, new FileOutputStream(file + ".yaml", false));
                 }
                 break;
             case "yaml":
-                printer().println(context);
+                printer().println(pipe);
                 break;
             case "json":
-                printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)), 2)
+                printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)), 2)
                         .replaceAll("\\\\/", "/"));
                 break;
             default:
@@ -442,4 +483,31 @@ public class Bind extends CamelCommand {
         }
     }
 
+    public void setFile(String file) {
+        this.file = file;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public void setSink(String sink) {
+        this.sink = sink;
+    }
+
+    public void setSteps(String[] steps) {
+        this.steps = steps;
+    }
+
+    public void setProperties(String[] properties) {
+        this.properties = properties;
+    }
+
+    public void setErrorHandler(String errorHandler) {
+        this.errorHandler = errorHandler;
+    }
+
+    public void setOutput(String output) {
+        this.output = output;
+    }
 }
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Bind.java b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Bind.java
new file mode 100644
index 00000000000..c536045c68f
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Bind.java
@@ -0,0 +1,184 @@
+/*
+ * 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.InputStream;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.v1.integrationspec.Traits;
+import org.apache.camel.v1.integrationspec.traits.ServiceBinding;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "bind",
+         description = "Bind Kubernetes resources such as Kamelets in a new integration pipe connecting a source and a sink",
+         sortOptions = false)
+public class Bind extends KubeBaseCommand {
+
+    private final org.apache.camel.dsl.jbang.core.commands.Bind delegate;
+
+    @CommandLine.Parameters(description = "Name of pipe", arity = "1",
+                            paramLabel = "<name>", parameterConsumer = NameConsumer.class)
+    Path pipeName; // Defined only for code completion; the field never used
+    String name;
+
+    @CommandLine.Option(names = { "--source" }, description = "Source (from) such as a Kamelet or Camel endpoint uri",
+                        required = true)
+    String source;
+
+    @CommandLine.Option(names = { "--step" }, description = "Optional steps such as a Kamelet or Camel endpoint uri")
+    String[] steps;
+
+    @CommandLine.Option(names = { "--sink" }, description = "Sink (to) such as a Kamelet or Camel endpoint uri",
+                        required = true)
+    String sink;
+
+    @CommandLine.Option(names = { "--error-handler" },
+                        description = "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format \"[[apigroup/]version:]kind:[namespace/]name\", plain Camel URIs or Kamelet name.")
+    String errorHandler;
+
+    @CommandLine.Option(names = { "--property" },
+                        description = "Adds a pipe property in the form of [source|sink|step-<n>].<key>=<value> where <n> is the step number starting from 1",
+                        arity = "0")
+    String[] properties;
+
+    @CommandLine.Option(names = { "--output" },
+                        defaultValue = "file",
+                        description = "Output format generated by this command (supports: file, yaml or json).")
+    String output;
+
+    @CommandLine.Option(names = { "--operator-id" },
+                        defaultValue = "camel-k",
+                        description = "Operator id selected to manage this integration.")
+    String operatorId = "camel-k";
+
+    @CommandLine.Option(names = { "--connect" },
+                        description = "A Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name.")
+    String[] connects;
+
+    @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 = { "--traits" },
+                        description = "Add a label to the integration. Use name values pairs like \"--label my.company=hello\".")
+    String[] traits;
+
+    public Bind(CamelJBangMain main) {
+        super(main);
+        delegate = new org.apache.camel.dsl.jbang.core.commands.Bind(
+                main, new org.apache.camel.dsl.jbang.core.commands.Bind.TemplateProvider() {
+                    @Override
+                    public InputStream getPipeTemplate(String in, String out) {
+                        return Bind.class.getClassLoader()
+                                .getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
+                    }
+                });
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Operator id must be set
+        if (ObjectHelper.isEmpty(operatorId)) {
+            printer().println("Operator id must be set");
+            return -1;
+        }
+
+        delegate.setFile(name);
+        delegate.setSource(source);
+        delegate.setSink(sink);
+        delegate.setSteps(steps);
+        delegate.setErrorHandler(errorHandler);
+        delegate.setProperties(properties);
+        delegate.setOutput(output);
+
+        String pipe = delegate.constructPipe();
+
+        if (pipe.isEmpty()) {
+            // Error in delegate exit now
+            printer().println("Failed to construct Pipe resource");
+            return -1;
+        }
+
+        // --operator-id={id} is a syntax sugar for '--annotation camel.apache.org/operator.id={id}'
+        if (annotations == null) {
+            annotations = new String[] { "%s=%s".formatted(KubeCommand.OPERATOR_ID_LABEL, operatorId) };
+        } else {
+            annotations = Arrays.copyOf(annotations, annotations.length + 1);
+            annotations[annotations.length - 1] = "%s=%s".formatted(KubeCommand.OPERATOR_ID_LABEL, operatorId);
+        }
+
+        String annotationsContext = "";
+        if (annotations != null) {
+            StringBuilder sb = new StringBuilder("  annotations:\n");
+
+            for (String annotation : annotations) {
+                String[] keyValue = annotation.split("=", 2);
+                if (keyValue.length != 2) {
+                    printer().printf(
+                            "annotation '%s' does not follow format <key>=<value>%n",
+                            annotation);
+                    continue;
+                }
+
+                sb.append("    ").append(keyValue[0]).append(": ").append(keyValue[1]).append("\n");
+            }
+
+            annotationsContext = sb.toString();
+        }
+
+        pipe = pipe.replaceFirst("\\{\\{ \\.Annotations }}\n", annotationsContext);
+
+        String integrationSpec = "";
+        Traits traitsSpec = null;
+        if (traits != null && traits.length > 0) {
+            traitsSpec = TraitHelper.parseTraits(traits);
+        }
+
+        if (connects != null && connects.length > 0) {
+            if (traitsSpec == null) {
+                traitsSpec = new Traits();
+            }
+
+            ServiceBinding serviceBindingTrait = new ServiceBinding();
+            serviceBindingTrait.setServices(List.of(connects));
+            traitsSpec.setServiceBinding(serviceBindingTrait);
+        }
+
+        if (traitsSpec != null) {
+            String traitYaml = KubernetesHelper.yaml().dumpAsMap(traitsSpec).replaceAll("\n", "\n        ");
+            integrationSpec = "  integration:\n    spec:\n      traits:\n        %s\n".formatted(traitYaml.trim());
+        }
+
+        pipe = pipe.replaceFirst("\\{\\{ \\.IntegrationSpec }}\n", integrationSpec);
+
+        return delegate.dumpPipe(pipe);
+    }
+
+    static class NameConsumer extends ParameterConsumer<Bind> {
+        @Override
+        protected void doConsumeParameters(Stack<String> args, Bind cmd) {
+            cmd.name = args.pop();
+        }
+    }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java
index 6d5b5a267ca..a435ad67b14 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRun.java
@@ -173,6 +173,12 @@ public class IntegrationRun extends KubeBaseCommand {
     }
 
     public Integer doCall() throws Exception {
+        // Operator id must be set
+        if (ObjectHelper.isEmpty(operatorId)) {
+            printer().println("Operator id must be set");
+            return -1;
+        }
+
         List<String> integrationSources
                 = Stream.concat(Arrays.stream(Optional.ofNullable(filePaths).orElseGet(() -> new String[] {})),
                         Arrays.stream(Optional.ofNullable(sources).orElseGet(() -> new String[] {}))).toList();
@@ -216,14 +222,13 @@ public class IntegrationRun extends KubeBaseCommand {
                     .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 (integration.getMetadata().getAnnotations() == null) {
+            integration.getMetadata().setAnnotations(new HashMap<>());
         }
 
+        // --operator-id={id} is a syntax sugar for '--annotation camel.apache.org/operator.id={id}'
+        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("="))
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubePlugin.java b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubePlugin.java
index 6da3e3fa6da..7d10cac8e09 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubePlugin.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubePlugin.java
@@ -31,6 +31,7 @@ public class KubePlugin implements Plugin {
                 .addSubcommand(Agent.ID, new picocli.CommandLine(new Agent(main)))
                 .addSubcommand("get", new picocli.CommandLine(new IntegrationGet(main)))
                 .addSubcommand("run", new picocli.CommandLine(new IntegrationRun(main)))
+                .addSubcommand("bind", new picocli.CommandLine(new Bind(main)))
                 .addSubcommand("delete", new picocli.CommandLine(new IntegrationDelete(main)))
                 .addSubcommand("logs", new picocli.CommandLine(new IntegrationLogs(main)));
 
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl
new file mode 100644
index 00000000000..400ae37bedf
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl
@@ -0,0 +1,21 @@
+apiVersion: camel.apache.org/v1
+kind: Pipe
+metadata:
+  name: {{ .Name }}
+{{ .Annotations }}
+spec:
+{{ .IntegrationSpec }}
+  source:
+    ref:
+      kind: Kamelet
+      apiVersion: camel.apache.org/v1
+      name: {{ .Source }}
+    {{ .SourceProperties }}
+{{ .Steps }}
+  sink:
+    ref:
+      kind: Kamelet
+      apiVersion: camel.apache.org/v1
+      name: {{ .Sink }}
+    {{ .SinkProperties }}
+{{ .ErrorHandler }}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
new file mode 100644
index 00000000000..1ac52571ece
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
@@ -0,0 +1,18 @@
+apiVersion: camel.apache.org/v1
+kind: Pipe
+metadata:
+  name: {{ .Name }}
+{{ .Annotations }}
+spec:
+{{ .IntegrationSpec }}
+  source:
+    ref:
+      kind: Kamelet
+      apiVersion: camel.apache.org/v1
+      name: {{ .Source }}
+    {{ .SourceProperties }}
+{{ .Steps }}
+  sink:
+    uri: {{ .Sink }}
+    {{ .SinkProperties }}
+{{ .ErrorHandler }}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
new file mode 100644
index 00000000000..e6c33789d94
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
@@ -0,0 +1,18 @@
+apiVersion: camel.apache.org/v1
+kind: Pipe
+metadata:
+  name: {{ .Name }}
+{{ .Annotations }}
+spec:
+{{ .IntegrationSpec }}
+  source:
+    uri: {{ .Source }}
+    {{ .SourceProperties }}
+{{ .Steps }}
+  sink:
+    ref:
+      kind: Kamelet
+      apiVersion: camel.apache.org/v1
+      name: {{ .Sink }}
+    {{ .SinkProperties }}
+{{ .ErrorHandler }}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
new file mode 100644
index 00000000000..2a181b6ece5
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
@@ -0,0 +1,15 @@
+apiVersion: camel.apache.org/v1
+kind: Pipe
+metadata:
+  name: {{ .Name }}
+{{ .Annotations }}
+spec:
+{{ .IntegrationSpec }}
+  source:
+    uri: {{ .Source }}
+    {{ .SourceProperties }}
+{{ .Steps }}
+  sink:
+    uri: {{ .Sink }}
+    {{ .SinkProperties }}
+{{ .ErrorHandler }}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/BindTest.java b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/BindTest.java
new file mode 100644
index 00000000000..9809bf44ee7
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/BindTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class BindTest extends KubeBaseTest {
+
+    @Test
+    public void shouldBindWithDefaultOperatorId() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.name = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                  annotations:
+                    camel.apache.org/operator.id: camel-k
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithAnnotations() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.name = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.annotations = new String[] {
+                "app=camel-k"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                  annotations:
+                    app: camel-k
+                    camel.apache.org/operator.id: camel-k
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithTraits() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.name = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.traits = new String[] {
+                "mount.configs=configmap:my-cm",
+                "logging.color=true",
+                "logging.level=DEBUG"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                  annotations:
+                    camel.apache.org/operator.id: camel-k
+                spec:
+                  integration:
+                    spec:
+                      traits:
+                        logging:
+                          color: true
+                          level: DEBUG
+                        mount:
+                          configs:
+                          - configmap:my-cm
+                          hotReload: false
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithServiceBindings() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.name = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.connects = new String[] {
+                "serving.knative.dev/v1:Service:my-service"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                  annotations:
+                    camel.apache.org/operator.id: camel-k
+                spec:
+                  integration:
+                    spec:
+                      traits:
+                        serviceBinding:
+                          services:
+                          - serving.knative.dev/v1:Service:my-service
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldFailWithMissingOperatorId() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.name = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.operatorId = "";
+
+        Assertions.assertEquals(-1, command.doCall());
+
+        Assertions.assertEquals("Operator id must be set", printer.getOutput());
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java
index 4dba66a0487..0a6a0e3df7c 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/IntegrationRunTest.java
@@ -642,6 +642,20 @@ class IntegrationRunTest extends KubeBaseTest {
                   traits: {}""", removeLicenseHeader(printer.getOutput()));
     }
 
+    @Test
+    public void shouldFailWithMissingOperatorId() throws Exception {
+        IntegrationRun command = createCommand();
+        command.filePaths = new String[] { "classpath:route.yaml" };
+        command.useFlows = false;
+        command.output = "yaml";
+
+        command.operatorId = "";
+
+        Assertions.assertEquals(-1, command.doCall());
+
+        Assertions.assertEquals("Operator id must be set", printer.getOutput());
+    }
+
     private IntegrationRun createCommand() {
         IntegrationRun command = new IntegrationRun(new CamelJBangMain().withPrinter(printer));
         command.withClient(kubernetesClient);