This is an automated email from the ASF dual-hosted git repository. christophd pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit afdf38ea81040e1a0eaacfa5bee600bb3f617368 Author: Christoph Deppisch <[email protected]> AuthorDate: Thu Nov 12 08:45:51 2026 +0100 CAMEL-26764: Limit legacy fallback to CliExportContext, validate stale cache - Added legacyContributors parameter to getActivePlugins so only CliExportContext (true) loads legacy plugins without appliesTo for run/export. addPlugins (false/default) skips them, avoiding double-resolution at startup and broken camel cmd. - effectiveAppliesTo now validates resolved.camelVersion before trusting resolved.appliesTo — stale cache from a different Camel version is ignored and the plugin is treated as legacy. - Upgrade guide wording: "run and export" instead of "all built-in commands"; opts in by listing a command; annotation default {} is not cached; PluginAdd reads from known plugins list. - Tests: legacyContributors true/false, stale resolved.appliesTo ignored, broken legacy plugin skipped by addPlugins. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++-- .../dsl/jbang/core/commands/CliExportContext.java | 2 +- .../camel/dsl/jbang/core/common/PluginHelper.java | 28 ++++++--- .../dsl/jbang/core/common/PluginFilteringTest.java | 73 ++++++++++++++++++---- 4 files changed, 89 insertions(+), 25 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 4c554e0fe7fb..0f6161c68faf 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 @@ -1185,12 +1185,13 @@ A CLI plugin can now declare which built-in commands it participates in beyond i `@CamelJBangPlugin(name = "...", firstVersion = "...", appliesTo = {"export"})` so it is loaded during `camel export` but not during `camel run`. -Existing plugins that do not declare `appliesTo` are loaded for all built-in commands as before (backward compatible). -Once a plugin declares `appliesTo` (even an empty array), only the declared commands load it. +Existing plugins that do not declare `appliesTo` are loaded for `camel run` and `camel export` as before (backward +compatible). A plugin opts in to filtering by listing at least one command in `appliesTo`; in the plugin configuration +file, an empty `appliesTo: []` array means the plugin is only loaded for its own command. -When adding a plugin via `camel plugin add`, the `appliesTo` value from the `@CamelJBangPlugin` annotation (for known -Camel plugins) or from the plugin's metadata is stored in the configuration. After a plugin is downloaded for the first -time, its `appliesTo` is learned from its annotation and cached for subsequent invocations. +When adding a plugin via `camel plugin add`, the `appliesTo` value from the list of known Camel plugins is stored in +the configuration. After a plugin is downloaded for the first time, its `appliesTo` is learned from its +`@CamelJBangPlugin` annotation and cached for subsequent invocations. === camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker in a doTry diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java index 4d4767155fb5..e435bc9ee639 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java @@ -59,7 +59,7 @@ public final class CliExportContext { T context, CamelJBangMain main, Printer printer, boolean yes, String target) { context.withPrinter(printer) .withPlugins(repos -> { - Collection<Plugin> plugins = PluginHelper.getActivePlugins(main, repos, target).values(); + Collection<Plugin> plugins = PluginHelper.getActivePlugins(main, repos, target, true).values(); return new ExportContext.ActivePlugins( plugins.stream().map(Plugin::getRunCustomizer).flatMap(Optional::stream).toList(), plugins.stream().map(Plugin::getExporter).flatMap(Optional::stream).toList()); 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 9fb61f22bc64..06d11503d4c0 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 @@ -345,6 +345,19 @@ public final class PluginHelper { * @throws CliException (exit code 1) when a plugin is not found, or needs a newer Camel CLI */ public static Map<String, Plugin> getActivePlugins(CamelJBangMain main, String repos, String target) { + return getActivePlugins(main, repos, target, false); + } + + /** + * Gets the active plugins, with an option to load legacy plugins (those without {@code appliesTo}) for run and + * export targets. {@code CliExportContext} passes {@code true} so that third-party plugins that predate + * {@code appliesTo} keep their run customizers and exporters. {@code addPlugins} passes {@code false} to avoid + * resolving and customizing unrelated plugins at CLI startup. + * + * @param legacyContributors when true and target is "run" or "export", plugins without appliesTo are loaded + */ + public static Map<String, Plugin> getActivePlugins( + CamelJBangMain main, String repos, String target, boolean legacyContributors) { Map<String, Plugin> activePlugins = new HashMap<>(); JsonObject config = getPluginConfig(); if (config != null) { @@ -363,12 +376,13 @@ public final class PluginHelper { // skip plugins that don't match the target command to avoid unnecessary downloads if (target != null && !"shell".equals(target) && !target.equals(command)) { - Collection<?> appliesTo = effectiveAppliesTo(properties, name); + Collection<?> appliesTo = effectiveAppliesTo(properties, name, version); if (appliesTo != null) { if (!appliesTo.contains(target)) { continue; } - } else if (!PLUGIN_CONSUMING_BUILTINS.contains(target)) { + } else if (!legacyContributors + || !("run".equals(target) || "export".equals(target))) { continue; } } @@ -627,17 +641,17 @@ public final class PluginHelper { /** * Returns the effective {@code appliesTo} for a plugin entry, checking (in order) the top-level config, the - * {@code resolved} block from a previous download, and the {@link PluginType} enum for known Camel plugins. Returns - * {@code null} when no {@code appliesTo} information is available — the caller treats this as a legacy plugin that - * should be loaded for all targets. + * {@code resolved} block from a previous download (only when its Camel version matches), and the {@link PluginType} + * enum for known Camel plugins. Returns {@code null} when no {@code appliesTo} information is available — the + * caller treats this as a legacy plugin. */ - private static Collection<?> effectiveAppliesTo(JsonObject properties, String name) { + private static Collection<?> effectiveAppliesTo(JsonObject properties, String name, String camelVersion) { Object obj = properties.get("appliesTo"); if (obj instanceof Collection<?> list) { return list; } JsonObject resolved = properties.getMap("resolved"); - if (resolved != null) { + if (resolved != null && sameCamelVersion(asString(resolved.get("camelVersion")), camelVersion)) { Object resolvedObj = resolved.get("appliesTo"); if (resolvedObj instanceof Collection<?> list) { return list; 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 23eb812bd09f..940179339aa2 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 @@ -106,30 +106,41 @@ class PluginFilteringTest { .containsKey("beta"); } - // ---- Legacy plugins without appliesTo are loaded for built-in commands (backward compat) ---- + // ---- Legacy plugins without appliesTo: backward compat only for CliExportContext ---- @Test - void legacyPluginWithoutAppliesToIsLoadedForRunTarget() throws Exception { + void legacyPluginIsLoadedForRunTargetWithLegacyContributors() throws Exception { writeTwoPluginConfig("alpha", "beta"); CamelJBangMain main = new CamelJBangMain(); - Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "run"); + Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "run", true); assertThat(plugins).containsKey("alpha") .containsKey("beta"); } @Test - void legacyPluginWithoutAppliesToIsLoadedForExportTarget() throws Exception { + void legacyPluginIsLoadedForExportTargetWithLegacyContributors() throws Exception { writeTwoPluginConfig("alpha", "beta"); CamelJBangMain main = new CamelJBangMain(); - Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "export"); + Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "export", true); assertThat(plugins).containsKey("alpha") .containsKey("beta"); } + @Test + void legacyPluginIsSkippedForRunTargetWithoutLegacyContributors() throws Exception { + writeTwoPluginConfig("alpha", "beta"); + + CamelJBangMain main = new CamelJBangMain(); + Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "run"); + + assertThat(plugins).doesNotContainKey("alpha") + .doesNotContainKey("beta"); + } + // ---- A broken unrelated plugin should not break the "run" target ---- @Test @@ -256,6 +267,47 @@ class PluginFilteringTest { assertThat(runPlugins).doesNotContainKey("test"); } + // ---- resolved.appliesTo is only trusted when the Camel version matches ---- + + @Test + void resolvedAppliesToIsUsedWhenCamelVersionMatches() throws Exception { + Path jar = tempDir.resolve("fake-exporter.jar"); + FakePluginJar.write(jar, "exporter"); + + // entry with resolved block matching the current Camel version, appliesTo: ["export"] + JsonObject entry = buildEntry("exporter", camelVersion, jar, Files.getLastModifiedTime(jar).toMillis()); + JsonObject resolved = (JsonObject) entry.getMap("resolved"); + resolved.put("appliesTo", jsonArray("export")); + writeConfig(entry); + + CamelJBangMain main = new CamelJBangMain(); + + // resolved.appliesTo says "export" and the version matches — plugin is skipped for "run" + Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, null, "run", true); + assertThat(plugins).doesNotContainKey("exporter"); + } + + @Test + void staleResolvedAppliesToIsIgnored() throws Exception { + // entry with resolved block from a DIFFERENT Camel version, appliesTo: ["export"] + // No valid jar — resolution will fail, proving the plugin was NOT filtered out. + JsonObject entry = buildBrokenEntry("exporter"); + JsonObject resolved = new JsonObject(); + resolved.put("camelVersion", "0.0.0-stale"); + resolved.put("appliesTo", jsonArray("export")); + entry.put("resolved", resolved); + writeConfig(entry); + + CamelJBangMain main = new CamelJBangMain(); + + // 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"); + } + // ---- CliExportContext should not resolve unrelated plugins (with explicit appliesTo) ---- @Test @@ -336,23 +388,20 @@ class PluginFilteringTest { // ---- A broken unrelated plugin should not break addPlugins for "run" ---- @Test - void brokenUnrelatedPluginDoesNotBreakAddPluginsForRun() throws Exception { + void brokenLegacyPluginDoesNotBreakAddPluginsForRun() throws Exception { Path goodJar = tempDir.resolve("fake-good.jar"); FakePluginJar.write(goodJar, "good"); - // broken plugin with empty appliesTo — explicitly not related to "run" - JsonObject brokenEntry = buildBrokenEntry("broken"); - brokenEntry.put("appliesTo", jsonArray()); - + // broken plugin without appliesTo — legacy entry; addPlugins uses the default + // non-legacy path, so this plugin is skipped for "run" writeConfig( buildEntry("good", camelVersion, goodJar, Files.getLastModifiedTime(goodJar).toMillis()), - brokenEntry); + buildBrokenEntry("broken")); CamelJBangMain main = new CamelJBangMain().withPrinter(new StringPrinter()); CommandTree commandLine = new CommandTree(main); commandLine.addSubcommand("run", new CommandTree(new NoOpCommand())); - // addPlugins for "run" should skip the broken plugin since its appliesTo excludes "run" assertThatCode(() -> PluginHelper.addPlugins(commandLine, main, "run", "foo.yaml")) .doesNotThrowAnyException(); }
