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
The following commit(s) were added to refs/heads/main by this push:
new e63c90106e3 CAMEL-21710 camel-jbang-plugin-kubernetes - automatic
cluster detection
e63c90106e3 is described below
commit e63c90106e394af22d6c17a655e04dd4f780fbd4
Author: Claudio Miranda <[email protected]>
AuthorDate: Thu Feb 6 23:38:14 2025 +0000
CAMEL-21710 camel-jbang-plugin-kubernetes - automatic cluster detection
Set parameters when connected to specific cluster
---
.../modules/ROOT/pages/camel-jbang-kubernetes.adoc | 4 +-
.../camel-jbang-plugin-kubernetes/pom.xml | 11 ++
.../core/commands/kubernetes/KubernetesHelper.java | 77 +++++++--
.../core/commands/kubernetes/KubernetesRun.java | 22 ++-
.../commands/kubernetes/KubernetesBaseTest.java | 6 +-
.../kubernetes/KubernetesRunCustomTest.java | 174 +++++++++++++++++++++
.../src/test/resources/log4j2.properties | 29 ++++
7 files changed, 305 insertions(+), 18 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 5048920b902..a5f3dd6665d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-kubernetes.adoc
@@ -1296,7 +1296,9 @@ camel kubernetes run route.yaml --image-registry=kind
----
When connecting to a local Kubernetes cluster, you may need to specify the
image registry where the application container image gets pushed to.
-The run command is able to automatically configure the local registry when
using predefined names such as `kind` or `minikube`.
+The run command is able to automatically configure the local registry when
using predefined names such as `kind` or `minikube`, there is a detection
mechanism to set some properties accordingly to the cluster type, currently it
can auto detect Openshift and Minikube. If you want to disable the cluster
detection, you have to set the `--disable-auto` cli parameter.
+
+NOTE: When running minikube, the easiest way to push the image to the minikube
container registry, is to enable the registry addon in minikube and to run
`eval $(minikube docker-env)`.
Use the `--image-group` or the `--image` option to customize the container
image.
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml
index 6452bb1d042..5ade3566b5c 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/pom.xml
@@ -86,6 +86,12 @@
<scope>test</scope>
<type>test-jar</type>
</dependency>
+ <dependency>
+ <groupId>org.junit-pioneer</groupId>
+ <artifactId>junit-pioneer</artifactId>
+ <version>${junit-pioneer-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>io.fabric8</groupId>
@@ -107,6 +113,11 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
+ <!-- This is required for the @SetEnvironmentVariable
usage of junit-pioneer in KubernetesRunCustomTest -->
+ <argLine>
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ </argLine>
<systemPropertyVariables>
<!-- Used in unit tests to resolve dependencies via
Maven downloader -->
<camel.version>${project.version}</camel.version>
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java
index 17c87f0a1a4..caaee6e85f6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesHelper.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.kubernetes;
import java.io.File;
import java.io.FileNotFoundException;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -31,10 +32,12 @@ import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
+import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
+import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
-import org.apache.camel.dsl.jbang.core.commands.CommandHelper;
+import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext;
import org.apache.camel.dsl.jbang.core.common.YamlHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.StringHelper;
@@ -76,7 +79,6 @@ public final class KubernetesHelper {
public static KubernetesClient getKubernetesClient() {
if (kubernetesClient == null) {
kubernetesClient = new KubernetesClientBuilder().build();
- printClientInfo(kubernetesClient);
}
return kubernetesClient;
@@ -91,19 +93,9 @@ public final class KubernetesHelper {
}
var client = new KubernetesClientBuilder().withConfig(config).build();
- printClientInfo(client);
return clients.put(config, client);
}
- private static void printClientInfo(KubernetesClient client) {
- var printer = CommandHelper.getPrinter();
- if (printer != null) {
- var serverUrl = client.getConfiguration().getMasterUrl();
- var info = client.getKubernetesVersion();
- printer.println(String.format("Kubernetes v%s.%s %s",
info.getMajor(), info.getMinor(), serverUrl));
- }
- }
-
/**
* Creates new Yaml instance. The implementation provided by Snakeyaml is
not thread-safe. It is better to create a
* fresh instance for every YAML stream.
@@ -125,6 +117,67 @@ public final class KubernetesHelper {
return OBJECT_MAPPER;
}
+ /**
+ * Verify what cluster this shell console is connected to, currently it
tests for Openshift and Minikube, in case of
+ * errors or no connected cluster, it defaults to Kubernetes.
+ *
+ * @return The cluster type may be: Openshift, Minikube or Kubernetes.
+ */
+ static ClusterType discoverClusterType() {
+ ClusterType cluster = ClusterType.KUBERNETES;
+ // in case it's not connected to a cluster, don't waste time waiting
for a response.
+ System.setProperty("kubernetes.connection.timeout", "2000");
+ System.setProperty("kubernetes.request.timeout", "2000");
+ System.setProperty("kubernetes.request.retry.backoffLimit", "1");
+ if (isConnectedToOpenshift()) {
+ cluster = ClusterType.OPENSHIFT;
+ } else if (isConnectedToMinikube()) {
+ cluster = ClusterType.MINIKUBE;
+ }
+ return cluster;
+ }
+
+ private static boolean isConnectedToOpenshift() {
+ boolean ocp = false;
+ try {
+ // set to openshift if there is
clusterversions.config.openshift.io/version
+ ResourceDefinitionContext ocpVersion = new
ResourceDefinitionContext.Builder()
+ .withGroup("config.openshift.io")
+ .withVersion("v1")
+ .withKind("ClusterVersion")
+ .withNamespaced(false)
+ .build();
+ GenericKubernetesResource versioncr
+ =
getKubernetesClient().genericKubernetesResources(ocpVersion).withName("version").get();
+ ocp = versioncr != null;
+ } catch (RuntimeException e) {
+ // ignore it, since we try to discover the cluster and don't want
the caller to handle any error
+ }
+ return ocp;
+ }
+
+ private static boolean isConnectedToMinikube() {
+ boolean minikube = false;
+ try {
+ ResourceDefinitionContext nodecrd = new
ResourceDefinitionContext.Builder()
+ .withVersion("v1")
+ .withKind("Node")
+ .withNamespaced(false)
+ .build();
+ // if there is a node with minikube label, then it's minikube
+ GenericKubernetesResourceList list =
getKubernetesClient().genericKubernetesResources(nodecrd)
+
.withLabels(Collections.singletonMap("minikube.k8s.io/name", null)).list();
+ minikube = list.getItems().size() > 0;
+ // thse env properties are set when running eval $(minikube
docker-env) in the console
+ // this is important for the docker builder to actually build the
image in the exposed docker from the minikube registry
+ minikube = minikube && System.getenv("MINIKUBE_ACTIVE_DOCKERD") !=
null
+ && System.getenv("DOCKER_TLS_VERIFY") != null;
+ } catch (Exception e) {
+ // ignore it, since we try to discover the cluster and don't want
the caller to handle any error
+ }
+ return minikube;
+ }
+
/**
* Sanitize given name to meet Kubernetes resource naming requirements.
*
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
index 18e5ba74980..c5246b6a723 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
@@ -257,6 +257,9 @@ public class KubernetesRun extends KubernetesBaseCommand {
description = "Maven/Gradle build properties, ex.
--build-property=prop1=foo")
List<String> buildProperties = new ArrayList<>();
+ @CommandLine.Option(names = { "--disable-auto" }, description = "Disable
automatic cluster type detection.")
+ boolean disableAuto = false;
+
// DevMode/Reload state
private CamelContext devModeContext;
private Thread devModeShutdownTask;
@@ -350,6 +353,7 @@ public class KubernetesRun extends KubernetesBaseCommand {
}
private KubernetesExport configureExport(String workingDir) {
+ detectCluster();
KubernetesExport.ExportConfigurer configurer = new
KubernetesExport.ExportConfigurer(
runtime,
quarkusVersion,
@@ -664,6 +668,23 @@ public class KubernetesRun extends KubernetesBaseCommand {
return 0;
}
+ private void detectCluster() {
+ if (!disableAuto) {
+ if (verbose) {
+ printer().print("Automatic kubernetes cluster detection... ");
+ }
+ ClusterType cluster = KubernetesHelper.discoverClusterType();
+ this.clusterType = cluster.name();
+ if (ClusterType.MINIKUBE == cluster) {
+ this.imageBuilder = "docker";
+ this.imagePush = false;
+ }
+ if (verbose) {
+ printer().println(this.clusterType);
+ }
+ }
+ }
+
@Override
protected Printer printer() {
if (quiet || output != null) {
@@ -674,7 +695,6 @@ public class KubernetesRun extends KubernetesBaseCommand {
CommandHelper.setPrinter(quietPrinter);
return quietPrinter;
}
-
return super.printer();
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseTest.java
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseTest.java
index 20473c84d58..8f6973ff6f2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesBaseTest.java
@@ -42,9 +42,7 @@ import org.junit.jupiter.api.TestInstance;
public class KubernetesBaseTest {
private KubernetesMockServer k8sServer;
-
protected KubernetesClient kubernetesClient;
-
protected StringPrinter printer;
public static boolean isDockerAvailable() {
@@ -74,11 +72,11 @@ public class KubernetesBaseTest {
k8sServer.destroy();
}
- protected InputStream getKubernetesManifestAsStream(String printerOutput) {
+ protected static InputStream getKubernetesManifestAsStream(String
printerOutput) {
return getKubernetesManifestAsStream(printerOutput, "yaml");
}
- protected InputStream getKubernetesManifestAsStream(String printerOutput,
String output) {
+ protected static InputStream getKubernetesManifestAsStream(String
printerOutput, String output) {
if (output.equals("yaml")) {
String manifest = StringHelper.after(printerOutput, "---");
if (manifest == null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunCustomTest.java
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunCustomTest.java
new file mode 100644
index 00000000000..ad524b3aa3c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRunCustomTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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;
+
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Node;
+import io.fabric8.kubernetes.api.model.NodeBuilder;
+import io.fabric8.kubernetes.api.model.NodeList;
+import io.fabric8.kubernetes.api.model.NodeListBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
+import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
+import io.fabric8.openshift.api.model.config.v1.ClusterVersion;
+import io.fabric8.openshift.api.model.config.v1.ClusterVersionBuilder;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+import picocli.CommandLine;
+
+/* This test class doesn't use the KubernetesRunTest since we had to use the
@EnableKubernetesMockClient
+ * annotation which works differently from KubernetesBaseTest manual creation
of the KubernetesMockServer
+ * and it seems the @TestInstance(TestInstance.Lifecycle.PER_CLASS) interferes
with the way the
+ * @EnableKubernetesMockClient works. In another work we can plan to tackle
this test inheritance mechanism.
+ *
+ */
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+ disabledReason = "Requires too much network
resources")
+@EnableKubernetesMockClient
+class KubernetesRunCustomTest {
+
+ protected KubernetesMockServer server;
+ protected KubernetesClient client;
+ protected StringPrinter printer;
+
+ @BeforeEach
+ public void setup() {
+ // Set Camel version with system property value, usually set via Maven
surefire plugin
+ // In case you run this test via local Java IDE you need to provide
the system property or a default value here
+ VersionHelper.setCamelVersion(System.getProperty("camel.version", ""));
+ printer = new StringPrinter();
+ }
+
+ @Test
+ public void disableAutomaticClusterDetection() throws Exception {
+ KubernetesHelper.setKubernetesClient(client);
+ ClusterVersion versionCR = new ClusterVersionBuilder()
+ .withNewMetadata().withName("version").endMetadata()
+ .withNewStatus()
+ .withNewDesired()
+ .withVersion("4.14.5")
+ .endDesired()
+ .endStatus()
+ .build();
+
+
server.expect().get().withPath("/apis/config.openshift.io/v1/clusterversions/version")
+ .andReturn(HttpURLConnection.HTTP_OK, versionCR)
+ .once();
+
+ KubernetesRun command = createCommand(new String[] {
"classpath:route.yaml" },
+ "--image-registry=quay.io", "--image-group=camel-test",
"--output=yaml",
+ "--disable-auto");
+ int exit = command.doCall();
+
+ Assertions.assertEquals(0, exit);
+ Assertions.assertEquals(ClusterType.KUBERNETES.name().toLowerCase(),
command.clusterType.toLowerCase());
+
+ var manifest =
KubernetesBaseTest.getKubernetesManifestAsStream(printer.getOutput(),
command.output);
+ List<HasMetadata> resources = client.load(manifest).items();
+ // expects Service, Deployment manifests in kubernetes.yml
+ Assertions.assertEquals(2, resources.size());
+ }
+
+ @Test
+ public void detectOpenshiftCluster() throws Exception {
+ KubernetesHelper.setKubernetesClient(client);
+ ClusterVersion versionCR = new ClusterVersionBuilder()
+ .withNewMetadata().withName("version").endMetadata()
+ .withNewStatus()
+ .withNewDesired()
+ .withVersion("4.14.5")
+ .endDesired()
+ .endStatus()
+ .build();
+
+
server.expect().get().withPath("/apis/config.openshift.io/v1/clusterversions/version")
+ .andReturn(HttpURLConnection.HTTP_OK, versionCR)
+ .once();
+
+ KubernetesRun command = createCommand(new String[] {
"classpath:route.yaml" },
+ "--image-registry=quay.io", "--image-group=camel-test",
"--output=yaml");
+ int exit = command.doCall();
+
+ Assertions.assertEquals(0, exit);
+ Assertions.assertEquals(ClusterType.OPENSHIFT.name().toLowerCase(),
command.clusterType.toLowerCase());
+
+ var manifest =
KubernetesBaseTest.getKubernetesManifestAsStream(printer.getOutput(),
command.output);
+ List<HasMetadata> resources = client.load(manifest).items();
+ // expects Service, Deployment, Route manifests in openshift.yml
+ Assertions.assertEquals(3, resources.size());
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "MINIKUBE_ACTIVE_DOCKERD", value = "foo")
+ @SetEnvironmentVariable(key = "DOCKER_TLS_VERIFY", value = "foo")
+ public void detectMinikubeCluster() throws Exception {
+ KubernetesHelper.setKubernetesClient(client);
+ Node nodeCR = new NodeBuilder()
+ .withNewMetadata()
+ .withName("minikube")
+ .withLabels(Collections.singletonMap("minikube.k8s.io/name",
"minikube"))
+ .endMetadata()
+ .build();
+ NodeList nodeList = new NodeListBuilder().addToItems(nodeCR)
+ .build();
+
+
server.expect().get().withPath("/api/v1/nodes?labelSelector=minikube.k8s.io%2Fname")
+ .andReturn(HttpURLConnection.HTTP_OK, nodeList)
+ .once();
+
+ KubernetesRun command = createCommand(new String[] {
"classpath:route.yaml" },
+ "--image-registry=quay.io", "--image-group=camel-test",
"--output=yaml");
+ int exit = command.doCall();
+
+ Assertions.assertEquals(0, exit);
+ Assertions.assertEquals(ClusterType.MINIKUBE.name().toLowerCase(),
command.clusterType.toLowerCase());
+
+ var manifest =
KubernetesBaseTest.getKubernetesManifestAsStream(printer.getOutput(),
command.output);
+ List<HasMetadata> resources = client.load(manifest).items();
+ // expects Service, Deployment manifests in kubernetes.yml
+ Assertions.assertEquals(2, resources.size());
+
+ Assertions.assertFalse(command.imagePush);
+ Assertions.assertEquals("docker", command.imageBuilder);
+ }
+
+ private KubernetesRun createCommand(String[] files, String... args) {
+ var argsArr = Optional.ofNullable(args).orElse(new String[0]);
+ var argsLst = new ArrayList<>(Arrays.asList(argsArr));
+ var jbangMain = new CamelJBangMain().withPrinter(printer);
+ KubernetesRun command = new KubernetesRun(jbangMain, files);
+ CommandLine.populateCommand(command, argsLst.toArray(new String[0]));
+ command.imageBuild = false;
+ command.imagePush = false;
+ return command;
+ }
+
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/log4j2.properties
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/log4j2.properties
new file mode 100644
index 00000000000..b7f5b1e0437
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/resources/log4j2.properties
@@ -0,0 +1,29 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+# console logger
+appender.stdout.type = Console
+appender.stdout.name = out
+appender.stdout.layout.type = PatternLayout
+appender.stdout.layout.pattern = %style{%d{yyyy-MM-dd HH:mm:ss.SSS}}
%style{%c}{Cyan}: %m%n
+
+# log to console
+rootLogger = INFO,out
+# rootLogger.level = DEBUG
+
+# logger.fabric8.name=io.fabric8
+# logger.fabric8.level=DEBUG