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 <[email protected]>
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;
+
[email protected](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"