You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/16 05:52:47 UTC
(camel) branch main updated: CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)
This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 7089992f4a97 CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)
7089992f4a97 is described below
commit 7089992f4a977a51eac52f95e67c4f0c5f29edb7
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Oct 16 07:52:39 2026 +0200
CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)
- harden --url that cannot be reached exits 1, instead of sending the route
to a local Ollama.
- export does not clean the current directory, or one that contains it, as
the export directory without --clean-dir: ./, $PWD and .. compared as
text with "." and deleted the files of the project.
- doc <name> looks in the catalog of Camel before it downloads the Kamelet
catalog (works offline), catalog kamelet uses --repos, and --sort on a
row without a description or support level no longer fails.
- version get shows a configuration with only the kamelets version,
version set --reset removes it too, and version list exits 1 when the
versions cannot be downloaded.
- sbom: an unknown --sbom-format or --sbom-output-format exits 2 before
exporting (it exited 0 doing nothing), and Maven not finishing in time
exits 124 with a message.
- update run applies --upgradeTimeout (the output was read to the end
before the wait) and exits 124 when it is reached.
- debug does not run the jar of a Spring Boot or Quarkus build that failed.
- validate yaml/properties with no file to validate exits 3.
- kubernetes logs exits 3 when no pod shows up and 1 when reading fails,
kubernetes run prints the end of the Maven output when the build or
deployment fails (it was hidden without --verbose), and a pod that is not
running after 10 minutes exits 124.
- --jvm-args keeps a quoted value with spaces as one argument.
camel kubernetes run removes the Maven log once Maven has ended.
Co-authored-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 21 ++++
.../camel-jbang-catalog-kamelet.adoc | 3 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../camel/dsl/jbang/core/commands/Debug.java | 14 ++-
.../dsl/jbang/core/commands/ExportBaseCommand.java | 10 ++
.../dsl/jbang/core/commands/ExportCamelMain.java | 2 +-
.../dsl/jbang/core/commands/ExportQuarkus.java | 2 +-
.../dsl/jbang/core/commands/ExportSpringBoot.java | 2 +-
.../camel/dsl/jbang/core/commands/Harden.java | 7 +-
.../apache/camel/dsl/jbang/core/commands/Run.java | 2 +-
.../dsl/jbang/core/commands/SBOMGenerator.java | 14 ++-
.../core/commands/catalog/CatalogBaseCommand.java | 9 +-
.../jbang/core/commands/catalog/CatalogDoc.java | 29 +++---
.../core/commands/catalog/CatalogKamelet.java | 36 ++-----
.../dsl/jbang/core/commands/update/UpdateRun.java | 35 +++----
.../jbang/core/commands/version/VersionGet.java | 2 +-
.../jbang/core/commands/version/VersionList.java | 5 +-
.../jbang/core/commands/version/VersionSet.java | 1 +
.../dsl/jbang/core/common/CommandLineHelper.java | 36 +++++++
.../core/commands/CommandInputErrorsTest.java | 30 ++++++
.../core/commands/catalog/CatalogDocTest.java | 12 +++
.../core/commands/catalog/CatalogKameletTest.java | 12 +++
.../core/commands/version/VersionGetTest.java | 15 +++
.../core/commands/version/VersionSetTest.java | 4 +-
.../core/common/CommandLineArgumentsTest.java | 50 ++++++++++
.../commands/kubernetes/KubernetesPodLogs.java | 18 +++-
.../core/commands/kubernetes/KubernetesRun.java | 107 +++++++++++++++------
.../commands/kubernetes/KubernetesPodLogsTest.java | 7 +-
.../validate/PropertiesValidateCommand.java | 4 +
.../commands/validate/YamlValidateCommand.java | 4 +
.../core/commands/validate/ValidateJsonTest.java | 21 +++-
31 files changed, 401 insertions(+), 115 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 5ee9c372ca85..51bc0359f6c2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -425,6 +425,27 @@ or follows, like Enter does.
answered at once), and `camel_control stop-routes` and `start-routes` answer with what was done instead of the
answer of the integration.
+=== camel-jbang - exit codes and errors of more commands
+
+* `camel export` does not clean the export directory when it is the current directory, or one that contains it, unless
+ `--clean-dir` is given: `--dir=./`, `--dir=$PWD` and `--dir=..` deleted the files of the project, as only `--dir=.`
+ was recognized.
+* `camel harden --url` with an LLM that cannot be reached exits with `1`, instead of sending the route to a local
+ Ollama.
+* `camel sbom` with an unknown `--sbom-format` or `--sbom-output-format` exits with `2` (it exited with `0` without
+ doing anything), and exits with `124` when Maven does not finish in time.
+* `camel update run --upgradeTimeout` applies (the output was read to the end before the wait), and exits with `124`
+ when it is reached.
+* `camel version list` exits with `1` when the versions cannot be downloaded. `camel version set --reset` removes the
+ Kamelets version too, and `camel version get` shows a configuration with only the Kamelets version.
+* `camel validate yaml` and `camel validate properties` with no file to validate exit with `3`.
+* `camel doc` looks a name up in the catalog of Camel before it downloads the Kamelet catalog, so a component is
+ documented also offline. `camel catalog kamelet` has `--repos`.
+* `camel kubernetes logs` exits with `3` when no pod with the label shows up, and with `1` when reading the log fails.
+ `camel kubernetes run` prints the end of the Maven output when the build or deployment fails (it was hidden without
+ `--verbose`), and exits with `124` when the pod is not running after 10 minutes.
+* `--jvm-args` keeps a quoted value with spaces as one argument.
+
=== camel-jpa, camel-hazelcast - an exchange marked rollback only is not consumed
When a route marks the exchange rollback only (for example with `markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
index 838371b055c7..fd8a4208c305 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
@@ -19,8 +19,11 @@ camel catalog kamelet [options]
[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
+| `--download` | Whether to allow automatic downloading JAR dependencies (over the internet) | true | boolean
| `--filter` | Filter by name or description | | String
+| `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false | boolean
| `--kamelets-version` | Apache Camel Kamelets version (auto-detected from classpath if not set) | | String
+| `--repo,--repos` | Additional maven repositories for download on-demand (Use commas to separate multiple repositories) | | String
| `--sort` | Sort by name, type, support-level, or description | name | String
| `--type,--filter-type` | Filter by type: source, sink, or action | | String
| `-h,--help` | Display the help and sub-commands | | boolean
diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 76bd529af26a..22520bf0328c 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -2,7 +2,7 @@
"commands": [
{ "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...]
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...]
- { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
+ { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
{ "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index bee35db959b9..a15c7aa33557 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -400,7 +400,12 @@ public class Debug extends Run {
pb.inheritIO();
Process p = pb.start();
- if (p.waitFor(120, TimeUnit.SECONDS)) {
+ boolean built = p.waitFor(120, TimeUnit.SECONDS);
+ if (built && p.exitValue() != 0) {
+ printer().printErr("Building Camel Spring Boot for debugging failed (exit code " + p.exitValue() + ")");
+ return CliExitCode.FAILURE;
+ }
+ if (built) {
AtomicReference<Process> processRef = new AtomicReference<>();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
@@ -526,7 +531,12 @@ public class Debug extends Run {
pb.inheritIO();
Process p = pb.start();
- if (p.waitFor(120, TimeUnit.SECONDS)) {
+ boolean built = p.waitFor(120, TimeUnit.SECONDS);
+ if (built && p.exitValue() != 0) {
+ printer().printErr("Building Camel Quarkus for debugging failed (exit code " + p.exitValue() + ")");
+ return CliExitCode.FAILURE;
+ }
+ if (built) {
AtomicReference<Process> processRef = new AtomicReference<>();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
index 7a3a7266cffc..637b941a8391 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
@@ -580,6 +580,16 @@ public abstract class ExportBaseCommand extends CamelCommand {
protected abstract Integer export() throws Exception;
+ /**
+ * Whether the directory is the current directory, or one that contains it, such as <tt>./</tt>, <tt>$PWD</tt> or
+ * <tt>..</tt>: cleaning it before the export deletes the files of the project, so it is only cleaned with
+ * --clean-dir.
+ */
+ static boolean isCurrentDirOrParent(String dir) {
+ Path path = Path.of(dir).toAbsolutePath().normalize();
+ return Path.of("").toAbsolutePath().normalize().startsWith(path);
+ }
+
protected Integer runSilently(boolean ignoreLoadingError, boolean lazyBean, boolean verbose) throws Exception {
if (kameletsVersion == null) {
kameletsVersion = VersionHelper.extractKameletsVersion();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
index a9cf94801f7f..e6df6dd6ecc3 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
@@ -196,7 +196,7 @@ class ExportCamelMain extends Export {
}
String appJar = Paths.get("target", ids[1] + "-" + ids[2] + ".jar").toString();
copyReadme(BUILD_DIR, appJar);
- if (cleanExportDir || !exportDir.equals(".")) {
+ if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
// cleaning current dir can be a bit dangerous so only clean if explicit enabled
// otherwise always clean export-dir to avoid stale data
if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
index 26f0357e24b8..7e141adb1fa2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
@@ -187,7 +187,7 @@ class ExportQuarkus extends Export {
copyCamelWrapper();
}
- if (cleanExportDir || !exportDir.equals(".")) {
+ if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
// cleaning current dir can be a bit dangerous so only clean if explicit enabled
// otherwise always clean export-dir to avoid stale data
if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
index 305b020a6fea..0ba2377713be 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
@@ -193,7 +193,7 @@ class ExportSpringBoot extends Export {
}
String appJar = "target" + File.separator + ids[1] + "-" + ids[2] + ".jar";
copyReadme(BUILD_DIR, appJar);
- if (cleanExportDir || !exportDir.equals(".")) {
+ if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
// cleaning current dir can be a bit dangerous so only clean if explicit enabled
// otherwise always clean export-dir to avoid stale data
if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
index f21b16500bf7..7a27199e3bcb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
@@ -257,8 +257,11 @@ public class Harden extends CamelCommand {
}
private String detectEndpoint() {
- return tryExplicitUrl()
- .or(this::tryInfraOllama)
+ if (url != null && !url.isBlank()) {
+ // the LLM asked for, or none: the route is not sent to another one
+ return tryExplicitUrl().orElse(null);
+ }
+ return tryInfraOllama()
.or(this::tryDefaultOllama)
.orElse(null);
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index d0220fcc274a..0a4f08332fdb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -2010,7 +2010,7 @@ public class Run extends CamelCommand {
javaCmd.add(Paths.get(System.getProperty("java.home"), "bin", "java").toString());
String runJvmArgs = mergeJvmArgs(jvmArgs, buildJfrJvmArgs());
if (runJvmArgs != null && !runJvmArgs.isBlank()) {
- javaCmd.addAll(Arrays.asList(runJvmArgs.trim().split("\\s+")));
+ javaCmd.addAll(CommandLineHelper.splitArguments(runJvmArgs));
}
if (jvmDebugPort > 0) {
javaCmd.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + jvmDebugPort);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
index 60ba2e839883..e3d654b6ae1b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
@@ -25,6 +25,8 @@ import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.apache.camel.dsl.jbang.core.common.*;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.util.CamelCaseOrderedProperties;
import org.apache.camel.util.FileUtil;
import picocli.CommandLine;
@@ -79,6 +81,12 @@ public class SBOMGenerator extends Export {
@Override
public Integer doCall() throws Exception {
+ if (!CYCLONEDX_FORMAT.equalsIgnoreCase(sbomFormat) && !SPDX_FORMAT.equalsIgnoreCase(sbomFormat)) {
+ throw CliException.usage("Unknown SBOM format: " + sbomFormat + " (use cyclonedx or spdx)");
+ }
+ if (!SBOM_JSON_FORMAT.equalsIgnoreCase(sbomOutputFormat) && !SBOM_XML_FORMAT.equalsIgnoreCase(sbomOutputFormat)) {
+ throw CliException.usage("Unknown SBOM output format: " + sbomOutputFormat + " (use json or xml)");
+ }
this.quiet = true; // be quiet and generate from fresh data to ensure the output is up-to-date
return super.doCall();
}
@@ -127,7 +135,8 @@ public class SBOMGenerator extends Export {
done = p.waitFor(60, TimeUnit.SECONDS);
if (!done) {
p.destroyForcibly();
- answer = 1;
+ printer().printErr("Maven did not generate the SBOM within 60 seconds");
+ answer = CliExitCode.TIMEOUT;
} else if (p.exitValue() != 0) {
answer = p.exitValue();
}
@@ -156,7 +165,8 @@ public class SBOMGenerator extends Export {
done = p.waitFor(60, TimeUnit.SECONDS);
if (!done) {
p.destroyForcibly();
- answer = 1;
+ printer().printErr("Maven did not generate the SBOM within 60 seconds");
+ answer = CliExitCode.TIMEOUT;
} else if (p.exitValue() != 0) {
answer = p.exitValue();
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
index d5419617a91a..87f7955da7f8 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
@@ -224,14 +224,19 @@ public abstract class CatalogBaseCommand extends CamelCommand {
return o1.name.compareToIgnoreCase(o2.name) * negate;
case "level":
case "support-level":
- return o1.level.compareToIgnoreCase(o2.level) * negate;
+ return compareIgnoreCase(o1.level, o2.level) * negate;
case "description":
- return o1.description.compareToIgnoreCase(o2.description) * negate;
+ return compareIgnoreCase(o1.description, o2.description) * negate;
default:
return 0;
}
}
+ // a missing value sorts first
+ static int compareIgnoreCase(String s1, String s2) {
+ return (s1 != null ? s1 : "").compareToIgnoreCase(s2 != null ? s2 : "");
+ }
+
String shortGav(Row r) {
// only output artifact id
return MavenGav.parseGav(r.gav).getArtifactId();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
index 9c27755bb3a7..4149fcd2aec8 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
@@ -179,13 +179,6 @@ public class CatalogDoc extends CamelCommand {
}
}
- if (prefix == null || "kamelet".equals(prefix)) {
- KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
- if (km != null) {
- docKamelet(km);
- return 0;
- }
- }
if (prefix == null || "component".equals(prefix)) {
ComponentModel cm = catalog.componentModel(name);
if (cm != null) {
@@ -218,6 +211,14 @@ public class CatalogDoc extends CamelCommand {
return 0;
}
}
+ // last, as the Kamelet catalog is downloaded (a component is documented also offline)
+ if (prefix == null || "kamelet".equals(prefix)) {
+ KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
+ if (km != null) {
+ docKamelet(km);
+ return 0;
+ }
+ }
if (prefix == null) {
// guess if a kamelet
@@ -267,13 +268,6 @@ public class CatalogDoc extends CamelCommand {
if ("main".equals(name) && catalog.mainModel() != null) {
return CatalogDocs.catalogDoc(catalog, name, null, null, filter, "all", false, false, null);
}
- if (prefix == null || "kamelet".equals(prefix)) {
- // the Kamelets of the version and repositories asked for, not only the ones of the CLI
- KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
- if (km != null) {
- return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " + kameletsVersion));
- }
- }
if (prefix == null || !"kamelet".equals(prefix) && !"other".equals(prefix)) {
JsonObject doc = CatalogDocs.catalogDoc(catalog, name, null, prefix, filter, "all", headers, false, null);
if (doc.get("error") == null) {
@@ -289,6 +283,13 @@ public class CatalogDoc extends CamelCommand {
return doc;
}
}
+ if (prefix == null || "kamelet".equals(prefix)) {
+ // the Kamelets of the version and repositories asked for, not only the ones of the CLI
+ KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
+ if (km != null) {
+ return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " + kameletsVersion));
+ }
+ }
return null;
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
index 18cae77dd45c..057025461fb4 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
@@ -16,7 +16,6 @@
*/
package org.apache.camel.dsl.jbang.core.commands.catalog;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -29,12 +28,10 @@ import com.github.freva.asciitable.Column;
import com.github.freva.asciitable.HorizontalAlign;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
import org.apache.camel.dsl.jbang.core.common.CamelTableColumns;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
-import org.apache.camel.main.download.DependencyDownloaderClassLoader;
-import org.apache.camel.main.download.MavenDependencyDownloader;
-import org.apache.camel.support.ObjectHelper;
import org.apache.camel.util.StringHelper;
import picocli.CommandLine;
@@ -63,6 +60,9 @@ public class CatalogKamelet extends CamelCommand {
"--kamelets-version" }, description = "Apache Camel Kamelets version (auto-detected from classpath if not set)")
String kameletsVersion;
+ @CommandLine.Mixin
+ MavenResolverMixin mavenResolver = new MavenResolverMixin();
+
public CatalogKamelet(CamelJBangMain main) {
super(main);
}
@@ -76,23 +76,11 @@ public class CatalogKamelet extends CamelCommand {
}
Map<String, Object> kamelets;
- var tccLoader = Thread.currentThread().getContextClassLoader();
- try (MavenDependencyDownloader downloader = new MavenDependencyDownloader()) {
- ClassLoader cl = createClassLoader();
- downloader.setClassLoader(cl);
- downloader.start();
- downloader.downloadDependency("org.apache.camel.kamelets", "camel-kamelets-catalog", kameletsVersion);
-
- Thread.currentThread().setContextClassLoader(cl);
- Class<?> clazz = cl.loadClass("org.apache.camel.kamelets.catalog.KameletsCatalog");
- Object catalog = clazz.getDeclaredConstructor().newInstance();
- Method m = clazz.getMethod("getKamelets");
- kamelets = (Map<String, Object>) ObjectHelper.invokeMethod(m, catalog);
+ try {
+ kamelets = KameletCatalogHelper.loadKamelets(kameletsVersion, mavenResolver.repos());
} catch (Exception e) {
printer().printErr("Cannot download camel-kamelets-catalog due to " + e.getMessage());
return 1;
- } finally {
- Thread.currentThread().setContextClassLoader(tccLoader);
}
for (Object o : kamelets.values()) {
@@ -140,23 +128,19 @@ public class CatalogKamelet extends CamelCommand {
switch (s) {
case "name":
return o1.name.compareToIgnoreCase(o2.name) * negate;
+ // a Kamelet without the value sorts first
case "type":
- return o1.type.compareToIgnoreCase(o2.type) * negate;
+ return CatalogBaseCommand.compareIgnoreCase(o1.type, o2.type) * negate;
case "level":
case "support-level":
- return o1.supportLevel.compareToIgnoreCase(o2.supportLevel) * negate;
+ return CatalogBaseCommand.compareIgnoreCase(o1.supportLevel, o2.supportLevel) * negate;
case "description":
- return o1.description.compareToIgnoreCase(o2.description) * negate;
+ return CatalogBaseCommand.compareIgnoreCase(o1.description, o2.description) * negate;
default:
return 0;
}
}
- private ClassLoader createClassLoader() {
- ClassLoader parentCL = CatalogKamelet.class.getClassLoader();
- return new DependencyDownloaderClassLoader(parentCL);
- }
-
private String getDescription(KameletModel r) {
String d = r.description;
if (d != null && d.contains(".")) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
index 937a5b785ff0..561680fda17f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
@@ -28,6 +28,7 @@ import java.util.stream.Stream;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.RuntimeType;
import org.apache.camel.main.download.DownloadException;
@@ -116,25 +117,25 @@ public class UpdateRun extends CamelCommand {
Process p = pb.redirectErrorStream(true)
.start();
- try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
- BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()))) {
-
- String line;
- while ((line = stdInput.readLine()) != null || (line = stdError.readLine()) != null) {
- printer().println(line);
- }
-
- if (!p.waitFor(updateMixin.upgradeTimeout, TimeUnit.SECONDS)) {
- p.destroyForcibly();
- printer().printErr("Update execution timed out");
-
- return CliExitCode.TIMEOUT;
+ // print the output while the command runs, so the timeout applies to the command
+ Thread reader = new Thread(() -> {
+ try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+ String line;
+ while ((line = stdInput.readLine()) != null) {
+ printer().println(line);
+ }
+ } catch (IOException e) {
+ // the command was destroyed
}
+ }, "camel-update-output");
+ reader.setDaemon(true);
+ reader.start();
- int exitCode = p.exitValue();
-
- return exitCode;
-
+ if (!p.waitFor(updateMixin.upgradeTimeout, TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ throw CliException.timeout("Update did not finish within " + updateMixin.upgradeTimeout + " seconds");
}
+ reader.join(TimeUnit.SECONDS.toMillis(5));
+ return p.exitValue();
}
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
index 89a59ba68bf0..b7d95d56885a 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
@@ -54,7 +54,7 @@ public class VersionGet extends CamelCommand {
String kv = properties.getProperty("kamelets-version");
String repos = properties.getProperty("repos");
String runtime = properties.getProperty("runtime");
- if (uv != null || repos != null || runtime != null) {
+ if (uv != null || kv != null || repos != null || runtime != null) {
printer().println("User configuration:");
if (uv != null) {
printer().println(" camel-version = " + uv);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
index 63b5137c219a..d461580807fb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
@@ -52,6 +52,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
import org.apache.camel.dsl.jbang.core.commands.QuarkusExtensionRegistryMixin;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.QuarkusHelper;
import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -403,10 +404,8 @@ public class VersionList extends CamelCommand {
return versions;
} catch (Exception e) {
- printer().printErr("Error downloading available Camel versions due to: " + e.getMessage());
+ throw CliException.failure("Error downloading available Camel versions due to: " + e.getMessage());
}
-
- return Stream.of();
}
private Stream<Row> filterVersions(Stream<CamelAndRuntimeVersions> versions, List<ReleaseModel> releases) throws Exception {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
index 04d2659112f8..a6c2833004b4 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
@@ -64,6 +64,7 @@ public class VersionSet extends CamelCommand {
CommandLineHelper.loadProperties(properties -> {
if (reset) {
properties.remove("camel-version");
+ properties.remove("kamelets-version");
properties.remove("repos");
properties.remove("runtime");
} else {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
index e14d9e23d914..1327ea893ace 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
@@ -24,6 +24,8 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Properties;
import java.util.function.Consumer;
@@ -205,6 +207,40 @@ public final class CommandLineHelper {
return homeDir;
}
+ /**
+ * The arguments of a command line typed as one text, such as <tt>--jvm-args</tt>: split at whitespace outside
+ * double or single quotes, which are removed, and a backslash escapes the next character (outside single quotes).
+ */
+ public static List<String> splitArguments(String line) {
+ List<String> answer = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ boolean token = false;
+ char quote = 0;
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+ if (quote == 0 && Character.isWhitespace(c)) {
+ if (token) {
+ answer.add(current.toString());
+ current.setLength(0);
+ token = false;
+ }
+ } else if (c == '\\' && quote != '\'' && i + 1 < line.length()) {
+ current.append(line.charAt(++i));
+ token = true;
+ } else if ((c == '"' || c == '\'') && (quote == 0 || quote == c)) {
+ quote = quote == 0 ? c : 0;
+ token = true;
+ } else {
+ current.append(c);
+ token = true;
+ }
+ }
+ if (token) {
+ answer.add(current.toString());
+ }
+ return answer;
+ }
+
/**
* Adjust basic home directory where user properties and other settings will be stored. Unit tests may set this in
* order to create stable and independent tests.
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
index 4ce505026a6a..180f7a8b5f49 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
@@ -173,6 +173,36 @@ class CommandInputErrorsTest {
.extracting(e -> ((CliException) e).getExitCode()).isEqualTo(CliExitCode.USAGE);
}
+ @Test
+ void sbomWithAnUnknownFormat() throws Exception {
+ Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n uri: timer:x\n");
+
+ assertThatThrownBy(() -> call(new SBOMGenerator(main), file.toString(), "--sbom-format=bogus"))
+ .isInstanceOf(CliException.class)
+ .hasMessage("Unknown SBOM format: bogus (use cyclonedx or spdx)")
+ .extracting(e -> ((CliException) e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+ }
+
+ @Test
+ void hardenWithAnLlmThatCannotBeReached() throws Exception {
+ Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n uri: timer:x\n");
+
+ // the route is not sent to another LLM, such as a local Ollama
+ assertThat(call(new Harden(main), file.toString(), "--url=http://127.0.0.1:9")).isEqualTo(CliExitCode.FAILURE);
+ assertThat(printer.getStderr()).startsWith("ERROR: Cannot connect to LLM service at: http://127.0.0.1:9");
+ assertThat(printer.getStdout()).doesNotContain("LLM Configuration");
+ }
+
+ @Test
+ void exportDoesNotCleanTheCurrentDirectory() {
+ assertThat(ExportBaseCommand.isCurrentDirOrParent(".")).isTrue();
+ assertThat(ExportBaseCommand.isCurrentDirOrParent("./")).isTrue();
+ assertThat(ExportBaseCommand.isCurrentDirOrParent(Path.of("").toAbsolutePath().toString())).isTrue();
+ assertThat(ExportBaseCommand.isCurrentDirOrParent("..")).isTrue();
+ assertThat(ExportBaseCommand.isCurrentDirOrParent("out")).isFalse();
+ assertThat(ExportBaseCommand.isCurrentDirOrParent(dir.resolve("out").toString())).isFalse();
+ }
+
@Test
void runQuarkusInTheBackground() throws Exception {
Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n uri: timer:x\n");
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
index b62adc4a3695..209321c9260d 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
@@ -28,6 +28,18 @@ import org.junit.jupiter.api.Test;
class CatalogDocTest extends CamelCommandBaseTestSupport {
+ @Test
+ public void shouldDocumentAComponentWithoutTheKameletCatalog() throws Exception {
+ CatalogDoc command = createCommand("timer", false);
+ // a Kamelet catalog that cannot be downloaded: the component is found before it is needed
+ command.kameletsVersion = "0.0.0-no-such-version";
+
+ int exit = command.doCall();
+
+ Assertions.assertEquals(0, exit);
+ Assertions.assertTrue(printer.getOutput().contains("timer"), "Should document the timer component");
+ }
+
@Test
public void shouldPrintExampleForConsumerComponent() throws Exception {
CatalogDoc command = createCommand("timer", true);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
index 9fc4d5dda497..0e3e239ac85b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
@@ -71,6 +71,18 @@ class CatalogKameletTest extends CamelCommandBaseTestSupport {
assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"), names(rows));
}
+ @Test
+ void shouldSortAKameletWithoutDescriptionFirst() {
+ CatalogKamelet cmd = command();
+ cmd.sort = "description";
+
+ List<KameletModel> rows = sampleRows();
+ rows.add(model("no-description-sink", "sink", null, null));
+ rows.sort(cmd::sortRow);
+
+ assertEquals(List.of("no-description-sink", "timer-source", "kafka-source", "Aws-S3-sink"), names(rows));
+ }
+
@Test
void shouldReverseOrderWhenSortKeyHasLeadingMinus() {
CatalogKamelet cmd = command();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
index 273841f6ae85..f916ede1081f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
@@ -65,6 +65,21 @@ class VersionGetTest extends CamelCommandBaseTestSupport {
Assertions.assertEquals("kamelets-version = greatest", lines.get(4));
}
+ @Test
+ public void shouldPrintOnlyTheKameletsVersion() throws Exception {
+ UserConfigHelper.createUserConfig("""
+ kamelets-version=4.18.0
+ """);
+ createJBangVersionFile("0.101");
+
+ VersionGet command = createCommand();
+ command.doCall();
+
+ List<String> lines = printer.getLines();
+ Assertions.assertEquals("User configuration:", lines.get(2));
+ Assertions.assertEquals("kamelets-version = 4.18.0", lines.get(3));
+ }
+
private void createJBangVersionFile(String version) throws IOException {
Path jBangDir = Paths.get("target", ".jbang", "cache");
if (!jBangDir.toFile().exists()) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
index 23fc0f5a171f..c6c5463113a2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
@@ -90,6 +90,7 @@ class VersionSetTest extends BaseConfigTestSupport {
void shouldRemoveAllSettingsOnReset() throws Exception {
UserConfigHelper.createUserConfig("""
camel-version=4.9.0
+ kamelets-version=4.9.0
repos=https://repo.example.org/maven
runtime=spring-boot
other=keep-me
@@ -101,9 +102,10 @@ class VersionSetTest extends BaseConfigTestSupport {
CommandLineHelper.loadProperties(properties -> {
assertFalse(properties.containsKey("camel-version"), "camel-version must be removed on reset");
+ assertFalse(properties.containsKey("kamelets-version"), "kamelets-version must be removed on reset");
assertFalse(properties.containsKey("repos"), "repos must be removed on reset");
assertFalse(properties.containsKey("runtime"), "runtime must be removed on reset");
- // unrelated settings are preserved: reset only clears the three version keys
+ // unrelated settings are preserved: reset only clears the version keys
assertEquals("keep-me", properties.get("other"));
});
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java
new file mode 100644
index 000000000000..42d56f85bd29
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.common;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The arguments of a command line typed as one text, such as --jvm-args: a quoted value with spaces is one argument.
+ */
+class CommandLineArgumentsTest {
+
+ @Test
+ void splitAtWhitespace() {
+ assertThat(CommandLineHelper.splitArguments(" -Xmx1g\t-Dfoo=bar ")).containsExactly("-Xmx1g", "-Dfoo=bar");
+ assertThat(CommandLineHelper.splitArguments("")).isEmpty();
+ }
+
+ @Test
+ void quotedValueIsOneArgument() {
+ assertThat(CommandLineHelper.splitArguments("-Xmx1g -Dgreeting=\"hello world\""))
+ .containsExactly("-Xmx1g", "-Dgreeting=hello world");
+ assertThat(CommandLineHelper.splitArguments("-Dgreeting='hello world' -Dempty=''"))
+ .containsExactly("-Dgreeting=hello world", "-Dempty=");
+ // a quote of the other kind inside quotes is kept
+ assertThat(CommandLineHelper.splitArguments("-Dname=\"O'Brien\"")).containsExactly("-Dname=O'Brien");
+ }
+
+ @Test
+ void backslashEscapes() {
+ assertThat(CommandLineHelper.splitArguments("-Dpath=a\\ b")).containsExactly("-Dpath=a b");
+ // not inside single quotes
+ assertThat(CommandLineHelper.splitArguments("'a\\b'")).containsExactly("a\\b");
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
index 97f3614842d9..5a88a88ff156 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
@@ -25,6 +25,8 @@ import io.fabric8.kubernetes.client.dsl.LogWatch;
import io.fabric8.kubernetes.client.dsl.PodResource;
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.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.util.ObjectHelper;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@@ -59,6 +61,9 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
// used for testing
long maxMessageCount = -1;
long messageCount = 0;
+ // whether a pod with the label showed up, and whether reading its log failed the last time
+ private boolean podFound;
+ private boolean readFailed;
public KubernetesPodLogs(CamelJBangMain main) {
super(main);
@@ -74,8 +79,7 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
String[] parts = label.split("=", 2);
if (parts.length != 2) {
- printer().printErr("--label selector must be in syntax: key=value");
- return 1;
+ throw CliException.usage("--label selector must be in syntax: key=value");
}
var retry = watchLogs();
@@ -86,7 +90,10 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
}
printer().println("Stopped pod logging!");
- return 0;
+ if (!podFound) {
+ throw CliException.notFound("No pod with label " + label);
+ }
+ return readFailed ? CliExitCode.FAILURE : CliExitCode.OK;
}
// Returns true if a retry should be attempted
@@ -100,6 +107,7 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
printer().printf("Pod for label %s not available%n", label);
return true;
}
+ podFound = true;
var terminated = isPodTerminated(podRes);
if (!terminated) {
@@ -128,8 +136,10 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
}
retryCount = 0;
}
+ readFailed = false;
} catch (IOException e) {
- printer().println("Failed to read pod logs - " + e.getMessage());
+ printer().printErr("Failed to read pod logs - " + e.getMessage());
+ readFailed = true;
}
terminated = isPodTerminated(podRes);
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 64785c15f45b..4f06110304af 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
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.fabric8.kubernetes.client.KubernetesClientTimeoutException;
import io.fabric8.kubernetes.client.vertx5.Vertx5HttpClientFactory;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
@@ -50,6 +51,7 @@ import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.MountTrait;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.TraitHelper;
import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.model.Traits;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.Printer;
import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom;
import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -627,8 +629,13 @@ public class KubernetesRun extends KubernetesBaseCommand {
}
printer().println("Run: " + kubectlCmd);
}
- var pod = client(Pod.class).withLabel(BaseTrait.KUBERNETES_LABEL_NAME, projectName)
- .waitUntilCondition(it -> "Running".equals(getPodPhase(it)), 10, TimeUnit.MINUTES);
+ Pod pod;
+ try {
+ pod = client(Pod.class).withLabel(BaseTrait.KUBERNETES_LABEL_NAME, projectName)
+ .waitUntilCondition(it -> "Running".equals(getPodPhase(it)), 10, TimeUnit.MINUTES);
+ } catch (KubernetesClientTimeoutException e) {
+ throw CliException.timeout("The pod of " + projectName + " is not running after 10 minutes");
+ }
printer().println(String.format("Pod '%s' in phase %s", pod.getMetadata().getName(), getPodPhase(pod)));
}
@@ -719,22 +726,20 @@ public class KubernetesRun extends KubernetesBaseCommand {
pb.command(args.toArray(String[]::new));
- if (quiet || !verbose) {
- pb.redirectErrorStream(true);
- pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
- } else {
- pb.inheritIO(); // run in foreground (with IO so logs are visible)
- }
-
- Process p = pb.start();
- // wait for that process to exit as we run in foreground
- int exit = p.waitFor();
- if (exit != 0) {
- printer().printErr("Build failed!");
- return exit;
+ Path log = mavenOutput(pb);
+ try {
+ Process p = pb.start();
+ // wait for that process to exit as we run in foreground
+ int exit = p.waitFor();
+ if (exit != 0) {
+ printer().printErr("Build failed!");
+ printMavenOutput(log);
+ return exit;
+ }
+ return 0;
+ } finally {
+ removeMavenOutput(log);
}
-
- return 0;
}
private Integer deployProject(String workingDir, boolean reload) throws Exception {
@@ -801,26 +806,66 @@ public class KubernetesRun extends KubernetesBaseCommand {
pb.command(args.toArray(String[]::new));
- if (quiet || !verbose) {
- pb.redirectErrorStream(true);
- pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
- } else {
+ Path log = mavenOutput(pb);
+ try {
+ Process p = pb.start();
+ // wait for that process to exit as we run in foreground
+ int exit = p.waitFor();
+ if (exit != 0) {
+ printer().printErr("Deployment to %s failed!".formatted(clusterType));
+ printMavenOutput(log);
+ return exit;
+ }
+ return 0;
+ } finally {
+ removeMavenOutput(log);
+ }
+ }
+
+ /**
+ * Shows the output of Maven with --verbose, and keeps it in a log file otherwise, for when it fails.
+ *
+ * @return the log file, or <tt>null</tt> with --verbose
+ */
+ private Path mavenOutput(ProcessBuilder pb) throws IOException {
+ if (!quiet && verbose) {
pb.inheritIO(); // run in foreground (with IO so logs are visible)
+ return null;
}
+ Path log = Files.createTempFile("camel-kubernetes-", ".log");
+ log.toFile().deleteOnExit();
+ pb.redirectErrorStream(true);
+ pb.redirectOutput(log.toFile());
+ return log;
+ }
- Process p = pb.start();
- // wait for that process to exit as we run in foreground
- int exit = p.waitFor();
- if (exit != 0) {
- String msg = "Deployment to %s failed!";
- if (!verbose) {
- msg += " (use --verbose for more details)";
+ /**
+ * Removes the log of Maven once it has ended: camel kubernetes run --dev builds and deploys again on every change.
+ */
+ private static void removeMavenOutput(Path log) {
+ if (log != null) {
+ try {
+ Files.deleteIfExists(log);
+ } catch (IOException e) {
+ // removed when the JVM exits
}
- printer().printErr(msg.formatted(clusterType));
- return exit;
}
+ }
- return 0;
+ /**
+ * Prints the end of the output of Maven when it failed (it was hidden, as no --verbose was given).
+ */
+ private void printMavenOutput(Path log) {
+ if (log == null) {
+ return;
+ }
+ try {
+ List<String> lines = Files.readAllLines(log);
+ printer().printDiagnostic(String.join(System.lineSeparator(),
+ lines.subList(Math.max(0, lines.size() - 30), lines.size())));
+ } catch (IOException e) {
+ // no output to show
+ }
}
void detectCluster() {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
index b422d8e0dbaf..b7b8840baf2e 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
@@ -23,6 +23,8 @@ import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
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.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
@@ -36,8 +38,9 @@ class KubernetesPodLogsTest extends KubernetesBaseTestSupport {
KubernetesPodLogs command = createCommand();
command.name = "mickey-mouse";
command.maxRetryAttempts = 2; // total timeout of 4 seconds
- int exit = command.doCall();
- Assertions.assertEquals(0, exit);
+ CliException e = Assertions.assertThrows(CliException.class, command::doCall);
+ Assertions.assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+ Assertions.assertEquals("No pod with label app.kubernetes.io/name=mickey-mouse", e.getMessage());
Assertions.assertTrue(
printer.getOutput().contains("Pod for label app.kubernetes.io/name=mickey-mouse not available"));
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
index 0f71f20448fd..0b5ae84b2e0b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
@@ -30,6 +30,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.JsonOutput;
import org.apache.camel.util.FileUtil;
import picocli.CommandLine;
@@ -70,6 +71,9 @@ public class PropertiesValidateCommand extends CamelCommand {
}
}
+ if (reports.isEmpty()) {
+ throw CliException.notFound("No properties files to validate");
+ }
int count = reports.values().stream().mapToInt(List::size).sum();
if (jsonOutput) {
JsonOutput.printList(printer(),
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
index 246a273caa6b..84ec7c2cd711 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
@@ -32,6 +32,7 @@ import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.JsonOutput;
import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.apache.camel.util.FileUtil;
@@ -109,6 +110,9 @@ public class YamlValidateCommand extends CamelCommand {
}
}
+ if (reports.isEmpty()) {
+ throw CliException.notFound("No YAML files to validate");
+ }
int count = errorCounts(reports);
if (jsonOutput) {
List<JsonObject> rows = new ArrayList<>();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
index 7113fd943a2c..79323113e0f7 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
@@ -21,6 +21,8 @@ import java.nio.file.Path;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.StringPrinter;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -30,6 +32,7 @@ import org.junit.jupiter.api.io.TempDir;
import picocli.CommandLine;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* camel validate with --json: one JSON array on stdout with valid, file and errors of each file, as the MCP tool
@@ -100,12 +103,24 @@ class ValidateJsonTest {
Path text = dir.resolve("notes.txt");
Files.writeString(text, "not a route");
- assertThat(run(new YamlValidateCommand(main()), "--json", text.toString())).isZero();
-
- assertThat(printer.getStdout()).isEqualTo("[]");
+ // nothing is left to validate, which is not found (the error document is printed by the error handler)
+ CliException e = assertThrows(CliException.class,
+ () -> run(new YamlValidateCommand(main()), "--json", text.toString()));
+ assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
assertThat(printer.getStderr()).startsWith("WARN: Skipping non-YAML file");
}
+ @Test
+ void nothingToValidateIsNotFound() throws Exception {
+ Path text = Files.writeString(dir.resolve("notes.txt"), "not a route");
+
+ CliException e = assertThrows(CliException.class, () -> run(new YamlValidateCommand(main()), text.toString()));
+ assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ assertThat(e.getMessage()).isEqualTo("No YAML files to validate");
+ e = assertThrows(CliException.class, () -> run(new PropertiesValidateCommand(main()), text.toString()));
+ assertThat(e.getMessage()).isEqualTo("No properties files to validate");
+ }
+
private CamelJBangMain main() {
printer = new StringPrinter();
return new CamelJBangMain().withPrinter(printer);