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 80d04951a422 CAMEL-26931: camel-jbang - camel plugin add refuses a
plugin that cannot be loaded, and run and export go on without one (#29918)
80d04951a422 is described below
commit 80d04951a422185b5a2a4391b5a085db3f2aeaf1
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 19:24:16 2026 +0100
CAMEL-26931: camel-jbang - camel plugin add refuses a plugin that cannot be
loaded, and run and export go on without one (#29918)
camel plugin add kubernets printed "Plugin kubernets added", and every
camel run and
camel export failed after, as a plugin without appliesTo contributes to
them and one
that does not resolve failed the command.
plugin add resolves a plugin it does not know (no Camel plugin, no known
plugin, no
--gav or --artifactId) before storing it, and exits 3 with the plugins of a
similar
name when it cannot be loaded. A plugin that only contributes to camel run
or camel
export and cannot be loaded is left out with a warning; its own command
still fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-projects.adoc | 5 ++
.../dsl/jbang/core/commands/plugin/PluginAdd.java | 56 +++++++++++++++++++++-
.../camel/dsl/jbang/core/common/PluginHelper.java | 29 ++++++++++-
.../jbang/core/commands/plugin/PluginAddTest.java | 20 +++++++-
.../dsl/jbang/core/common/PluginFilteringTest.java | 20 ++++----
.../dsl/jbang/core/common/PluginHelperTest.java | 23 +++++++++
6 files changed, 141 insertions(+), 12 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
index 5b4820208a6b..ab0998ebc891 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
@@ -278,6 +278,11 @@ camel plugin get
camel plugin delete <plugin-name>
----
+`camel plugin add` checks that a plugin it does not know (neither a plugin of
the Apache Camel project nor a known
+community plugin) can be loaded before it stores it: a name such as
`kubernets` exits with code `3` and the plugins of a
+similar name. Give `--gav` for a plugin of your own. A stored plugin that
cannot be loaded no longer fails `camel run`
+and `camel export`: they warn and go on without it.
+
=== Custom plugin configuration
By default, the CLI stores the plugin configuration in
`~/.camel-jbang-plugins.json`.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
index e127c477f324..9e7a78f8902d 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.dsl.jbang.core.commands.plugin;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import org.aesh.command.CommandDefinition;
@@ -24,6 +26,7 @@ import org.aesh.command.option.Option;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
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.Plugin;
import org.apache.camel.dsl.jbang.core.common.PluginCompatibility;
import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Compat;
@@ -36,8 +39,11 @@ import
org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Older;
import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Result;
import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Selected;
import
org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Unavailable;
+import org.apache.camel.dsl.jbang.run.common.Printer;
+import org.apache.camel.dsl.jbang.run.common.StringPrinter;
import org.apache.camel.main.download.DependencyDownloader;
import org.apache.camel.main.download.MavenDependencyDownloader;
+import org.apache.camel.main.util.SuggestSimilarHelper;
import org.apache.camel.tooling.maven.MavenGav;
import org.apache.camel.util.json.JsonObject;
@@ -121,6 +127,9 @@ public class PluginAdd extends PluginBaseCommand {
repositories = knownRepos;
}
}
+ } else if (gav == null && artifactId == null && !resolves()) {
+ // stored, a plugin that cannot be loaded broke every camel
run and camel export (CAMEL-26931)
+ throw unknownPlugin(name);
}
}
@@ -183,6 +192,44 @@ public class PluginAdd extends PluginBaseCommand {
return 0;
}
+ /**
+ * Whether a plugin of a name that is neither a Camel plugin nor a known
one can be loaded, as it is when used: from
+ * the classpath, or as
org.apache.camel:camel-jbang-plugin-<command>.
+ */
+ private boolean resolves() {
+ if (command == null) {
+ command = name;
+ }
+ try {
+ // quietly: the answer says why it is not added
+ return downloadPlugin(null, new StringPrinter()).isPresent();
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ /**
+ * A plugin that is neither a Camel plugin nor a known one, given without
--gav or --artifactId, and that cannot be
+ * loaded: exit code 3, with the plugins of a similar name.
+ */
+ static CliException unknownPlugin(String name) {
+ List<String> names = new ArrayList<>();
+ for (PluginType type : PluginType.values()) {
+ names.add(type.getName());
+ }
+ for (JsonObject known : PluginHelper.loadKnownPlugins()) {
+ String n = known.getString("name");
+ if (n != null) {
+ names.add(n);
+ }
+ }
+ List<String> similar = SuggestSimilarHelper.didYouMean(names, name);
+ return CliException.notFound("Unknown plugin: " + name
+ + (similar.isEmpty() ? "" : ". Did you
mean: " + String.join(", ", similar))
+ + ". camel plugin list shows the plugins;
give --gav (or --artifactId) to add a"
+ + " plugin of your own");
+ }
+
/**
* Selects the newest release of a community plugin built for this Camel
CLI, and downloads it, so a release that
* cannot be used never gets into the configuration. Sets {@link #version}
to the selected release, or leaves it
@@ -336,7 +383,14 @@ public class PluginAdd extends PluginBaseCommand {
* Downloads the plugin, as when it is used.
*/
Optional<Plugin> downloadPlugin(String dependency) {
- return PluginHelper.getPlugin(command, cliVersion(), dependency,
repositories, printer());
+ return downloadPlugin(dependency, printer());
+ }
+
+ /**
+ * Downloads the plugin, as when it is used, reporting to the given
printer.
+ */
+ Optional<Plugin> downloadPlugin(String dependency, Printer printer) {
+ return PluginHelper.getPlugin(command, cliVersion(), dependency,
repositories, printer);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
index faf49f4d1233..08316cf2e175 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
@@ -32,6 +32,7 @@ import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
@@ -41,6 +42,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
+import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
@@ -421,12 +423,26 @@ public final class PluginHelper {
compatibilityCheck(main.getOut(), version, name,
properties);
}
- ResolveResult res = resolvePlugin(properties, command,
version, gav, repos, main.getOut());
+ // a plugin that only contributes to this command (camel run,
camel export): one that cannot be loaded
+ // is left out with a warning, rather than failing the command
(CAMEL-26931)
+ boolean contributor = target != null &&
!target.equals(command);
+ ResolveResult res;
+ try {
+ res = resolvePlugin(properties, command, version, gav,
repos, main.getOut());
+ } catch (RuntimeException e) {
+ if (!contributor) {
+ throw e;
+ }
+ warnNotLoaded(main.getOut(), name, target, e.getMessage());
+ continue;
+ }
if (res.plugin().isPresent()) {
activePlugins.put(command, res.plugin().get());
if (res.cacheWritten()) {
configDirty = true;
}
+ } else if (contributor) {
+ warnNotLoaded(main.getOut(), name, target,
"camel-jbang-plugin-" + command + " not found");
} else {
throw CliException.failure("camel-jbang-plugin-" + command
+ " not found");
}
@@ -439,6 +455,17 @@ public final class PluginHelper {
return activePlugins;
}
+ // the plugins already warned about, per printer (one per command), as a
command may look up the plugins more
+ // than once
+ private static final Map<Printer, Set<String>> WARNED_NOT_LOADED =
Collections.synchronizedMap(new WeakHashMap<>());
+
+ private static void warnNotLoaded(Printer printer, String name, String
target, String reason) {
+ if (WARNED_NOT_LOADED.computeIfAbsent(printer, p ->
ConcurrentHashMap.newKeySet()).add(name)) {
+ printer.printWarn("Plugin " + name + " cannot be loaded (" +
reason + "), so camel " + target
+ + " goes on without it: remove it with camel
plugin delete " + name);
+ }
+ }
+
public static Optional<Plugin> getPlugin(String name, String
defaultVersion, String gav, String repos, Printer printer) {
return resolvePlugin(null, name, defaultVersion, gav, repos,
printer).plugin();
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
index 5a4aebf8eb00..feeff1e03148 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
@@ -23,10 +23,13 @@ import java.util.Optional;
import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.common.CachedFakePlugin;
+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.FakePluginDownloader;
import org.apache.camel.dsl.jbang.core.common.Plugin;
import org.apache.camel.dsl.jbang.core.common.PluginHelper;
import org.apache.camel.dsl.jbang.core.common.PluginType;
+import org.apache.camel.dsl.jbang.run.common.Printer;
import org.apache.camel.main.download.DependencyDownloader;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Assertions;
@@ -103,6 +106,21 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
PluginHelper.getOrCreatePluginConfig().toJson());
}
+ @Test
+ public void aPluginThatCannotBeLoadedIsNotAdded() throws Exception {
+ // camel plugin add kubernets said added, and every camel run and
camel export failed after (CAMEL-26931)
+ TestPluginAdd command = newCommand();
+ command.name = "kubernets";
+ command.downloadFails = true;
+
+ CliException e = Assertions.assertThrows(CliException.class,
command::doCall);
+
+ Assertions.assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+ Assertions.assertTrue(e.getMessage().startsWith("Unknown plugin:
kubernets. Did you mean: kubernetes"),
+ e.getMessage());
+ Assertions.assertEquals("{\"plugins\":{}}",
PluginHelper.getOrCreatePluginConfig().toJson());
+ }
+
@Test
public void shouldUseMavenGAV() throws Exception {
TestPluginAdd command = newCommand();
@@ -374,7 +392,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
}
@Override
- Optional<Plugin> downloadPlugin(String dependency) {
+ Optional<Plugin> downloadPlugin(String dependency, Printer printer) {
downloaded.add(dependency);
return downloadFails ? Optional.empty() : Optional.of(new
CachedFakePlugin());
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
index d324d33017f7..94e0ff8b6baf 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
@@ -230,7 +230,8 @@ class PluginFilteringTest {
@Test
void pluginWithAppliesToExportIsLoadedByCliExportContext() throws
Exception {
// A broken entry with appliesTo ["export"]: if CliExportContext
includes it, resolution
- // fails — proving the plugin was included in the export target.
+ // fails — proving the plugin was included in the export target. A
contributor that cannot
+ // be loaded is left out with a warning (CAMEL-26931), which names it.
JsonObject entry = buildBrokenEntry("exporter");
entry.put("appliesTo", jsonArray("export"));
writeConfig(entry);
@@ -240,9 +241,9 @@ class PluginFilteringTest {
ExportContext context = CliExportContext.create(main, printer, true);
- assertThatThrownBy(() -> context.getPlugins(null))
- .isInstanceOf(CliException.class)
- .hasMessageContaining("camel-jbang-plugin-exporter not found");
+ assertThat(context.getPlugins(null).exporters()).isEmpty();
+ assertThat(printer.getStderr())
+ .contains("Plugin exporter cannot be loaded
(camel-jbang-plugin-exporter not found), so camel export");
}
// ---- Known PluginType appliesTo is used at runtime without config entry
----
@@ -298,14 +299,15 @@ class PluginFilteringTest {
entry.put("resolved", resolved);
writeConfig(entry);
- CamelJBangMain main = new CamelJBangMain();
+ StringPrinter printer = new StringPrinter();
+ CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
// The resolved.appliesTo says "export", but the camelVersion is stale
so it is ignored.
// With legacyContributors=true, the plugin passes the filter for
"run" (treated as legacy).
- // Resolution fails — the exception proves the stale appliesTo did not
filter it out.
- assertThatThrownBy(() -> PluginHelper.getActivePlugins(main, null,
"run", true))
- .isInstanceOf(CliException.class)
- .hasMessageContaining("camel-jbang-plugin-exporter not found");
+ // Resolution fails — the warning proves the stale appliesTo did not
filter it out.
+ assertThat(PluginHelper.getActivePlugins(main, null, "run",
true)).isEmpty();
+ assertThat(printer.getStderr())
+ .contains("Plugin exporter cannot be loaded
(camel-jbang-plugin-exporter not found), so camel run");
}
// ---- CliExportContext should not resolve unrelated plugins (with
explicit appliesTo) ----
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
index dd539742aefd..6660c74c0c90 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
@@ -242,6 +242,29 @@ public class PluginHelperTest {
assertFalse(main.quitCalled, "the error ends the command, not the
JVM");
}
+ @Test
+ public void aPluginThatCannotBeLoadedDoesNotFailCamelRun() throws
Exception {
+ Path jar = tempDir.resolve("fake-plugin.jar");
+ FakePluginJar.write(jar, "fake");
+ String camelVersion = new
org.apache.camel.catalog.DefaultCamelCatalog().getCatalogVersion();
+ long staleMtime = Files.getLastModifiedTime(jar).toMillis() - 1000;
+ // without appliesTo the plugin contributes to camel run and camel
export, and it cannot be resolved
+ writeConfig(buildEntry("fake", camelVersion, jar, staleMtime));
+ StringPrinter printer = new StringPrinter();
+ QuitCapture main = new QuitCapture();
+ main.withPrinter(printer);
+
+ // before, camel run failed for it (CAMEL-26931): it is left out with
a warning
+ Map<String, Plugin> plugins = assertDoesNotThrow(() ->
PluginHelper.getActivePlugins(main, null, "run", true));
+
+ assertTrue(plugins.isEmpty());
+ assertTrue(printer.getStderr().contains("Plugin fake cannot be loaded
(camel-jbang-plugin-fake not found), so camel"
+ + " run goes on without it:
remove it with camel plugin delete fake"),
+ printer.getStderr());
+ // its own command still fails
+ assertThrows(CliException.class, () ->
PluginHelper.getActivePlugins(main, null, "fake"));
+ }
+
@Test
public void testCachedPluginThatDoesNotLinkIsResolvedAgain() throws
Exception {
Path jar = tempDir.resolve("unlinked-plugin.jar");