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

lburgazzoli 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 f00ee7dd915 feat(jbang): camel-k agent sub-command
f00ee7dd915 is described below

commit f00ee7dd91566fa59c4673f37e6fb1809e149316
Author: Luca Burgazzoli <lburgazz...@gmail.com>
AuthorDate: Tue Mar 5 15:56:26 2024 +0100

    feat(jbang): camel-k agent sub-command
---
 dsl/camel-jbang/camel-jbang-plugin-k/pom.xml       |   6 +
 .../camel/dsl/jbang/core/commands/k/Agent.java     | 424 +++++++++++++++++++++
 .../dsl/jbang/core/commands/k/KubePlugin.java      |   7 +-
 .../jbang/core/commands/k/support/Capability.java  |  52 +++
 .../jbang/core/commands/k/support/RuntimeType.java |  38 ++
 .../commands/k/support/RuntimeTypeConverter.java   |  26 ++
 .../core/commands/k/support/SourceMetadata.java    |  60 +++
 .../commands/k/support/StubComponentResolver.java  |  71 ++++
 .../commands/k/support/StubDataFormatResolver.java |  61 +++
 .../commands/k/support/StubLanguageResolver.java   |  60 +++
 .../k/support/StubTransformerResolver.java         |  61 +++
 .../camel/dsl/jbang/core/commands/k/AgentTest.java | 167 ++++++++
 .../src/test/resources/route-i.yaml                |  41 ++
 13 files changed, 1072 insertions(+), 2 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-k/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-k/pom.xml
index 1036a344e39..a843ff29a32 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-k/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/pom.xml
@@ -72,6 +72,12 @@
             <version>${kubernetes-client-version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.rest-assured</groupId>
+            <artifactId>rest-assured</artifactId>
+            <version>${rest-assured-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Agent.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Agent.java
new file mode 100644
index 00000000000..6111fa741a9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/Agent.java
@@ -0,0 +1,424 @@
+/*
+ * 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.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.http.HttpServer;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.BodyHandler;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.Route;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.component.kamelet.KameletEndpoint;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.k.support.Capability;
+import org.apache.camel.dsl.jbang.core.commands.k.support.RuntimeType;
+import org.apache.camel.dsl.jbang.core.commands.k.support.RuntimeTypeConverter;
+import org.apache.camel.dsl.jbang.core.commands.k.support.SourceMetadata;
+import 
org.apache.camel.dsl.jbang.core.commands.k.support.StubComponentResolver;
+import 
org.apache.camel.dsl.jbang.core.commands.k.support.StubDataFormatResolver;
+import org.apache.camel.dsl.jbang.core.commands.k.support.StubLanguageResolver;
+import 
org.apache.camel.dsl.jbang.core.commands.k.support.StubTransformerResolver;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.DefaultModelReifierFactory;
+import org.apache.camel.model.CircuitBreakerDefinition;
+import org.apache.camel.model.FromDefinition;
+import org.apache.camel.model.Model;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.reifier.DisabledReifier;
+import org.apache.camel.reifier.ProcessorReifier;
+import org.apache.camel.spi.ComponentResolver;
+import org.apache.camel.spi.DataFormatResolver;
+import org.apache.camel.spi.LanguageResolver;
+import org.apache.camel.spi.TransformerResolver;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.tooling.model.BaseModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EntityRef;
+import org.apache.camel.tooling.model.Kind;
+import org.apache.camel.tooling.model.LanguageModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = Agent.ID,
+                     description = "Start a Camel K agent service that exposes 
functionalities to inspect routes and interact with a Camel Catalog",
+                     sortOptions = false)
+public class Agent extends CamelCommand {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Agent.class);
+
+    private static final ObjectMapper MAPPER = new ObjectMapper()
+            .setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+
+    private static final Map<String, BiConsumer<CamelCatalog, SourceMetadata>> 
COMPONENT_CUSTOMIZERS;
+
+    public static final String ID = "agent";
+    public static final String STUB_PATTERN = "*";
+    public static final String MIME_TYPE_JSON = "application/json";
+    public static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+    static {
+        // the core CircuitBreaker reifier fails as it depends on a specific 
implementation provided by
+        // a dedicated component/module, but as we are orly inspecting the 
route to determine its capabilities,
+        // we can safely disable the EIP with a disabled/stub reifier.
+        ProcessorReifier.registerReifier(CircuitBreakerDefinition.class, 
DisabledReifier::new);
+
+        COMPONENT_CUSTOMIZERS = new HashMap<>();
+        COMPONENT_CUSTOMIZERS.put("platform-http", (catalog, meta) -> {
+            meta.capabilities.put(
+                    Capability.PlatformHttp,
+                    catalog
+                            
.findCapabilityRef(Capability.PlatformHttp.getValue())
+                            .orElseGet(() -> new EntityRef(null, null)));
+        });
+
+    }
+
+    @CommandLine.Option(names = { "--listen-host" },
+                        description = "The host to listen on")
+    String host = "localhost";
+
+    @CommandLine.Option(names = { "--listen-port" },
+                        description = "The port to listen on")
+    int port = 8081;
+
+    @CommandLine.Option(names = { "--runtime-version" },
+                        description = "To use a different runtime version than 
the default version")
+    String runtimeVersion;
+
+    @CommandLine.Option(names = { "--runtime" },
+                        converter = RuntimeTypeConverter.class,
+                        description = "Runtime (spring-boot, quarkus, 
camel-main)")
+    RuntimeType runtimeType = RuntimeType.camelMain;
+
+    @CommandLine.Option(names = { "--repos" },
+                        description = "Comma separated list of additional 
maven repositories")
+    String repos;
+
+    public Agent(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        Vertx vertx = null;
+        HttpServer server = null;
+
+        try {
+            CountDownLatch latch = new CountDownLatch(1);
+
+            vertx = Vertx.vertx();
+            server = serve(vertx).toCompletableFuture().get();
+
+            latch.await();
+        } finally {
+            if (server != null) {
+                server.close();
+            }
+            if (vertx != null) {
+                vertx.close();
+            }
+        }
+
+        return 0;
+    }
+
+    // Visible for testing
+    CompletionStage<HttpServer> serve(Vertx vertx) {
+        HttpServer server = vertx.createHttpServer();
+        Router router = Router.router(vertx);
+
+        router.route()
+                .handler(BodyHandler.create())
+                .failureHandler(this::handleFailure);
+
+        router.route(HttpMethod.GET, "/catalog/model/:kind/:name")
+                .produces(MIME_TYPE_JSON)
+                .blockingHandler(this::handleCatalogModel);
+
+        router.route(HttpMethod.GET, "/catalog/capability/:name")
+                .produces(MIME_TYPE_JSON)
+                .blockingHandler(this::handleCatalogCapability);
+
+        router.route(HttpMethod.POST, "/inspect/:location")
+                .produces(MIME_TYPE_JSON)
+                .blockingHandler(this::handleInspect);
+
+        return server.requestHandler(router).listen(port, 
host).toCompletionStage();
+    }
+
+    private void handleFailure(RoutingContext ctx) {
+        LOGGER.warn("", ctx.failure());
+
+        ctx.response()
+                .setStatusCode(500)
+                .setStatusMessage(
+                        ctx.failure().getCause() != null
+                                ? ctx.failure().getCause().getMessage()
+                                : ctx.failure().getMessage())
+                .end();
+    }
+
+    private void handleCatalogCapability(RoutingContext ctx) {
+        try {
+            final CamelCatalog catalog = loadCatalog(runtimeType, 
runtimeVersion);
+            final String name = ctx.pathParam("name");
+            final Optional<EntityRef> ref = catalog.findCapabilityRef(name);
+
+            if (ref.isPresent()) {
+                ctx.response()
+                        .putHeader(CONTENT_TYPE_HEADER, MIME_TYPE_JSON)
+                        
.end(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(ref.get()));
+            } else {
+                ctx.response()
+                        .setStatusCode(204)
+                        .putHeader(CONTENT_TYPE_HEADER, MIME_TYPE_JSON)
+                        
.end(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(Map.of()));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void handleCatalogModel(RoutingContext ctx) {
+        try {
+            final CamelCatalog catalog = loadCatalog(runtimeType, 
runtimeVersion);
+            final String kind = ctx.pathParam("kind");
+            final String name = ctx.pathParam("name");
+            final BaseModel<?> model = catalog.model(Kind.valueOf(kind), name);
+
+            if (model != null) {
+                ctx.response()
+                        .putHeader(CONTENT_TYPE_HEADER, MIME_TYPE_JSON)
+                        
.end(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(model));
+            } else {
+                ctx.response()
+                        .setStatusCode(204)
+                        .putHeader(CONTENT_TYPE_HEADER, MIME_TYPE_JSON)
+                        
.end(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(Map.of()));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void handleInspect(RoutingContext ctx) {
+        final String content = ctx.body().asString();
+        List<String> rt = ctx.queryParam("runtimeType");
+        List<String> rv = ctx.queryParam("runtimeVersion");
+        List<String> capabilities = ctx.queryParam("capabilities");
+
+        try (CamelContext context = createCamelContext()) {
+            final Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+            final String name = ctx.pathParam("location");
+            final CamelCatalog catalog = loadCatalog(runtimeType, 
runtimeVersion);
+
+            PluginHelper.getRoutesLoader(context).loadRoutes(
+                    ResourceHelper.fromString(name, content));
+
+            context.start();
+
+            final Set<String> fromEndpoints = 
model.getRouteDefinitions().stream()
+                    .map(RouteDefinition::getInput)
+                    .map(FromDefinition::getEndpointUri)
+                    .map(context::getEndpoint)
+                    .map(Endpoint::getEndpointUri)
+                    .collect(Collectors.toSet());
+
+            final Set<String> toEndpoints = context.getEndpoints().stream()
+                    .map(Endpoint::getEndpointUri)
+                    .filter(Predicate.not(fromEndpoints::contains))
+                    .collect(Collectors.toSet());
+
+            final Set<String> kamelets = context.getEndpoints().stream()
+                    .filter(KameletEndpoint.class::isInstance)
+                    .map(KameletEndpoint.class::cast)
+                    .map(KameletEndpoint::getTemplateId)
+                    .collect(Collectors.toSet());
+
+            SourceMetadata meta = new SourceMetadata();
+            meta.resources.components.addAll(context.getComponentNames());
+            meta.resources.languages.addAll(context.getLanguageNames());
+            meta.resources.dataformats.addAll(context.getDataFormatNames());
+            meta.resources.kamelets.addAll(kamelets);
+            meta.endpoints.from.addAll(fromEndpoints);
+            meta.endpoints.to.addAll(toEndpoints);
+
+            // determine capabilities based on components
+            for (String component : meta.resources.components) {
+                // TODO: add this information to the model so we can retrieve 
them automatically
+                BiConsumer<CamelCatalog, SourceMetadata> consumer = 
COMPONENT_CUSTOMIZERS.get(component);
+                if (consumer != null) {
+                    consumer.accept(catalog, meta);
+                }
+            }
+
+            // determine capabilities based on EIP
+            for (RouteDefinition definition : model.getRouteDefinitions()) {
+                navigateRoute(
+                        definition,
+                        d -> {
+                            if (d instanceof CircuitBreakerDefinition) {
+
+                                meta.capabilities.put(
+                                        Capability.CircuitBreaker,
+                                        catalog
+                                                
.findCapabilityRef(Capability.CircuitBreaker.getValue())
+                                                .orElseGet(() -> new 
EntityRef(null, null)));
+                            }
+                        });
+            }
+
+            if (capabilities.size() == 1) {
+                for (String item : capabilities.get(0).split(",")) {
+                    meta.capabilities.put(
+                            Capability.fromValue(item),
+                            catalog
+                                    .findCapabilityRef(item)
+                                    .orElseGet(() -> new EntityRef(null, 
null)));
+                }
+            }
+
+            meta.dependencies.addAll(deps(
+                    context,
+                    catalog,
+                    rt.size() == 1 ? RuntimeType.fromValue(rt.get(0)) : 
runtimeType,
+                    rv.size() == 1 ? rv.get(0) : runtimeVersion));
+
+            ctx.response()
+                    .putHeader(CONTENT_TYPE_HEADER, MIME_TYPE_JSON)
+                    
.end(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(meta));
+
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void navigateRoute(ProcessorDefinition<?> root, 
Consumer<ProcessorDefinition<?>> consumer) {
+        consumer.accept(root);
+
+        for (ProcessorDefinition<?> def : root.getOutputs()) {
+            navigateRoute(def, consumer);
+        }
+    }
+
+    private Collection<String> deps(CamelContext context, CamelCatalog 
catalog, RuntimeType runtimeType, String runtimeVersion)
+            throws Exception {
+        final Set<String> answer = new TreeSet<>();
+
+        for (String name : context.getComponentNames()) {
+            ComponentModel model = catalog.componentModel(name);
+            if (model != null) {
+                answer.add(String.format("mvn:%s/%s/%s", model.getGroupId(), 
model.getArtifactId(), model.getVersion()));
+            }
+        }
+        for (String name : context.getLanguageNames()) {
+            LanguageModel model = catalog.languageModel(name);
+            if (model != null) {
+                answer.add(String.format("mvn:%s/%s/%s", model.getGroupId(), 
model.getArtifactId(), model.getVersion()));
+            }
+        }
+        for (String name : context.getDataFormatNames()) {
+            DataFormatModel model = catalog.dataFormatModel(name);
+            if (model != null) {
+                answer.add(String.format("mvn:%s/%s/%s", model.getGroupId(), 
model.getArtifactId(), model.getVersion()));
+            }
+        }
+
+        return answer;
+    }
+
+    private CamelCatalog loadCatalog(RuntimeType runtime, String 
runtimeVersion) throws Exception {
+        switch (runtime) {
+            case springBoot:
+                return CatalogLoader.loadSpringBootCatalog(repos, 
runtimeVersion);
+            case quarkus:
+                return CatalogLoader.loadQuarkusCatalog(repos, runtimeVersion);
+            case camelMain:
+                return CatalogLoader.loadCatalog(repos, runtimeVersion);
+            default:
+                throw new IllegalArgumentException("Unsupported runtime: " + 
runtime);
+        }
+    }
+
+    private CamelContext createCamelContext() {
+        final StubComponentResolver componentResolver = new 
StubComponentResolver(STUB_PATTERN, true);
+        final StubDataFormatResolver dataFormatResolver = new 
StubDataFormatResolver(STUB_PATTERN, true);
+        final StubLanguageResolver languageResolver = new 
StubLanguageResolver(STUB_PATTERN, true);
+        final StubTransformerResolver transformerResolver = new 
StubTransformerResolver(STUB_PATTERN, true);
+
+        CamelContext context = new DefaultCamelContext() {
+            @Override
+            public Set<String> getDataFormatNames() {
+                // data formats names are no necessary stored in the context 
as they
+                // are created on demand
+                //
+                // TODO: maybe the camel context should keep track of those ?
+                return dataFormatResolver.getNames();
+            }
+        };
+
+        final ExtendedCamelContext ec = context.getCamelContextExtension();
+        final Model model = ec.getContextPlugin(Model.class);
+
+        model.setModelReifierFactory(new AgentModelReifierFactory());
+
+        ec.addContextPlugin(ComponentResolver.class, componentResolver);
+        ec.addContextPlugin(DataFormatResolver.class, dataFormatResolver);
+        ec.addContextPlugin(LanguageResolver.class, languageResolver);
+        ec.addContextPlugin(TransformerResolver.class, transformerResolver);
+
+        return context;
+    }
+
+    private static final class AgentModelReifierFactory extends 
DefaultModelReifierFactory {
+        @Override
+        public Route createRoute(CamelContext camelContext, Object 
routeDefinition) {
+            if (routeDefinition instanceof RouteDefinition) {
+                ((RouteDefinition) routeDefinition).autoStartup(false);
+            }
+
+            return super.createRoute(camelContext, routeDefinition);
+        }
+    }
+
+}
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 b344ee6e4e7..6da3e3fa6da 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
@@ -27,10 +27,13 @@ public class KubePlugin implements Plugin {
 
     @Override
     public void customize(CommandLine commandLine, CamelJBangMain main) {
-        commandLine.addSubcommand("k", new picocli.CommandLine(new 
KubeCommand(main))
+        var cmd = new picocli.CommandLine(new KubeCommand(main))
+                .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("delete", new picocli.CommandLine(new 
IntegrationDelete(main)))
-                .addSubcommand("logs", new picocli.CommandLine(new 
IntegrationLogs(main))));
+                .addSubcommand("logs", new picocli.CommandLine(new 
IntegrationLogs(main)));
+
+        commandLine.addSubcommand("k", cmd);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/Capability.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/Capability.java
new file mode 100644
index 00000000000..6ffba370f5e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/Capability.java
@@ -0,0 +1,52 @@
+/*
+ * 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.support;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+public enum Capability {
+    PlatformHttp("platform-http"),
+    CircuitBreaker("circuit-breaker"),
+    Health("health"),
+    Tracing("tracing");
+
+    private final String name;
+
+    Capability(String name) {
+        this.name = name;
+    }
+
+    @JsonValue
+    public String getValue() {
+        return this.name;
+    }
+
+    @JsonCreator
+    public static Capability fromValue(String value) {
+        for (Capability c : values()) {
+            if (Objects.equals(c.name, value)) {
+                return c;
+            }
+        }
+
+        throw new IllegalArgumentException("Unsupported value: " + value);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeType.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeType.java
new file mode 100644
index 00000000000..40834ebbeba
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeType.java
@@ -0,0 +1,38 @@
+/*
+ * 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.support;
+
+public enum RuntimeType {
+    springBoot,
+    quarkus,
+    camelMain;
+
+    public static RuntimeType fromValue(String value) {
+        switch (value) {
+            case "spring-boot":
+                return RuntimeType.springBoot;
+            case "quarkus":
+                return RuntimeType.quarkus;
+            case "camel-main":
+                return RuntimeType.camelMain;
+            default:
+                throw new IllegalArgumentException("Unsupported runtime " + 
value);
+        }
+
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeTypeConverter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeTypeConverter.java
new file mode 100644
index 00000000000..c630aaa18fb
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/RuntimeTypeConverter.java
@@ -0,0 +1,26 @@
+/*
+ * 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.support;
+
+import picocli.CommandLine;
+
+public class RuntimeTypeConverter implements 
CommandLine.ITypeConverter<RuntimeType> {
+    public RuntimeType convert(String value) throws Exception {
+        return RuntimeType.fromValue(value);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/SourceMetadata.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/SourceMetadata.java
new file mode 100644
index 00000000000..b6e5c263700
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/SourceMetadata.java
@@ -0,0 +1,60 @@
+/*
+ * 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.support;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import org.apache.camel.tooling.model.EntityRef;
+
+@JsonPropertyOrder(alphabetic = true)
+public class SourceMetadata {
+
+    @JsonProperty
+    public final Reources resources = new Reources();
+    @JsonProperty
+    public final Endpoints endpoints = new Endpoints();
+    @JsonProperty
+    public final Map<Capability, EntityRef> capabilities = new TreeMap<>();
+    @JsonProperty
+    public final Set<String> dependencies = new TreeSet<>();
+
+    @JsonPropertyOrder(alphabetic = true)
+    public static class Reources {
+        @JsonProperty
+        public final Set<String> components = new TreeSet<>();
+        @JsonProperty
+        public final Set<String> languages = new TreeSet<>();
+        @JsonProperty
+        public final Set<String> dataformats = new TreeSet<>();
+        @JsonProperty
+        public final Set<String> kamelets = new TreeSet<>();
+    }
+
+    @JsonPropertyOrder(alphabetic = true)
+    public static class Endpoints {
+        @JsonProperty
+        public final Set<String> from = new TreeSet<>();
+        @JsonProperty
+        public final Set<String> to = new TreeSet<>();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubComponentResolver.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubComponentResolver.java
new file mode 100644
index 00000000000..4c157c5a771
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubComponentResolver.java
@@ -0,0 +1,71 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.component.stub.StubComponent;
+import org.apache.camel.impl.engine.DefaultComponentResolver;
+
+public final class StubComponentResolver extends DefaultComponentResolver {
+    private static final Set<String> ACCEPTED_STUB_NAMES = Set.of(
+            "stub", "bean", "class", "direct", "kamelet", "log", "rest", 
"rest-api", "seda", "vrtx-http");
+
+    private final Set<String> names;
+    private final String stubPattern;
+    private final boolean silent;
+
+    public StubComponentResolver(String stubPattern, boolean silent) {
+        this.names = new TreeSet<>();
+        this.stubPattern = stubPattern;
+        this.silent = silent;
+    }
+
+    @Override
+    public Component resolveComponent(String name, CamelContext context) {
+        final boolean accept = accept(name);
+        final Component answer = super.resolveComponent(accept ? name : 
"stub", context);
+
+        if ((silent || stubPattern != null) && answer instanceof 
StubComponent) {
+            StubComponent sc = (StubComponent) answer;
+            // enable shadow mode on stub component
+            sc.setShadow(true);
+            sc.setShadowPattern(stubPattern);
+        }
+
+        this.names.add(name);
+
+        return answer;
+    }
+
+    private boolean accept(String name) {
+        if (stubPattern == null) {
+            return true;
+        }
+
+        // we are stubbing but need to accept the following
+        return ACCEPTED_STUB_NAMES.contains(name);
+    }
+
+    public Set<String> getNames() {
+        return Set.copyOf(names);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubDataFormatResolver.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubDataFormatResolver.java
new file mode 100644
index 00000000000..6f6c35376e6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubDataFormatResolver.java
@@ -0,0 +1,61 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.DefaultDataFormatResolver;
+import org.apache.camel.main.stub.StubDataFormat;
+import org.apache.camel.spi.DataFormat;
+
+public final class StubDataFormatResolver extends DefaultDataFormatResolver {
+    private final Set<String> names;
+    private final String stubPattern;
+    private final boolean silent;
+
+    public StubDataFormatResolver(String stubPattern, boolean silent) {
+        this.names = new TreeSet<>();
+        this.stubPattern = stubPattern;
+        this.silent = silent;
+    }
+
+    @Override
+    public DataFormat createDataFormat(String name, CamelContext context) {
+        final boolean accept = accept(name);
+        final DataFormat answer = accept ? super.createDataFormat(name, 
context) : new StubDataFormat();
+
+        this.names.add(name);
+
+        return answer;
+    }
+
+    private boolean accept(String name) {
+        if (stubPattern == null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public Set<String> getNames() {
+        return Set.copyOf(this.names);
+    }
+
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubLanguageResolver.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubLanguageResolver.java
new file mode 100644
index 00000000000..46ec7e4791e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubLanguageResolver.java
@@ -0,0 +1,60 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.DefaultLanguageResolver;
+import org.apache.camel.main.stub.StubLanguage;
+import org.apache.camel.spi.Language;
+
+public final class StubLanguageResolver extends DefaultLanguageResolver {
+    private final Set<String> names;
+    private final String stubPattern;
+    private final boolean silent;
+
+    public StubLanguageResolver(String stubPattern, boolean silent) {
+        this.names = new TreeSet<>();
+        this.stubPattern = stubPattern;
+        this.silent = silent;
+    }
+
+    @Override
+    public Language resolveLanguage(String name, CamelContext context) {
+        final boolean accept = accept(name);
+        final Language answer = accept ? super.resolveLanguage(name, context) 
: new StubLanguage();
+
+        this.names.add(name);
+
+        return answer;
+    }
+
+    private boolean accept(String name) {
+        if (stubPattern == null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public Set<String> getNames() {
+        return Set.copyOf(this.names);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubTransformerResolver.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubTransformerResolver.java
new file mode 100644
index 00000000000..bb1e4433b5b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/support/StubTransformerResolver.java
@@ -0,0 +1,61 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.DefaultTransformerResolver;
+import org.apache.camel.impl.engine.TransformerKey;
+import org.apache.camel.main.stub.StubTransformer;
+import org.apache.camel.spi.Transformer;
+
+public final class StubTransformerResolver extends DefaultTransformerResolver {
+    private final Set<String> names;
+    private final String stubPattern;
+    private final boolean silent;
+
+    public StubTransformerResolver(String stubPattern, boolean silent) {
+        this.names = new TreeSet<>();
+        this.stubPattern = stubPattern;
+        this.silent = silent;
+    }
+
+    @Override
+    public Transformer resolve(TransformerKey key, CamelContext context) {
+        final boolean accept = accept(key.toString());
+        final Transformer answer = accept ? super.resolve(key, context) : new 
StubTransformer();
+
+        this.names.add(key.toString());
+
+        return answer;
+    }
+
+    private boolean accept(String name) {
+        if (stubPattern == null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public Set<String> getNames() {
+        return Set.copyOf(this.names);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/AgentTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/AgentTest.java
new file mode 100644
index 00000000000..cb404726a57
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/java/org/apache/camel/dsl/jbang/core/commands/k/AgentTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.restassured.RestAssured;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.StringPrinter;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PluginHelper;
+import org.apache.camel.dsl.jbang.core.common.PluginType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class AgentTest {
+
+    @BeforeAll
+    public void setupFixtures() {
+        CommandLineHelper.useHomeDir("target");
+        PluginHelper.enable(PluginType.CAMEL_K);
+    }
+
+    @Test
+    public void testInspect() throws Exception {
+        Agent agent = cmd();
+        agent.port = 0;
+
+        Vertx vertx = Vertx.vertx();
+        HttpServer server = agent.serve(vertx).toCompletableFuture().get();
+
+        try {
+            int port = server.actualPort();
+            String route = """
+                    - route:
+                        from:
+                          uri: 'timer:tick'
+                          steps:
+                          - to: 'log:info'
+                    """;
+
+            RestAssured.given()
+                    .baseUri("http://localhost";)
+                    .port(port)
+                    .body(route)
+                    .when()
+                    .post("/inspect/routes.yaml")
+                    .then()
+                    .statusCode(200)
+                    .body("resources.components", hasItems("timer", "log"));
+
+        } finally {
+            server.close();
+            vertx.close();
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({ "component/log,200", "component/baz,204" })
+    public void testCatalog(String entityRef, int code) throws Exception {
+        Agent agent = cmd();
+        agent.port = 0;
+
+        Vertx vertx = Vertx.vertx();
+        HttpServer server = agent.serve(vertx).toCompletableFuture().get();
+
+        try {
+            int port = server.actualPort();
+            RestAssured.given()
+                    .baseUri("http://localhost";)
+                    .port(port)
+                    .when()
+                    .get("/catalog/model/" + entityRef)
+                    .then()
+                    .statusCode(code);
+
+        } finally {
+            server.close();
+            vertx.close();
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({ "platform-http,200", "baz,204" })
+    public void testCapability(String name, int code) throws Exception {
+        Agent agent = cmd();
+        agent.port = 0;
+
+        Vertx vertx = Vertx.vertx();
+        HttpServer server = agent.serve(vertx).toCompletableFuture().get();
+
+        try {
+            int port = server.actualPort();
+            RestAssured.given()
+                    .baseUri("http://localhost";)
+                    .port(port)
+                    .when()
+                    .get("/catalog/capability/" + name)
+                    .then()
+                    .statusCode(code);
+
+        } finally {
+            server.close();
+            vertx.close();
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({ "platform-http,other,platform-http-main" })
+    public void testCapabilities(String name, String expectedKind, String 
expectedName) throws Exception {
+        Agent agent = cmd();
+        agent.port = 0;
+
+        Vertx vertx = Vertx.vertx();
+        HttpServer server = agent.serve(vertx).toCompletableFuture().get();
+
+        try {
+            int port = server.actualPort();
+            RestAssured.given()
+                    .baseUri("http://localhost";)
+                    .port(port)
+                    .when()
+                    .get("/catalog/capability/" + name)
+                    .then()
+                    .statusCode(200)
+                    .body("kind", is(expectedKind))
+                    .body("name", is(expectedName));
+
+        } finally {
+            server.close();
+            vertx.close();
+        }
+    }
+
+    @Disabled
+    @Test
+    public void testCall() throws Exception {
+        cmd().doCall();
+    }
+
+    private Agent cmd() {
+        return new Agent(new CamelJBangMain().withPrinter(new 
StringPrinter()));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/test/resources/route-i.yaml 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/resources/route-i.yaml
new file mode 100644
index 00000000000..812400486bc
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-k/src/test/resources/route-i.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+#
+
+- route:
+    from:
+      uri: 'platform-http:/foo'
+      steps:
+      - setBody:
+          constant: 'Hello Camel !!!'
+      - setBody:
+          language:
+            groovy: 'foo'
+      - to: 'log:info'
+      - to: 'kafka:topic'
+      - marshal:
+          json:
+            library: 'Jackson'
+      - circuitBreaker:
+          steps:
+            - log: 'test'
+          configuration: 'my-config'
+          resilience4jConfiguration:
+            failureRateThreshold: 10
+          onFallback:
+            fallbackViaNetwork: true
+      - to:
+          uri: 
"knative:event/foo?apiVersion=eventing.knavine.dev/v1alpha1&kind=Channel&name=bar"

Reply via email to