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"