You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gf...@apache.org on 2024/08/12 19:50:16 UTC
(camel) 01/02: CAMEL-21000: camel-jbang Kubernetes - Add Ingress trait
This is an automated email from the ASF dual-hosted git repository.
gfournier pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 0de09915ac2703681d2db4cc14d7416b044ecbe2
Author: Gaelle Fournier <ga...@gmail.com>
AuthorDate: Wed Jul 31 18:05:27 2024 +0200
CAMEL-21000: camel-jbang Kubernetes - Add Ingress trait
* Implement Ingress trait that is capable of configuring the Kubenetes Ingress resource
* Fix parsing of annotations in traits
---
.../modules/ROOT/pages/camel-jbang-kubernetes.adoc | 89 +++++++++++++++++
.../commands/kubernetes/traits/IngressTrait.java | 105 +++++++++++++++++++++
.../commands/kubernetes/traits/TraitCatalog.java | 1 +
.../commands/kubernetes/traits/TraitContext.java | 8 ++
.../commands/kubernetes/traits/TraitHelper.java | 27 +++++-
.../commands/kubernetes/KubernetesCommandTest.java | 2 +-
.../commands/kubernetes/KubernetesExportTest.java | 46 +++++++++
.../commands/kubernetes/KubernetesRunTest.java | 2 +-
8 files changed, 277 insertions(+), 3 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 59e5bb0e3cf..160199158c2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc
@@ -908,6 +908,95 @@ spec:
The trait volume mounts follow some best practices in specifying the mount paths in the container. Configurations and resources, secrets and configmaps do use different paths in the container. The Camel application is automatically configured to read these paths as resource folders, so you can use the mounted data in the Camel routes via classpath reference for instance.
+=== Ingress trait options
+
+The ingress trait enhance the Kubernetes manifest with an Ingress resource to expose the application to the outside world. This requires the presence in the Kubernetes manifest of a Service Resource.
+
+The Camel JBang plugin automatically creates an Ingress Resource if the Service Resource is generated for the Camel route's service trait application.
+
+The ingress trait provides the following configuration options:
+
+[cols="2m,1m,5a"]
+|===
+|Property | Type | Description
+
+| ingress.enabled
+| bool
+| Can be used to enable or disable a trait. All traits share this common property (default `true`).
+
+| ingress.annotations
+| map[string]string
+| The annotations added to the ingress. This can be used to set controller specific annotations, e.g., when using the https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md[NGINX Ingress controller].
+
+| ingress.host
+| string
+| To configure the host exposed by the ingress.
+
+| ingress.path
+| string
+| To configure the path exposed by the ingress (default `/`).
+
+| ingress.pathType
+| string
+| To configure the https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types[path type] exposed by the ingress. One of Exact, Prefix, ImplementationSpecific (default to Prefix).
+
+| ingress.auto
+| bool
+| To automatically add an Ingress Resource whenever the route uses an HTTP endpoint consumer (default `true`).
+
+|===
+
+he syntax to specify container trait options is as follows:
+
+[source,bash]
+----
+camel kubernetes export Sample.java --trait ingress.[key]=[value]
+----
+
+You may specify these options with the export command to customize the Ingress Resource specification.
+
+[source,bash]
+----
+camel kubernetes export Sample.java --trait ingress.host=example.com --trait ingress.path=/sample(/|$)(.*) --trait ingress.pathType=ImplementationSpecific --trait ingress.annotations=nginx.ingress.kubernetes.io/rewrite-target=/\$2 --trait ingress.annotations=nginx.ingress.kubernetes.io/use-regex=true
+----
+
+
+This results in the following container specification in the Ingress resource.
+
+[source,yaml]
+----
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations: #<1>
+ nginx.ingress.kubernetes.io/rewrite-target: /$2
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ labels:
+ app.kubernetes.io/name: sample
+ name: sample
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - backend:
+ service:
+ name: route-service
+ port:
+ name: http #<2>
+ path: /sample(/|$)(.*) #<3>
+ pathType: ImplementationSpecific #<4>
+
+
+----
+<1> Custom annotations configuration for ingress behavior
+<2> Service port name
+<3> Custom ingress backend path
+<4> Custom ingress backend path type
+
+
+
=== OpenApi specifications
You can mount OpenAPI specifications to the application container with this trait.
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/IngressTrait.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/IngressTrait.java
new file mode 100644
index 00000000000..0d2eb4e49e5
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/traits/IngressTrait.java
@@ -0,0 +1,105 @@
+/*
+ * 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 io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPathBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRule;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRuleBuilder;
+import org.apache.camel.v1.integrationspec.Traits;
+import org.apache.camel.v1.integrationspec.traits.Container;
+import org.apache.camel.v1.integrationspec.traits.Ingress;
+
+import static org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.ContainerTrait.DEFAULT_CONTAINER_PORT_NAME;
+
+public class IngressTrait extends BaseTrait {
+
+ public static final int IngressTrait = 2400;
+
+ public static final String INGRESS_CLASS_NAME = "nginx";
+ public static final String DEFAULT_INGRESS_HOST = "";
+ public static final String DEFAULT_INGRESS_PATH = "/";
+ public static final Ingress.PathType DEFAULT_INGRESS_PATH_TYPE = Ingress.PathType.PREFIX;
+
+ public IngressTrait() {
+ super("ingress", IngressTrait);
+ }
+
+ @Override
+ public boolean configure(Traits traitConfig, TraitContext context) {
+ if (context.getIngress().isPresent()) {
+ return false;
+ }
+
+ // explicitly disabled
+ if (traitConfig.getIngress() != null && !Optional.ofNullable(traitConfig.getIngress().getEnabled()).orElse(true)) {
+ return false;
+ }
+
+ // auto configuration with configured service
+ if (traitConfig.getIngress() != null && Optional.ofNullable(traitConfig.getIngress().getAuto()).orElse(true)) {
+ return context.getService() != null;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void apply(Traits traitConfig, TraitContext context) {
+ Ingress ingressTrait = Optional.ofNullable(traitConfig.getIngress()).orElseGet(Ingress::new);
+ Container containerTrait = Optional.ofNullable(traitConfig.getContainer()).orElseGet(Container::new);
+
+ IngressBuilder ingressBuilder = new IngressBuilder();
+ ingressBuilder.withNewMetadata()
+ .withName(context.getName())
+ .endMetadata();
+ if (ingressTrait.getAnnotations() != null) {
+ ingressBuilder.editMetadata().withAnnotations(ingressTrait.getAnnotations()).endMetadata();
+ }
+
+ HTTPIngressPath path = new HTTPIngressPathBuilder()
+ .withPath(Optional.ofNullable(ingressTrait.getPath()).orElse(DEFAULT_INGRESS_PATH))
+ .withPathType(Optional.ofNullable(ingressTrait.getPathType()).orElse(DEFAULT_INGRESS_PATH_TYPE).getValue())
+ .withNewBackend()
+ .withNewService()
+ .withName(context.getName())
+ .withNewPort()
+ .withName(Optional.ofNullable(containerTrait.getServicePortName()).orElse(DEFAULT_CONTAINER_PORT_NAME))
+ .endPort()
+ .endService()
+ .endBackend()
+ .build();
+
+ IngressRule rule = new IngressRuleBuilder()
+ .withHost(Optional.ofNullable(ingressTrait.getHost()).orElse(DEFAULT_INGRESS_HOST))
+ .withNewHttp()
+ .withPaths(path)
+ .endHttp()
+ .build();
+
+ ingressBuilder
+ .withNewSpec()
+ .withIngressClassName(INGRESS_CLASS_NAME)
+ .withRules(rule)
+ .endSpec();
+
+ context.add(ingressBuilder);
+ }
+}
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 01c00630b31..339d9a9fb0b 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
@@ -40,6 +40,7 @@ public class TraitCatalog {
register(new KnativeTrait());
register(new KnativeServiceTrait());
register(new ServiceTrait());
+ register(new IngressTrait());
register(new ContainerTrait());
register(new EnvTrait());
register(new MountTrait());
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 6607d93ae1c..5585e2188a3 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
@@ -35,6 +35,7 @@ import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.CatalogHelper;
@@ -134,6 +135,13 @@ public class TraitContext {
.findFirst();
}
+ public Optional<IngressBuilder> getIngress() {
+ return resourceRegistry.stream()
+ .filter(it -> it.getClass().isAssignableFrom(IngressBuilder.class))
+ .map(it -> (IngressBuilder) it)
+ .findFirst();
+ }
+
public Optional<io.fabric8.knative.serving.v1.ServiceBuilder> getKnativeService() {
return resourceRegistry.stream()
.filter(it -> it.getClass().isAssignableFrom(io.fabric8.knative.serving.v1.ServiceBuilder.class))
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 11f8b90b949..d4382d6f68e 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
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -91,12 +92,36 @@ public final class TraitHelper {
} else {
values.add(traitValue.toString());
}
+ } else if (existingValue instanceof Map) {
+ Map<String, String> values = (Map<String, String>) existingValue;
+ if (traitValue instanceof Map) {
+ Map<String, String> traitValueList = (Map<String, String>) traitValue;
+ values.putAll(traitValueList);
+ } else {
+ final String[] traitValueConfig = traitValue.toString().split("=", 2);
+ values.put(traitValueConfig[0], traitValueConfig[1]);
+ }
} else if (traitValue instanceof List) {
List<String> traitValueList = (List<String>) traitValue;
traitValueList.add(0, existingValue.toString());
config.put(traitKey, traitValueList);
+ } else if (traitValue instanceof Map) {
+ Map<String, String> traitValueMap = (Map<String, String>) traitValue;
+ final String[] existingValueConfig = existingValue.toString().split("=", 2);
+ traitValueMap.put(existingValueConfig[0], existingValueConfig[1]);
+ config.put(traitKey, traitValueMap);
} else {
- config.put(traitKey, Arrays.asList(existingValue.toString(), traitValue));
+ if (traitKey.endsWith("annotations")) {
+ System.out.println("annotations");
+ Map<String, String> map = new LinkedHashMap<>();
+ final String[] traitValueConfig = traitValue.toString().split("=", 2);
+ final String[] existingValueConfig = existingValue.toString().split("=", 2);
+ map.put(traitValueConfig[0], traitValueConfig[1]);
+ map.put(existingValueConfig[0], existingValueConfig[1]);
+ config.put(traitKey, map);
+ } else {
+ config.put(traitKey, Arrays.asList(existingValue.toString(), traitValue));
+ }
}
} else {
config.put(traitKey, traitValue);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java
index ce6dc58ca84..a7ef0113f0a 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesCommandTest.java
@@ -45,7 +45,7 @@ class KubernetesCommandTest extends KubernetesBaseTest {
"yaml");
List<HasMetadata> resources = kubernetesClient.load(getKubernetesManifestAsStream(printer.getOutput())).items();
- Assertions.assertEquals(2, resources.size());
+ Assertions.assertEquals(3, resources.size());
Deployment deployment = resources.stream()
.filter(it -> Deployment.class.isAssignableFrom(it.getClass()))
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 a918d215cfa..5de16087997 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
@@ -40,6 +40,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
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;
@@ -207,6 +208,46 @@ class KubernetesExportTest extends KubernetesBaseTest {
Assertions.assertEquals("http", ports.get(0).getTargetPort().getStrVal());
}
+ @ParameterizedTest
+ @MethodSource("runtimeProvider")
+ public void shouldAddIngressSpec(RuntimeType rt) throws Exception {
+ KubernetesExport command = createCommand(new String[] { "classpath:route-service.yaml" },
+ "--trait", "ingress.host=example.com",
+ "--trait", "ingress.path=/something(/|$)(.*)",
+ "--trait", "ingress.pathType=ImplementationSpecific",
+ "--trait", "ingress.annotations=nginx.ingress.kubernetes.io/rewrite-target=/$2",
+ "--trait", "ingress.annotations=nginx.ingress.kubernetes.io/use-regex=true",
+ "--runtime=" + rt.runtime());
+ command.doCall();
+
+ Assertions.assertTrue(hasService(rt));
+ Assertions.assertFalse(hasKnativeService(rt));
+
+ Deployment deployment = getDeployment(rt);
+ Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
+ Assertions.assertEquals("route-service", deployment.getMetadata().getName());
+ Assertions.assertEquals(1, deployment.getSpec().getTemplate().getSpec().getContainers().size());
+ Assertions.assertEquals("route-service:1.0-SNAPSHOT", container.getImage());
+ Assertions.assertEquals(1, container.getPorts().size());
+ Assertions.assertEquals("http", container.getPorts().get(0).getName());
+ Assertions.assertEquals(8080, container.getPorts().get(0).getContainerPort());
+
+ Ingress ingress = getIngress(rt);
+ Assertions.assertEquals("route-service", ingress.getMetadata().getName());
+ Assertions.assertEquals("example.com", ingress.getSpec().getRules().get(0).getHost());
+ Assertions.assertEquals("/something(/|$)(.*)",
+ ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getPath());
+ Assertions.assertEquals("ImplementationSpecific",
+ ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getPathType());
+ Assertions.assertEquals("route-service",
+ ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getBackend().getService().getName());
+ Assertions.assertEquals("http",
+ ingress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getBackend().getService().getPort().getName());
+ Assertions.assertEquals("/$2",
+ ingress.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/rewrite-target"));
+ Assertions.assertEquals("true", ingress.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/use-regex"));
+ }
+
@ParameterizedTest
@MethodSource("runtimeProvider")
public void shouldAddContainerSpec(RuntimeType rt) throws Exception {
@@ -684,6 +725,11 @@ class KubernetesExportTest extends KubernetesBaseTest {
.orElseThrow(() -> new RuntimeCamelException("Cannot find service for: %s".formatted(rt.runtime())));
}
+ private Ingress getIngress(RuntimeType rt) throws IOException {
+ return getResource(rt, Ingress.class)
+ .orElseThrow(() -> new RuntimeCamelException("Cannot find ingress for: %s".formatted(rt.runtime())));
+ }
+
private boolean hasService(RuntimeType rt) throws IOException {
return getResource(rt, Service.class).isPresent();
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunTest.java
index b2d7f934765..89411e099f8 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunTest.java
@@ -58,7 +58,7 @@ class KubernetesRunTest extends KubernetesBaseTest {
Assertions.assertEquals(0, exit);
List<HasMetadata> resources = kubernetesClient.load(getKubernetesManifestAsStream(printer.getOutput())).items();
- Assertions.assertEquals(2, resources.size());
+ Assertions.assertEquals(3, resources.size());
Deployment deployment = resources.stream()
.filter(it -> Deployment.class.isAssignableFrom(it.getClass()))