This is an automated email from the ASF dual-hosted git repository. cdeppisch pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new ea0bb2c161d CAMEL-20983: Add JBang Knative trait support ea0bb2c161d is described below commit ea0bb2c161d485910e4193ddc5af97d4de42b717 Author: Christoph Deppisch <cdeppi...@redhat.com> AuthorDate: Wed Jul 24 11:14:18 2024 +0200 CAMEL-20983: Add JBang Knative trait support - When applicable add Knative trigger to Kubenretes manifest - When applicable add Knative channel subscription to Kubernetes manifest - When applicable add Knative sink binding for endpoint URI injection - Auto configure Knative Camel component with proper resources in environment config (knative.json) - Auto configure application.properties in exported project to hold Knative environment configuration --- .../modules/ROOT/pages/camel-jbang-kubernetes.adoc | 244 ++++++++++ .../core/commands/kubernetes/KubernetesExport.java | 20 + .../core/commands/kubernetes/MetadataHelper.java | 24 + .../kubernetes/support/StubComponentResolver.java | 2 +- .../commands/kubernetes/traits/CamelTrait.java | 48 ++ .../commands/kubernetes/traits/TraitCatalog.java | 3 + .../commands/kubernetes/traits/TraitContext.java | 74 +++ .../commands/kubernetes/traits/TraitHelper.java | 2 +- .../kubernetes/traits/knative/KnativeTrait.java | 509 +++++++++++++++++++++ .../commands/kubernetes/KubernetesExportTest.java | 254 ++++++++++ .../src/test/resources/knative-channel-sink.yaml | 23 + .../src/test/resources/knative-channel-source.yaml | 21 + .../src/test/resources/knative-endpoint-sink.yaml | 23 + .../src/test/resources/knative-event-sink.yaml | 23 + .../src/test/resources/knative-event-source.yaml | 21 + .../download/DependencyDownloaderRoutesLoader.java | 4 + 16 files changed, 1293 insertions(+), 2 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc index e9a06d01e11..d1c9f05b197 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc @@ -474,6 +474,250 @@ Refer to the Knative documentation for more information. |=== +=== Connecting to Knative + +The previous section described how the exported Apache Camel application can leverage the Knative service resource with auto-scaling as part of the deployment to Kubernetes. + +Apache Camel also provides a Knative component that makes you easily interact with https://knative.dev/docs/eventing/[Knative eventing] and https://knative.dev/docs/serving/[Knative serving]. + +The Knative component enables you to exchange data with the Knative eventing broker and other Knative services deployed on Kubernetes. +The Camel JBang Kubernetes plugin provides some autoconfiguration options when connecting with the Knative component. +The export command assists you in configuring both the Knative component and the Kubernetes manifest for connecting to Knative resources on the Kubenretes cluster. + +=== Knative trigger + +The concept of a Knative trigger allows you to consume events from the https://knative.dev/docs/eventing/[Knative eventing] broker. +In case your Camel route uses the Knative component as a consumer you may need to create a trigger in Kubernetes in order +to connect your Camel application with the Knative broker. + +The Camel JBang Kubernetes plugin is able to automatically create this trigger for you. + +The following Camel route uses the Knative event component and references a Knative broker by its name. +The plugin inspects the code and automatically generates the Knative trigger as part of the Kubernetes manifest that is used +to run the Camel application on Kubernetes. + +[source,yaml] +---- +- from: + uri: knative:event/camel.evt.type?name=my-broker + steps: + - to: log:info +---- + +The route consumes Knative events of type `camel.evt.type`. +If you export this route with the Camel JBang Kubernetes plugin you will see a Knative trigger being generated as part of the Kubernetes manifest (kubernetes.yml). + +[source,bash] +---- +camel kubernetes export knative-route.yaml +---- + +The generated export project can be deployed to Kubernetes and as part of the deployment the trigger is automatically created so the application can start consuming events. + +The generated trigger looks as follows: + +[source,yaml] +---- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: my-broker-knative-route-camel-evt-type +spec: + broker: my-broker + filter: + attributes: + type: camel.evt.type + subscriber: + ref: + apiVersion: v1 + kind: Service + name: knative-route + uri: /events/camel-evt-type +---- + +The trigger uses a default filter on the event type CloudEvents attribute and calls the Camel application via the exposed Kubernetes service resource. + +The Camel application is automatically configured to expose an Http service so incoming events are handed over to the Camel route. +You can review the Knative service resource configuration that makes Camel configure the Knative component. +The configuration has been automatically created in `src/main/resources/knative.json` in the exported project. + +Here is an example of the generated `knative.json` file: + +[source,json] +---- +{ + "resources" : [ { + "name" : "camel-event", + "type" : "event", + "endpointKind" : "source", + "path" : "/events/camel-event", + "objectApiVersion" : "eventing.knative.dev/v1", + "objectKind" : "Broker", + "objectName" : "my-broker", + "reply" : false + } ] +} +---- + +The exported project has everything configured to run the application on Kubernetes. +Of course, you need Knative eventing installed on your target cluster, and you need to have a Knative broker named `my-broker` available in the target namespace. + +Now you can just deploy the application using the Kubernetes manifest and see the Camel route consuming events from the broker. + +=== Knative channel subscription + +Knative channels represent another form of producing and consuming events from the Knative broker. +Instead of using a trigger you can create a subscription for a Knative channel to consume events. + +The Camel route that connects to a Knative channel in order to receive events looks like this: + +[source,yaml] +---- +- from: + uri: knative:channel/my-channel + steps: + - to: log:info +---- + +The Knative channel is referenced by its name. +The Camel JBang Kubernetes plugin will inspect your code to automatically create a channel subscription as part of the Kubernetes manifest. +You just need to export the Camel route as usual. + +[source,bash] +---- +camel kubernetes export knative-route.yaml +---- + +The code inspection recognizes the Knative component that references the Knative channel and the subscription automatically becomes part of the exported Kubernetes manifest. + +Here is an example subscription that has been generated during the export: + +[source,yaml] +---- +apiVersion: messaging.knative.dev/v1 +kind: Subscription +metadata: + name: my-channel-knative-route +spec: + channel: + apiVersion: messaging.knative.dev/v1 + kind: Channel + name: my-channel + subscriber: + ref: + apiVersion: v1 + kind: Service + name: knative-route + uri: /channels/my-channel +---- + +The subscription connects the Camel application with the channel so each event on the channel is sent to the Kubernetes service resource that also has been created as part of the Kubernetes manifest. + +The Camel Knative component uses a service resource configuration internally to create the proper Http service. +You can review the Knative service resource configuration that makes Camel configure the Knative component. +The configuration has been automatically created in `src/main/resources/knative.json` in the exported project. + +Here is an example of the generated `knative.json` file: + +[source,json] +---- +{ + "resources" : [ { + "name" : "my-channel", + "type" : "channel", + "endpointKind" : "source", + "path" : "/channels/my-channel", + "objectApiVersion" : "messaging.knative.dev/v1", + "objectKind" : "Channel", + "objectName" : "my-channel", + "reply" : false + } ] +} +---- + +Assuming that you have Knative eventing installed on your cluster and that you have setup the Knative channel `my-channel` you can start consuming events right away. +The deployment of the exported project uses the Kubernetes manifest to create all required resources including the Knative subscription. + +=== Knative sink binding + +When connecting to a Knative resource (Broker, Channel, Service) in order to produce events for Knative eventing you probably want to use a `SinkBinding` that resolves the URL to the Knative resource for you. +The sink binding is a Kubernetes resource that makes Knative eventing automatically inject the resource URL into your Camel application on startup. +The Knative URL injection uses environment variables (`K_SINK`, `K_CE_OVERRIDES`) on your deployment. +The Knative eventing operator will automatically resolve the Knative resource (e.g. a Knative broker URL) and inject the value so your application does not need to know the actual URL when deploying. + +The Camel JBang Kubernetes plugin leverages the sink binding concept for all routes that use the Knative component as an output. + +The following route produces events on a Knative broker: + +[source, yaml] +---- +- from: + uri: timer:tick + steps: + - setBody: + constant: Hello Camel !!! + - to: knative:event/camel.evt.type?name=my-broker +---- + +The route produces events of type `camel.evt.type` and pushes the events to the broker named `my-broker`. +At this point the actual Knative broker URL is unknown. +The sink binding is going to resolve the URL and inject its value at deployment time using the `K_SINK` environment variable. + +The Camel JBang Kubernetes plugin export automatically inspects such a route and automatically creates the sink binding resource for us. +The sink binding is part of the exported Kubernetes manifest and is created on the cluster as part of the deployment. + +A sink binding resource that is created by the export command looks like follows: + +[source,bash] +---- +camel kubernetes export knative-route.yaml +---- + +[source,yaml] +---- +apiVersion: sources.knative.dev/v1 +kind: SinkBinding +metadata: + finalizers: + - sinkbindings.sources.knative.dev + name: knative-route +spec: + sink: + ref: + apiVersion: eventing.knative.dev/v1 + kind: Broker + name: my-broker + subject: + apiVersion: apps/v1 + kind: Deployment + name: knative-route +---- + +In addition to creating the sink binding the Camel JBang plugin also takes care of configuring the Knative Camel component. +The Knative component uses a configuration file that you can find in `src/main/resources/knative.json`. +As you can see the configuration uses the `K_SINK` injected property placeholder as a broker URL. + +[source,json] +---- +{ + "resources" : [ { + "name" : "camel-evt-type", + "type" : "event", + "endpointKind" : "sink", + "url" : "{{k.sink}}", + "objectApiVersion" : "eventing.knative.dev/v1", + "objectKind" : "Broker", + "objectName" : "my-broker", + "reply" : false + } ] +} +---- + +As soon as the Kubernetes deployment for the exported project has started the sink binding will inject the `K_SINK` environment variable so that the Camel applicaiton is ready to send events to the Knative broker. + +The sink binding concept works for Knative Broker, Channel and Service resources. +You just reference the resource by its name in your Camel route when sending data to the Knative component as an output of the route (`to("knative:event|channel|endpoint/<resource-name>")`). + === Mount trait options The mount trait is able to configure volume mounts on the Deployment resource in order to inject data from Kubernetes resources such as config maps or secrets. diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExport.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExport.java index bfc65a0fe7a..57b89707985 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExport.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExport.java @@ -20,6 +20,8 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes; import java.io.ByteArrayInputStream; import java.io.File; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -110,6 +112,8 @@ public class KubernetesExport extends Export { description = "The image registry group used to push images to.") protected String imageGroup; + private static final String SRC_MAIN_RESOURCES = "/src/main/resources/"; + public KubernetesExport(CamelJBangMain main) { super(main); } @@ -334,6 +338,22 @@ public class KubernetesExport extends Export { } } + context.doWithConfigurationResources((fileName, content) -> { + try { + File target = new File(exportDir + SRC_MAIN_RESOURCES + fileName); + if (target.exists()) { + Files.writeString(target.toPath(), "%n%s".formatted(content), StandardOpenOption.APPEND); + } else { + safeCopy(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), target); + } + } catch (Exception e) { + if (!quiet) { + printer().printf("Failed to create configuration resource %s - %s%n", + exportDir + SRC_MAIN_RESOURCES + fileName, e.getMessage()); + } + } + }); + if (!quiet) { printer().println("Project export successful!"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/MetadataHelper.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/MetadataHelper.java index 5829ef80da8..87c649d14bb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/MetadataHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/MetadataHelper.java @@ -20,6 +20,7 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.function.BiConsumer; @@ -64,6 +65,7 @@ import org.apache.camel.spi.DataFormatResolver; import org.apache.camel.spi.FactoryFinder; import org.apache.camel.spi.FactoryFinderResolver; import org.apache.camel.spi.LanguageResolver; +import org.apache.camel.spi.PropertiesComponent; import org.apache.camel.spi.Resource; import org.apache.camel.spi.RoutesLoader; import org.apache.camel.spi.TransformerResolver; @@ -72,6 +74,7 @@ import org.apache.camel.support.ResourceHelper; import org.apache.camel.tooling.model.ComponentModel; import org.apache.camel.tooling.model.DataFormatModel; import org.apache.camel.tooling.model.LanguageModel; +import org.apache.camel.util.URISupport; public class MetadataHelper { @@ -199,6 +202,23 @@ public class MetadataHelper { // TODO: maybe the camel context should keep track of those ? return dataFormatResolver.getNames(); } + + @Override + public PropertiesComponent getPropertiesComponent() { + return new org.apache.camel.component.properties.PropertiesComponent() { + @Override + public Optional<String> resolveProperty(String key) { + // properties may not be resolvable in this stubbed context, just use the key as a value + return Optional.of(key); + } + + @Override + public String parseUri(String uri, boolean keepUnresolvedOptional) { + // we are not interested in the query part of endpoint uris, remove it to avoid unresolvable properties + return URISupport.stripQuery(uri); + } + }; + } }; final ExtendedCamelContext ec = context.getCamelContextExtension(); @@ -264,6 +284,10 @@ public class MetadataHelper { if (catalog.componentModel(componentName).isConsumerOnly() && componentName.contains("http")) { return true; } + + if (componentName.equals("knative")) { + return true; + } } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/support/StubComponentResolver.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/support/StubComponentResolver.java index c3d2d7e8201..07858a27da7 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/support/StubComponentResolver.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/support/StubComponentResolver.java @@ -27,7 +27,7 @@ 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"); + "stub", "bean", "class", "direct", "kamelet", "log", "rest", "rest-api", "seda", "vertx-http"); private final Set<String> names; private final String stubPattern; diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/CamelTrait.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/CamelTrait.java new file mode 100644 index 00000000000..72d3701c700 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/CamelTrait.java @@ -0,0 +1,48 @@ +/* + * 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.kubernetes.traits; + +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.v1.integrationspec.Traits; +import org.apache.camel.v1.integrationspec.traits.Camel; + +public class CamelTrait extends BaseTrait { + + public CamelTrait() { + super("camel", 12000); + } + + @Override + public boolean configure(Traits traitConfig, TraitContext context) { + return true; + } + + @Override + public void apply(Traits traitConfig, TraitContext context) { + Camel camelTrait = Optional.ofNullable(traitConfig.getCamel()).orElseGet(Camel::new); + + if (ObjectHelper.isNotEmpty(camelTrait.getProperties())) { + // TODO: use ConfigMap resource + context.addConfigurationResource("application.properties", + camelTrait.getProperties().stream().collect(Collectors.joining(System.lineSeparator()))); + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitCatalog.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitCatalog.java index 017211d904b..01c00630b31 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitCatalog.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitCatalog.java @@ -23,6 +23,7 @@ import java.util.Locale; import java.util.stream.Collectors; import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.knative.KnativeServiceTrait; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.knative.KnativeTrait; import org.apache.camel.v1.integrationspec.Traits; /** @@ -36,6 +37,7 @@ public class TraitCatalog { public TraitCatalog() { register(new DeploymentTrait()); + register(new KnativeTrait()); register(new KnativeServiceTrait()); register(new ServiceTrait()); register(new ContainerTrait()); @@ -44,6 +46,7 @@ public class TraitCatalog { register(new OpenApiTrait()); register(new LabelTrait()); register(new AnnotationTrait()); + register(new CamelTrait()); } public List<Trait> allTraits() { diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitContext.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitContext.java index a4682171fcb..6607d93ae1c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitContext.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitContext.java @@ -23,8 +23,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.stream.Collectors; +import io.fabric8.knative.eventing.v1.TriggerBuilder; +import io.fabric8.knative.messaging.v1.SubscriptionBuilder; import io.fabric8.kubernetes.api.builder.Builder; import io.fabric8.kubernetes.api.builder.VisitableBuilder; import io.fabric8.kubernetes.api.builder.Visitor; @@ -35,6 +38,8 @@ import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder; import org.apache.camel.RuntimeCamelException; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.dsl.jbang.core.commands.kubernetes.CatalogHelper; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.MetadataHelper; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.support.SourceMetadata; import org.apache.camel.dsl.jbang.core.common.Printer; import org.apache.camel.dsl.jbang.core.common.RuntimeType; import org.apache.camel.dsl.jbang.core.common.Source; @@ -44,6 +49,10 @@ public class TraitContext { private final List<VisitableBuilder<?, ?>> resourceRegistry = new ArrayList<>(); private TraitProfile profile = TraitProfile.KUBERNETES; + private final Map<String, String> configurationResources = new HashMap<>(); + + private final Map<String, SourceMetadata> sourceMetadata = new HashMap<>(); + private final Map<String, String> annotations = new HashMap<>(); private final Map<String, String> labels = new HashMap<>(); @@ -132,6 +141,23 @@ public class TraitContext { .findFirst(); } + public Optional<TriggerBuilder> getKnativeTrigger(String triggerName, String brokerName) { + return resourceRegistry.stream() + .filter(it -> it.getClass().isAssignableFrom(TriggerBuilder.class)) + .map(it -> (TriggerBuilder) it) + .filter(trigger -> triggerName.equals(trigger.buildMetadata().getName())) + .filter(trigger -> brokerName.equals(trigger.buildSpec().getBroker())) + .findFirst(); + } + + public Optional<SubscriptionBuilder> getKnativeSubscription(String name) { + return resourceRegistry.stream() + .filter(it -> it.getClass().isAssignableFrom(SubscriptionBuilder.class)) + .map(it -> (SubscriptionBuilder) it) + .filter(subscription -> name.equals(subscription.buildMetadata().getName())) + .findFirst(); + } + public String getName() { return name; } @@ -188,4 +214,52 @@ public class TraitContext { public String getServiceAccount() { return serviceAccount; } + + /** + * Performs source metadata inspection and uses local cache to not inspect the same source over and over again. + * + * @param source the source to inspect and create the metadata from. + * @return the metadata holding information such as components, endpoints, languages used with the source. + * @throws Exception + */ + public SourceMetadata inspectMetaData(Source source) throws Exception { + if (sourceMetadata.containsKey(source.name())) { + return sourceMetadata.get(source.name()); + } + + SourceMetadata metadata = MetadataHelper.readFromSource(catalog, source); + sourceMetadata.put(source.name(), metadata); + return metadata; + } + + /** + * Inspect all sources in this context and retrieve the source metadata. Uses internal cache for sources that have + * already been inspected. + * + * @return + */ + public List<SourceMetadata> getSourceMetadata() { + List<SourceMetadata> answer = new ArrayList<>(); + if (sources != null) { + for (Source source : sources) { + answer.add(sourceMetadata.computeIfAbsent(source.name(), name -> { + try { + return MetadataHelper.readFromSource(catalog, source); + } catch (Exception e) { + throw new RuntimeCamelException(e); + } + })); + } + } + + return answer; + } + + public void addConfigurationResource(String name, String content) { + this.configurationResources.put(name, content); + } + + public void doWithConfigurationResources(BiConsumer<String, String> consumer) { + configurationResources.forEach(consumer); + } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitHelper.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitHelper.java index d4d65c38bb5..6e7a9bf32b2 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/TraitHelper.java @@ -327,7 +327,7 @@ public final class TraitHelper { CamelCatalog catalog = context.getCatalog(); if (context.getSources() != null) { for (Source source : context.getSources()) { - SourceMetadata metadata = MetadataHelper.readFromSource(catalog, source); + SourceMetadata metadata = context.inspectMetaData(source); if (MetadataHelper.exposesHttpServices(catalog, metadata)) { exposesHttpServices = true; break; diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/knative/KnativeTrait.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/knative/KnativeTrait.java new file mode 100644 index 00000000000..f92fd26152b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/knative/KnativeTrait.java @@ -0,0 +1,509 @@ +/* + * 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.kubernetes.traits.knative; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.fabric8.knative.eventing.v1.TriggerBuilder; +import io.fabric8.knative.eventing.v1.TriggerFilterBuilder; +import io.fabric8.knative.internal.pkg.apis.duck.v1.DestinationBuilder; +import io.fabric8.knative.internal.pkg.apis.duck.v1.KReference; +import io.fabric8.knative.internal.pkg.apis.duck.v1.KReferenceBuilder; +import io.fabric8.knative.internal.pkg.tracker.ReferenceBuilder; +import io.fabric8.knative.messaging.v1.SubscriptionBuilder; +import io.fabric8.knative.sources.v1.SinkBindingBuilder; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.KubernetesHelper; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.support.SourceMetadata; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.ServiceTrait; +import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.TraitContext; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.URISupport; +import org.apache.camel.v1.integrationspec.Traits; +import org.apache.camel.v1.integrationspec.traits.Camel; +import org.apache.camel.v1.integrationspec.traits.Knative; + +public class KnativeTrait extends KnativeBaseTrait { + + private static final Pattern knativeUriPattern + = Pattern.compile("^knative:/*(channel|endpoint|event)(?:[?].*|$|/([A-Za-z0-9.-]+)(?:[/?].*|$))"); + private static final Pattern plainNamePattern = Pattern.compile("^[A-Za-z0-9.-]+$"); + private static final String K_SINK_URL = "{{k.sink:http://localhost:8080}}"; + + private final List<Map<String, Object>> knativeResourcesConfig = new ArrayList<>(); + + public KnativeTrait() { + super("knative", ServiceTrait.SERVICE_TRAIT_ORDER + 100); + } + + @Override + public boolean configure(Traits traitConfig, TraitContext context) { + Knative knativeTrait = Optional.ofNullable(traitConfig.getKnative()).orElseGet(Knative::new); + + if (knativeTrait.getEnabled() != null && !knativeTrait.getEnabled()) { + // Knative explicitly disabled + return false; + } + + List<SourceMetadata> allSourcesMetadata = context.getSourceMetadata(); + + if (knativeTrait.getChannelSources() == null) { + knativeTrait.setChannelSources(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.CHANNEL, "from")); + } + + if (knativeTrait.getChannelSinks() == null) { + knativeTrait.setChannelSinks(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.CHANNEL, "to")); + } + + if (knativeTrait.getEndpointSources() == null) { + knativeTrait + .setEndpointSources(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.ENDPOINT, "from")); + } + + if (knativeTrait.getEndpointSinks() == null) { + knativeTrait.setEndpointSinks(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.ENDPOINT, "to")); + } + + if (knativeTrait.getEventSources() == null) { + knativeTrait.setEventSources(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.EVENT, "from")); + } + + if (knativeTrait.getEventSinks() == null) { + knativeTrait.setEventSinks(extractKnativeEndpointUris(allSourcesMetadata, KnativeResourceType.EVENT, "to")); + } + + boolean hasKnativeEndpoint = !knativeTrait.getChannelSources().isEmpty() || + !knativeTrait.getChannelSinks().isEmpty() || + !knativeTrait.getEndpointSources().isEmpty() || + !knativeTrait.getEndpointSinks().isEmpty() || + !knativeTrait.getEventSources().isEmpty() || + !knativeTrait.getEventSinks().isEmpty(); + + if (knativeTrait.getSinkBinding() == null) { + knativeTrait.setSinkBinding(shouldCreateSinkBinding(knativeTrait)); + } + + knativeTrait.setEnabled(hasKnativeEndpoint); + traitConfig.setKnative(knativeTrait); + + return hasKnativeEndpoint; + } + + @Override + public void apply(Traits traitConfig, TraitContext context) { + Knative knativeTrait = Optional.ofNullable(traitConfig.getKnative()).orElseGet(Knative::new); + + configureChannels(knativeTrait, context); + configureEndpoints(knativeTrait, context); + configureEvents(knativeTrait, context); + + if (knativeTrait.getSinkBinding() != null && knativeTrait.getSinkBinding()) { + createSinkBinding(knativeTrait, context); + } + + if (!knativeResourcesConfig.isEmpty()) { + try { + context.addConfigurationResource("knative.json", KubernetesHelper.json() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(Collections.singletonMap("resources", knativeResourcesConfig))); + Camel camelTrait = Optional.ofNullable(traitConfig.getCamel()).orElseGet(Camel::new); + if (camelTrait.getProperties() == null) { + camelTrait.setProperties(new ArrayList<>()); + } + camelTrait.getProperties().add("camel.component.knative.environmentPath=classpath:knative.json"); + traitConfig.setCamel(camelTrait); + } catch (JsonProcessingException e) { + context.printer().printf("Failed to write knative.json environment configuration - %s%n", e.getMessage()); + } + } + } + + private void configureChannels(Knative knativeTrait, TraitContext context) { + for (String uri : knativeTrait.getChannelSources()) { + createSubscription(toKnativeUri(KnativeResourceType.CHANNEL, uri), knativeTrait, context); + } + + for (String uri : knativeTrait.getChannelSinks()) { + String channelName = extractKnativeResource(uri); + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + channelName, + KnativeResourceType.CHANNEL, + "sink", + K_SINK_URL, + null, + channelName, + null)); + } + } + + private void configureEndpoints(Knative knativeTrait, TraitContext context) { + for (String uri : knativeTrait.getEndpointSources()) { + String endpointName = extractKnativeResource(uri); + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + endpointName, + KnativeResourceType.ENDPOINT, + "source", + null, + "/", + endpointName, + null)); + } + + for (String uri : knativeTrait.getEndpointSinks()) { + String endpointName = extractKnativeResource(uri); + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + endpointName, + KnativeResourceType.ENDPOINT, + "sink", + K_SINK_URL, + null, + endpointName, + null)); + } + } + + private void configureEvents(Knative knativeTrait, TraitContext context) { + for (String uri : knativeTrait.getEventSources()) { + createTrigger(toKnativeUri(KnativeResourceType.EVENT, uri), knativeTrait, context); + } + + for (String uri : knativeTrait.getEventSinks()) { + String eventType = extractKnativeResource(uri); + String brokerName = extractBrokerName(uri); + String serviceName = ObjectHelper.isNotEmpty(eventType) ? brokerName : "default"; + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + serviceName, + KnativeResourceType.EVENT, + "sink", + K_SINK_URL, + null, + brokerName, + null)); + } + } + + private void createSubscription(String uri, Knative knativeTrait, TraitContext context) { + String channelName = extractKnativeResource(uri); + + String subscriptionName = createSubscriptionName(context.getName(), channelName); + if (ObjectHelper.isEmpty(channelName) || context.getKnativeSubscription(subscriptionName).isPresent()) { + // no channel name given or same subscription already exists + return; + } + + String servicePath = "/channels/%s".formatted(channelName); + + SubscriptionBuilder subscription = new SubscriptionBuilder() + .withNewMetadata() + .withName(subscriptionName) + .endMetadata() + .withNewSpec() + .withChannel(new KReferenceBuilder() + .withApiVersion(KnativeResourceType.CHANNEL.getApiVersion()) + .withKind(KnativeResourceType.CHANNEL.getKind()) + .withName(channelName) + .build()) + .withSubscriber(new DestinationBuilder() + .withRef(getSubscriberRef(context)) + .withUri(servicePath) + .build()) + .endSpec(); + + context.add(subscription); + + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + channelName, + KnativeResourceType.CHANNEL, + "source", + null, + servicePath, + channelName, + null)); + } + + private void createTrigger(String uri, Knative knativeTrait, TraitContext context) { + String eventType = extractKnativeResource(uri); + String brokerName = extractBrokerName(uri); + + String triggerName = createTriggerName(context.getName(), brokerName, eventType); + if (context.getKnativeTrigger(triggerName, brokerName).isPresent()) { + // same trigger already exists + return; + } + + String servicePath = "/events/%s".formatted(eventType); + + TriggerBuilder trigger = new TriggerBuilder() + .withNewMetadata() + .withName(triggerName) + .endMetadata() + .withNewSpec() + .withBroker(brokerName) + .withSubscriber(new DestinationBuilder() + .withRef(getSubscriberRef(context)) + .withUri(servicePath) + .build()) + .withFilter(new TriggerFilterBuilder().addToAttributes(getFilterAttributes(knativeTrait, eventType)).build()) + .endSpec(); + + context.add(trigger); + + String serviceName = ObjectHelper.isNotEmpty(eventType) ? eventType : "default"; + addKnativeResourceConfiguration(new KnativeResourceConfiguration( + serviceName, + KnativeResourceType.EVENT, + "source", + null, + servicePath, + brokerName, + null)); + } + + private Map<String, String> getFilterAttributes(Knative knativeTrait, String eventType) { + Map<String, String> filterAttributes = new HashMap<>(); + filterAttributes.put("type", eventType); + + // TODO: use this as soon as new Camel K CRD model has been released + // for (String filterExpression : knativeTrait.getFilters()) { + // String[] keyValue = filterExpression.split("=", 2); + // if (keyValue.length != 2) { + // throw new RuntimeCamelException("Invalid Knative trigger filter expression: %s".formatted(filterExpression)); + // } + // filterAttributes.put(keyValue[0].trim(), keyValue[1].trim()); + // } + // + // if (!filterAttributes.containsKey("type") && Optional.ofNullable(knativeTrait.getFilterEventType()).orElse(true) && ObjectHelper.isNotEmpty(eventType)) { + // // Apply default trigger filter attribute for the event type + // filterAttributes.put("type", eventType); + // } + + return filterAttributes; + } + + /** + * Adds service endpoint to the Knative component environment configuration. + */ + private void addKnativeResourceConfiguration(KnativeResourceConfiguration configuration) { + knativeResourcesConfig.add(configuration.toJsonMap()); + } + + /** + * Create subscriber reference based on which service type has been set on the give context. Assumes that the either + * Knative service or arbitrary service has already been set on the context due to trait execution ordering. If + * Knative service is present use this service subscriber kind otherwise use arbitrary service subscriber reference. + */ + private static KReference getSubscriberRef(TraitContext context) { + if (context.getKnativeService().isPresent()) { + return new KReferenceBuilder() + .withApiVersion(KnativeResourceType.ENDPOINT.getApiVersion()) + .withKind(KnativeResourceType.ENDPOINT.getKind()) + .withName(context.getName()) + .build(); + } else { + return new KReferenceBuilder() + .withApiVersion("v1") + .withKind("Service") + .withName(context.getName()) + .build(); + } + } + + private static String createTriggerName(String subscriberName, String brokerName, String eventType) { + String nameSuffix = ""; + if (ObjectHelper.isNotEmpty(eventType)) { + nameSuffix = "-%s".formatted(KubernetesHelper.sanitize(eventType)); + } + + return KubernetesHelper.sanitize(brokerName) + "-" + subscriberName + nameSuffix; + } + + private void createSinkBinding(Knative knativeTrait, TraitContext context) { + final KnativeResourceType resourceType; + final String uri; + final String resourceName; + if (!knativeTrait.getChannelSinks().isEmpty()) { + uri = knativeTrait.getChannelSinks().get(0); + resourceType = KnativeResourceType.CHANNEL; + resourceName = extractKnativeResource(uri); + } else if (!knativeTrait.getEndpointSinks().isEmpty()) { + uri = knativeTrait.getEndpointSinks().get(0); + resourceType = KnativeResourceType.ENDPOINT; + resourceName = extractKnativeResource(uri); + } else if (!knativeTrait.getEventSinks().isEmpty()) { + uri = knativeTrait.getEventSinks().get(0); + resourceType = KnativeResourceType.EVENT; + resourceName = extractBrokerName(uri); + } else { + context.printer().println("Failed to create sink binding!"); + return; + } + + context.add(new SinkBindingBuilder() + .withNewMetadata() + .withName(context.getName()) + .withFinalizers("sinkbindings.sources.knative.dev") + .endMetadata() + .withNewSpec() + .withSubject(new ReferenceBuilder() + .withApiVersion("apps/v1") + .withKind("Deployment") + .withName(context.getName()) + .build()) + .withNewSink() + .withRef(resourceType.getReference(resourceName)) + .endSink() + .endSpec()); + } + + private static String createSubscriptionName(String subscriberName, String channelName) { + return "%s-%s".formatted(channelName, subscriberName); + } + + private static String extractBrokerName(String uri) { + try { + return URISupport.parseQuery(URISupport.extractQuery(uri)).getOrDefault("name", "default").toString(); + } catch (Exception e) { + return "default"; + } + } + + /** + * Sink binding should be created only when a single Knative sink is being used. + * + * @param knativeTrait holding the sinks for channels, endpoints and events. + * @return true when single Knative sink is used, otherwise false. + */ + private static boolean shouldCreateSinkBinding(Knative knativeTrait) { + return knativeTrait.getChannelSinks().size() + knativeTrait.getEndpointSinks().size() + + knativeTrait.getEventSinks().size() == 1; + } + + private static List<String> extractKnativeEndpointUris( + List<SourceMetadata> metadata, KnativeResourceType resourceType, String endpointType) { + return metadata.stream() + .flatMap(m -> endpointType.equals("from") ? m.endpoints.from.stream() : m.endpoints.to.stream()) + .filter(uri -> isKnativeUri(resourceType, uri)) + .collect(Collectors.toList()); + } + + private static String extractKnativeResource(String uri) { + Matcher uriMatch = knativeUriPattern.matcher(uri); + if (uriMatch.matches()) { + return Optional.ofNullable(uriMatch.group(2)).orElse(""); + } + + return ""; + } + + private static boolean isKnativeUri(KnativeResourceType resourceType, String uri) { + Matcher uriMatcher = knativeUriPattern.matcher(uri); + return uriMatcher.matches() && uriMatcher.group(1).equals(resourceType.getType()); + } + + /** + * If applicable, converts plain service, channel or broker name to Knative component endpoint URI. + */ + private static String toKnativeUri(KnativeResourceType resourceType, String uriOrName) { + if (plainNamePattern.matcher(uriOrName).matches()) { + return "knative://%s/%s".formatted(resourceType.getType(), uriOrName); + } + + return uriOrName; + } + + enum KnativeResourceType { + CHANNEL("Channel", "messaging.knative.dev/v1"), + ENDPOINT("Service", "serving.knative.dev/v1"), + EVENT("Broker", "eventing.knative.dev/v1"); + + private final String kind; + private final String apiVersion; + + KnativeResourceType(String kind, String apiVersion) { + this.kind = kind; + this.apiVersion = apiVersion; + } + + public String getKind() { + return kind; + } + + public String getApiVersion() { + return apiVersion; + } + + public String getType() { + return this.name().toLowerCase(Locale.US); + } + + public KReference getReference(String resourceName) { + return new KReferenceBuilder() + .withApiVersion(apiVersion) + .withKind(kind) + .withName(resourceName) + .build(); + } + } + + private record KnativeResourceConfiguration(String name, KnativeResourceType resourceType, String endpointKind, String url, + String path, String objectName, Map<String, String> ceOverrides) { + + public Map<String, Object> toJsonMap() { + Map<String, Object> json = new LinkedHashMap<>(); + + json.put("name", name); + json.put("type", resourceType.getType()); + json.put("endpointKind", endpointKind); + + if (ObjectHelper.isNotEmpty(url)) { + json.put("url", url); + } + + if (ObjectHelper.isNotEmpty(path)) { + json.put("path", path); + } + + json.put("objectApiVersion", resourceType.getApiVersion()); + json.put("objectKind", resourceType.getKind()); + json.put("objectName", objectName); + + if (ObjectHelper.isNotEmpty(ceOverrides)) { + json.put("ceOverrides", ceOverrides); + } + + // knative.reply is set to true in case of endpoint service as a source + if (resourceType == KnativeResourceType.ENDPOINT && endpointKind.equals("source")) { + json.put("reply", true); + } else { + json.put("reply", false); + } + + return json; + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExportTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExportTest.java index 8d7918ac5d7..6e75daf6bff 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExportTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesExportTest.java @@ -17,18 +17,24 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.stream.Stream; +import io.fabric8.knative.eventing.v1.Trigger; +import io.fabric8.knative.messaging.v1.Subscription; +import io.fabric8.knative.sources.v1.SinkBinding; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; @@ -36,6 +42,7 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait; import org.apache.camel.dsl.jbang.core.common.RuntimeType; +import org.apache.camel.util.IOHelper; import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.junit.jupiter.api.Assertions; @@ -108,6 +115,36 @@ class KubernetesExportTest extends KubernetesBaseTest { Assertions.assertFalse(hasKnativeService(rt)); } + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddApplicationProperties(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:route.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.traits = new String[] { + "camel.properties=[foo=bar, bar=baz]" }; + int exit = command.doCall(); + + Assertions.assertEquals(0, exit); + Deployment deployment = getDeployment(rt); + Assertions.assertEquals("route", deployment.getMetadata().getName()); + Assertions.assertEquals(1, deployment.getSpec().getTemplate().getSpec().getContainers().size()); + Assertions.assertEquals("route", deployment.getMetadata().getLabels().get(BaseTrait.INTEGRATION_LABEL)); + Assertions.assertEquals("route", deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getName()); + Assertions.assertEquals(1, deployment.getSpec().getSelector().getMatchLabels().size()); + Assertions.assertEquals("route", + deployment.getSpec().getSelector().getMatchLabels().get(BaseTrait.INTEGRATION_LABEL)); + Assertions.assertEquals("camel-test/route:1.0-SNAPSHOT", + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + + Assertions.assertFalse(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + Properties applicationProperties = getApplicationProperties(workingDir); + + Assertions.assertEquals("bar", applicationProperties.get("foo")); + Assertions.assertEquals("baz", applicationProperties.get("bar")); + } + @ParameterizedTest @MethodSource("runtimeProvider") public void shouldAddServiceSpec(RuntimeType rt) throws Exception { @@ -235,6 +272,206 @@ class KubernetesExportTest extends KubernetesBaseTest { service.getSpec().getTemplate().getMetadata().getAnnotations().get("autoscaling.knative.dev/maxScale")); } + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddKnativeTrigger(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:knative-event-source.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.doCall(); + + Assertions.assertTrue(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + Trigger trigger = getResource(rt, Trigger.class) + .orElseThrow(() -> new RuntimeCamelException("Missing Knative trigger in Kubernetes manifest")); + + Assertions.assertEquals("my-broker-knative-event-source-camel-event", trigger.getMetadata().getName()); + Assertions.assertEquals("my-broker", trigger.getSpec().getBroker()); + Assertions.assertEquals(1, trigger.getSpec().getFilter().getAttributes().size()); + Assertions.assertEquals("camel-event", trigger.getSpec().getFilter().getAttributes().get("type")); + Assertions.assertEquals("knative-event-source", trigger.getSpec().getSubscriber().getRef().getName()); + Assertions.assertEquals("Service", trigger.getSpec().getSubscriber().getRef().getKind()); + Assertions.assertEquals("v1", trigger.getSpec().getSubscriber().getRef().getApiVersion()); + Assertions.assertEquals("/events/camel-event", trigger.getSpec().getSubscriber().getUri()); + + Properties applicationProperties = getApplicationProperties(workingDir); + Assertions.assertEquals("classpath:knative.json", applicationProperties.get("camel.component.knative.environmentPath")); + + Assertions.assertEquals(""" + { + "resources" : [ { + "name" : "camel-event", + "type" : "event", + "endpointKind" : "source", + "path" : "/events/camel-event", + "objectApiVersion" : "eventing.knative.dev/v1", + "objectKind" : "Broker", + "objectName" : "my-broker", + "reply" : false + } ] + } + """, getKnativeResourceConfiguration(workingDir)); + } + + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddKnativeSubscription(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:knative-channel-source.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.doCall(); + + Assertions.assertTrue(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + Subscription subscription = getResource(rt, Subscription.class) + .orElseThrow(() -> new RuntimeCamelException("Missing Knative subscription in Kubernetes manifest")); + + Assertions.assertEquals("my-channel-knative-channel-source", subscription.getMetadata().getName()); + Assertions.assertEquals("my-channel", subscription.getSpec().getChannel().getName()); + Assertions.assertEquals("knative-channel-source", subscription.getSpec().getSubscriber().getRef().getName()); + Assertions.assertEquals("Service", subscription.getSpec().getSubscriber().getRef().getKind()); + Assertions.assertEquals("v1", subscription.getSpec().getSubscriber().getRef().getApiVersion()); + Assertions.assertEquals("/channels/my-channel", subscription.getSpec().getSubscriber().getUri()); + + Properties applicationProperties = getApplicationProperties(workingDir); + Assertions.assertEquals("classpath:knative.json", applicationProperties.get("camel.component.knative.environmentPath")); + + Assertions.assertEquals(""" + { + "resources" : [ { + "name" : "my-channel", + "type" : "channel", + "endpointKind" : "source", + "path" : "/channels/my-channel", + "objectApiVersion" : "messaging.knative.dev/v1", + "objectKind" : "Channel", + "objectName" : "my-channel", + "reply" : false + } ] + } + """, getKnativeResourceConfiguration(workingDir)); + } + + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddKnativeBrokerSinkBinding(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:knative-event-sink.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.doCall(); + + Assertions.assertFalse(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + SinkBinding sinkBinding = getResource(rt, SinkBinding.class) + .orElseThrow(() -> new RuntimeCamelException("Missing Knative sinkBinding in Kubernetes manifest")); + + Assertions.assertEquals("knative-event-sink", sinkBinding.getMetadata().getName()); + Assertions.assertEquals("my-broker", sinkBinding.getSpec().getSink().getRef().getName()); + Assertions.assertEquals("Broker", sinkBinding.getSpec().getSink().getRef().getKind()); + Assertions.assertEquals("eventing.knative.dev/v1", sinkBinding.getSpec().getSink().getRef().getApiVersion()); + Assertions.assertEquals("knative-event-sink", sinkBinding.getSpec().getSubject().getName()); + Assertions.assertEquals("Deployment", sinkBinding.getSpec().getSubject().getKind()); + Assertions.assertEquals("apps/v1", sinkBinding.getSpec().getSubject().getApiVersion()); + + Properties applicationProperties = getApplicationProperties(workingDir); + Assertions.assertEquals("classpath:knative.json", applicationProperties.get("camel.component.knative.environmentPath")); + + Assertions.assertEquals(""" + { + "resources" : [ { + "name" : "my-broker", + "type" : "event", + "endpointKind" : "sink", + "url" : "{{k.sink:http://localhost:8080}}", + "objectApiVersion" : "eventing.knative.dev/v1", + "objectKind" : "Broker", + "objectName" : "my-broker", + "reply" : false + } ] + } + """, getKnativeResourceConfiguration(workingDir)); + } + + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddKnativeChannelSinkBinding(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:knative-channel-sink.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.doCall(); + + Assertions.assertFalse(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + SinkBinding sinkBinding = getResource(rt, SinkBinding.class) + .orElseThrow(() -> new RuntimeCamelException("Missing Knative sinkBinding in Kubernetes manifest")); + + Assertions.assertEquals("knative-channel-sink", sinkBinding.getMetadata().getName()); + Assertions.assertEquals("my-channel", sinkBinding.getSpec().getSink().getRef().getName()); + Assertions.assertEquals("Channel", sinkBinding.getSpec().getSink().getRef().getKind()); + Assertions.assertEquals("messaging.knative.dev/v1", sinkBinding.getSpec().getSink().getRef().getApiVersion()); + Assertions.assertEquals("knative-channel-sink", sinkBinding.getSpec().getSubject().getName()); + Assertions.assertEquals("Deployment", sinkBinding.getSpec().getSubject().getKind()); + Assertions.assertEquals("apps/v1", sinkBinding.getSpec().getSubject().getApiVersion()); + + Properties applicationProperties = getApplicationProperties(workingDir); + Assertions.assertEquals("classpath:knative.json", applicationProperties.get("camel.component.knative.environmentPath")); + + Assertions.assertEquals(""" + { + "resources" : [ { + "name" : "my-channel", + "type" : "channel", + "endpointKind" : "sink", + "url" : "{{k.sink:http://localhost:8080}}", + "objectApiVersion" : "messaging.knative.dev/v1", + "objectKind" : "Channel", + "objectName" : "my-channel", + "reply" : false + } ] + } + """, getKnativeResourceConfiguration(workingDir)); + } + + @ParameterizedTest + @MethodSource("runtimeProvider") + public void shouldAddKnativeEndpointSinkBinding(RuntimeType rt) throws Exception { + KubernetesExport command = createCommand(new String[] { "classpath:knative-endpoint-sink.yaml" }, + "--image-group=camel-test", "--runtime=" + rt.runtime()); + command.doCall(); + + Assertions.assertFalse(hasService(rt)); + Assertions.assertFalse(hasKnativeService(rt)); + + SinkBinding sinkBinding = getResource(rt, SinkBinding.class) + .orElseThrow(() -> new RuntimeCamelException("Missing Knative sinkBinding in Kubernetes manifest")); + + Assertions.assertEquals("knative-endpoint-sink", sinkBinding.getMetadata().getName()); + Assertions.assertEquals("my-endpoint", sinkBinding.getSpec().getSink().getRef().getName()); + Assertions.assertEquals("Service", sinkBinding.getSpec().getSink().getRef().getKind()); + Assertions.assertEquals("serving.knative.dev/v1", sinkBinding.getSpec().getSink().getRef().getApiVersion()); + Assertions.assertEquals("knative-endpoint-sink", sinkBinding.getSpec().getSubject().getName()); + Assertions.assertEquals("Deployment", sinkBinding.getSpec().getSubject().getKind()); + Assertions.assertEquals("apps/v1", sinkBinding.getSpec().getSubject().getApiVersion()); + + Properties applicationProperties = getApplicationProperties(workingDir); + Assertions.assertEquals("classpath:knative.json", applicationProperties.get("camel.component.knative.environmentPath")); + + Assertions.assertEquals(""" + { + "resources" : [ { + "name" : "my-endpoint", + "type" : "endpoint", + "endpointKind" : "sink", + "url" : "{{k.sink:http://localhost:8080}}", + "objectApiVersion" : "serving.knative.dev/v1", + "objectKind" : "Service", + "objectName" : "my-endpoint", + "reply" : false + } ] + } + """, getKnativeResourceConfiguration(workingDir)); + } + @ParameterizedTest @MethodSource("runtimeProvider") public void shouldAddVolumes(RuntimeType rt) throws Exception { @@ -447,6 +684,23 @@ class KubernetesExportTest extends KubernetesBaseTest { return Optional.empty(); } + private String readResource(File workingDir, String path) throws IOException { + try (FileInputStream fis = new FileInputStream(workingDir.toPath().resolve(path).toFile())) { + return IOHelper.loadText(fis); + } + } + + private Properties getApplicationProperties(File workingDir) throws IOException { + String content = readResource(workingDir, "src/main/resources/application.properties"); + Properties applicationProperties = new Properties(); + applicationProperties.load(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + return applicationProperties; + } + + private String getKnativeResourceConfiguration(File workingDir) throws IOException { + return readResource(workingDir, "src/main/resources/knative.json"); + } + private Model readMavenModel() throws Exception { File f = workingDir.toPath().resolve("pom.xml").toFile(); Assertions.assertTrue(f.isFile(), "Not a pom.xml file: " + f); diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-sink.yaml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-sink.yaml new file mode 100644 index 00000000000..8ab97dad8fe --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-sink.yaml @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- from: + uri: timer:tick + steps: + - setBody: + constant: Hello Camel !!! + - to: knative:channel/my-channel diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-source.yaml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-source.yaml new file mode 100644 index 00000000000..e5ec605b038 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-channel-source.yaml @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- from: + uri: knative:channel/my-channel + steps: + - to: log:info diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-endpoint-sink.yaml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-endpoint-sink.yaml new file mode 100644 index 00000000000..db73e3191e2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-endpoint-sink.yaml @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- from: + uri: timer:tick + steps: + - setBody: + constant: Hello Camel !!! + - to: knative:endpoint/my-endpoint diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-sink.yaml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-sink.yaml new file mode 100644 index 00000000000..fe58b60a7ea --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-sink.yaml @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- from: + uri: timer:tick + steps: + - setBody: + constant: Hello Camel !!! + - to: knative:event/camel-event?name=my-broker diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-source.yaml b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-source.yaml new file mode 100644 index 00000000000..24e1d35cecf --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/knative-event-source.yaml @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- from: + uri: knative:event/camel-event?name=my-broker + steps: + - to: log:info diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloaderRoutesLoader.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloaderRoutesLoader.java index 5539d1642fb..6a81aaa6ecc 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloaderRoutesLoader.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloaderRoutesLoader.java @@ -74,6 +74,10 @@ public class DependencyDownloaderRoutesLoader extends DefaultRoutesLoader { // special for kamelet as we want to track loading kamelets RoutesBuilderLoader loader; if (KameletRoutesBuilderLoader.EXTENSION.equals(extension)) { + if (!downloader.alreadyOnClasspath("org.apache.camel.kamelets", "camel-kamelets-catalog", kameletsVersion)) { + downloader.downloadDependency("org.apache.camel.kamelets", "camel-kamelets-catalog", kameletsVersion); + } + loader = new KnownKameletRoutesBuilderLoader(kameletsVersion); CamelContextAware.trySetCamelContext(loader, getCamelContext()); // allows for custom initialization