Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kamelets: load kamelets definition at build time #2549

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/modules/ROOT/pages/reference/extensions/kamelet.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,46 @@ Or add the coordinates to your existing project:
----

Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.

== Usage

=== Pre-load Kamelets at build-time

This extension allow to pre-load a set of Kamelets at build time through the following two options. The kamelet to pre-load can be listed using the `quarkus.camel.kamelet.names` property.

=== Using the Kamelet Catalog

//TODO: does not yet exist

A set of pre-made Kamelets can be found on the https://camel.apache.org/camel-kamelets/latest[Kamelet Catalog].
To use the Kamelet from the catalog you either need to copy them on your project in the classpath location defined by the `quarkus.camel.kamelet.location` property, or you can add the `camel-quarkus-kamelets-catalog` artifact to your `pom.xml`:

[source,xml]
----
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-kamelets-catalog</artifactId>
</dependency>
----

This artifact makes all the kamelets available in the catalog available to Camel Quarkus for build time processing, the artefact is not part of the runtime classpath.


== Additional Camel Quarkus configuration

[width="100%",cols="80,5,15",options="header"]
|===
| Configuration property | Type | Default


|icon:lock[title=Fixed at build time] [[quarkus.camel.kamelet.names]]`link:#quarkus.camel.kamelet.names[quarkus.camel.kamelet.names]`

List of kamelets names to pre-load at build time.
Each individual name is used to set the related `org.apache.camel.model.RouteTemplateDefinition` id. TODO: find a better name for this configuration option
| `string`
|
|===

[.configuration-legend]
icon:lock[title=Fixed at build time] Configuration property fixed at build time. All other configuration properties are overridable at runtime.

8 changes: 8 additions & 0 deletions extensions/kamelet/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-kamelet</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-yaml-dsl</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,126 @@
*/
package org.apache.camel.quarkus.component.kamelet.deployment;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import org.apache.camel.Ordered;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.dsl.yaml.YamlRoutesBuilderLoader;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.RouteTemplateDefinition;
import org.apache.camel.quarkus.component.kamelet.KameletConfiguration;
import org.apache.camel.quarkus.component.kamelet.KameletRecorder;
import org.apache.camel.quarkus.core.deployment.spi.CamelContextCustomizerBuildItem;
import org.apache.camel.spi.Resource;
import org.apache.camel.support.ResourceHelper;

class KameletProcessor {

private static final String FEATURE = "camel-kamelet";

@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}

@BuildStep
KameletResolverBuildItem defaultResolver() {
return new KameletResolverBuildItem(new KameletResolver() {
@Override
public Optional<InputStream> resolve(String id) throws Exception {
return Optional.ofNullable(
Thread.currentThread().getContextClassLoader()
.getResourceAsStream("/kamelets/" + id + ".kamelet.yaml"));
}
});
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
CamelContextCustomizerBuildItem configureTemplates(
List<KameletResolverBuildItem> resolvers,
KameletConfiguration configuration,
KameletRecorder recorder) throws Exception {

ObjectMapper mapper = new ObjectMapper(new YAMLFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

List<RouteTemplateDefinition> definitions = new ArrayList<>();
List<KameletResolver> kameletResolvers = resolvers.stream()
.map(KameletResolverBuildItem::getResolver)
.sorted(Comparator.comparingInt(Ordered::getOrder))
.collect(Collectors.toList());

try (YamlRoutesBuilderLoader ybl = new YamlRoutesBuilderLoader()) {
ybl.setCamelContext(new DefaultCamelContext());
ybl.start();

for (String name : configuration.names.orElse(Collections.emptyList())) {

for (KameletResolver resolver : kameletResolvers) {
final Optional<InputStream> is = resolver.resolve(name);
if (!is.isPresent()) {
continue;
}

try {
final ObjectNode definition = (ObjectNode) mapper.readTree(is.get());
final JsonNode properties = definition.requiredAt("/spec/definition/properties");
final JsonNode flow = mapper.createArrayNode().add(definition.requiredAt("/spec/flow"));
final Resource res = ResourceHelper.fromBytes(name + ".yaml", mapper.writeValueAsBytes(flow));

RouteTemplateDefinition rt = new RouteTemplateDefinition();
rt.setId(name);

Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
while (it.hasNext()) {
final Map.Entry<String, JsonNode> property = it.next();
final String key = property.getKey();
final JsonNode def = property.getValue().at("/default");

if (def.isMissingNode()) {
rt.templateParameter(key);
} else {
rt.templateParameter(key, def.asText());
}
}

RouteBuilder rb = (RouteBuilder) ybl.loadRoutesBuilder(res);
rb.configure();
if (rb.getRouteCollection().getRoutes().size() != 1) {
throw new IllegalStateException(
"A kamelet is not supposed to create more than one route ("
+ "kamelet:" + name + ","
+ "routes: " + rb.getRouteCollection().getRoutes().size()
+ ")");
}

rt.setRoute(rb.getRouteCollection().getRoutes().get(0));

definitions.add(rt);
} finally {
is.get().close();
}
}
}
}

return new CamelContextCustomizerBuildItem(recorder.createTemplateLoaderCustomizer(definitions));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.apache.camel.quarkus.component.kamelet.deployment;

import java.io.InputStream;
import java.util.Optional;

import org.apache.camel.Ordered;

public interface KameletResolver extends Ordered {
Optional<InputStream> resolve(String id) throws Exception;

@Override
default int getOrder() {
return Ordered.LOWEST;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.apache.camel.quarkus.component.kamelet.deployment;

import io.quarkus.builder.item.MultiBuildItem;

/**
* Build item used by kamelet providers to plug their own way of resolving kamelets giving a name. This could be
* leveraged by a future camel-quarkus-kamelet-catalog extension to resolve kamelets as they may have a different naming
* structure or location int the classpath.
*/
public final class KameletResolverBuildItem extends MultiBuildItem {
private final KameletResolver resolver;

public KameletResolverBuildItem(KameletResolver resolver) {
this.resolver = resolver;
}

public KameletResolver getResolver() {
return this.resolver;
}
}
20 changes: 20 additions & 0 deletions extensions/kamelet/runtime/src/main/doc/usage.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
=== Pre-load Kamelets at build-time

This extension allow to pre-load a set of Kamelets at build time through the following two options. The kamelet to pre-load can be listed using the `quarkus.camel.kamelet.names` property.

=== Using the Kamelet Catalog

//TODO: does not yet exist

A set of pre-made Kamelets can be found on the https://camel.apache.org/camel-kamelets/latest[Kamelet Catalog].
To use the Kamelet from the catalog you either need to copy them on your project in the classpath location defined by the `quarkus.camel.kamelet.location` property, or you can add the `camel-quarkus-kamelets-catalog` artifact to your `pom.xml`:

[source,xml]
----
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-kamelets-catalog</artifactId>
</dependency>
----

This artifact makes all the kamelets available in the catalog available to Camel Quarkus for build time processing, the artefact is not part of the runtime classpath.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.quarkus.component.kamelet;

import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "camel.kamelet", phase = ConfigPhase.BUILD_TIME)
public class KameletConfiguration {
/**
* List of kamelets names to pre-load at build time.
*
* <p/>
* Each individual name is used to set the related {@link org.apache.camel.model.RouteTemplateDefinition} id.
* TODO: find a better name for this configuration option
*/
@ConfigItem
public Optional<List<String>> names;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.quarkus.component.kamelet;

import java.util.List;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import org.apache.camel.CamelContext;
import org.apache.camel.model.Model;
import org.apache.camel.model.RouteTemplateDefinition;
import org.apache.camel.quarkus.core.CamelContextCustomizer;

@Recorder
public class KameletRecorder {
public RuntimeValue<CamelContextCustomizer> createTemplateLoaderCustomizer(List<RouteTemplateDefinition> definitions) {
return new RuntimeValue<>(new CamelContextCustomizer() {
@Override
public void customize(CamelContext context) {
try {
context.getExtension(Model.class).addRouteTemplateDefinitions(definitions);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
4 changes: 4 additions & 0 deletions integration-tests/kamelet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>

<!-- test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,28 @@
package org.apache.camel.quarkus.component.kamelet.it;

import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.apache.camel.CamelContext;
import org.apache.camel.ConsumerTemplate;
import org.apache.camel.FluentProducerTemplate;
import org.apache.camel.model.Model;
import org.apache.camel.model.OptionalIdentifiedDefinition;

@Path("/kamelet")
public class KameletResource {

@Inject
CamelContext camelContext;

@Inject
FluentProducerTemplate fluentProducerTemplate;

Expand Down Expand Up @@ -64,4 +73,19 @@ public String bodyFromProperty() throws Exception {
public String kameletChain(String message) throws Exception {
return fluentProducerTemplate.to("direct:chain").withBody(message).request(String.class);
}

@Path("/list")
@GET
@Produces(MediaType.TEXT_PLAIN)
public JsonArray list() {
JsonArrayBuilder builder = Json.createArrayBuilder();

camelContext.getExtension(Model.class)
.getRouteTemplateDefinitions()
.stream()
.map(OptionalIdentifiedDefinition::getId)
.forEach(builder::add);

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
## limitations under the License.
## ---------------------------------------------------------------------------
camel.kamelet.setBodyFromProperties.bodyValueFromProperty=Camel Quarkus Kamelet Property

quarkus.camel.kamelet.names = injector,logger
Loading