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-&lt;command&gt;.
+     */
+    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");

Reply via email to