From 63dac7d61d656c4734195b06c242a9c382db781f Mon Sep 17 00:00:00 2001 From: ChangYu Huang Date: Tue, 30 Sep 2025 15:30:03 -0400 Subject: [PATCH 1/4] Add a test to catch exception when calling version-mapping without a bootstrap --- .../main/java/org/apache/kafka/tools/FeatureCommand.java | 2 +- .../java/org/apache/kafka/tools/FeatureCommandTest.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java index 87e4c228baf2e..e36bc6790fc51 100644 --- a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java +++ b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java @@ -92,7 +92,7 @@ static void execute(String... args) throws Exception { addVersionMappingParser(subparsers); addFeatureDependenciesParser(subparsers); - Namespace namespace = parser.parseArgsOrFail(args); + Namespace namespace = parser.parseArgs(args); String command = namespace.getString("command"); String configPath = namespace.getString("command_config"); Properties properties = (configPath == null) ? new Properties() : Utils.loadProps(configPath); diff --git a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java index 2caaf8a2918e8..9c08f90f5e321 100644 --- a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java +++ b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java @@ -459,6 +459,15 @@ public void testHandleVersionMappingWithInvalidReleaseVersion() { MetadataVersion.MINIMUM_VERSION, MetadataVersion.latestTesting()), exception2.getMessage()); } + @Test + public void testHandleVersionMappingWithoutBootstrap() { + Map.Entry output = ToolsTestUtils.grabConsoleOutputAndError(() -> FeatureCommand.mainNoExit("version-mapping")); + assertEquals("", output.getValue()); + MetadataVersion metadataVersion = MetadataVersion.latestProduction(); + assertTrue(output.getKey().contains("metadata.version=" + metadataVersion.featureLevel() + " (" + metadataVersion.version() + ")"), + "Output did not contain expected Metadata Version: " + output.getKey()); + } + @Test public void testHandleFeatureDependenciesForFeatureWithDependencies() { Map namespace = new HashMap<>(); From 3940027e878d3f67efce8cb4ee2c833c00919fb4 Mon Sep 17 00:00:00 2001 From: ChangYu Huang Date: Tue, 30 Sep 2025 18:45:32 -0400 Subject: [PATCH 2/4] Add a test to catch exception when calling a remote command without a bootstrap --- .../java/org/apache/kafka/tools/FeatureCommandTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java index 9c08f90f5e321..f1ca441992cf9 100644 --- a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java +++ b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java @@ -468,6 +468,12 @@ public void testHandleVersionMappingWithoutBootstrap() { "Output did not contain expected Metadata Version: " + output.getKey()); } + @Test + public void testHandleRemoteCommandWithoutBootstrap() { + String errorMessage = ToolsTestUtils.grabConsoleError(() -> FeatureCommand.mainNoExit("upgrade")); + assertTrue(errorMessage.contains("one of the arguments --bootstrap-server --bootstrap-controller is required")); + } + @Test public void testHandleFeatureDependenciesForFeatureWithDependencies() { Map namespace = new HashMap<>(); From c8ccf23c881aa9dc18f751661de2bf6f8d9e7b2d Mon Sep 17 00:00:00 2001 From: ChangYu Huang Date: Tue, 30 Sep 2025 18:49:40 -0400 Subject: [PATCH 3/4] Set bootstrapGroup not-required and check their existence if they are needed --- .../apache/kafka/tools/FeatureCommand.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java index e36bc6790fc51..e3a4ca8b55749 100644 --- a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java +++ b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java @@ -76,7 +76,7 @@ static void execute(String... args) throws Exception { .newArgumentParser("kafka-features") .defaultHelp(true) .description("This tool manages feature flags in Kafka."); - MutuallyExclusiveGroup bootstrapGroup = parser.addMutuallyExclusiveGroup().required(true); + MutuallyExclusiveGroup bootstrapGroup = parser.addMutuallyExclusiveGroup(); bootstrapGroup.addArgument("--bootstrap-server") .help("A comma-separated list of host:port pairs to use for establishing the connection to the Kafka cluster."); bootstrapGroup.addArgument("--bootstrap-controller") @@ -94,12 +94,25 @@ static void execute(String... args) throws Exception { Namespace namespace = parser.parseArgs(args); String command = namespace.getString("command"); + if (command.equals("version-mapping")) { + handleVersionMapping(namespace, Feature.PRODUCTION_FEATURES); + return; + } else if (command.equals("feature-dependencies")) { + handleFeatureDependencies(namespace, Feature.PRODUCTION_FEATURES); + return; + } String configPath = namespace.getString("command_config"); Properties properties = (configPath == null) ? new Properties() : Utils.loadProps(configPath); - CommandLineUtils.initializeBootstrapProperties(properties, - Optional.ofNullable(namespace.getString("bootstrap_server")), - Optional.ofNullable(namespace.getString("bootstrap_controller"))); + try { + CommandLineUtils.initializeBootstrapProperties(properties, + Optional.ofNullable(namespace.getString("bootstrap_server")), + Optional.ofNullable(namespace.getString("bootstrap_controller"))); + } catch (Exception e) { + // bootstrap_server and bootstrap_controller are in a mutually exclusive group, + // so the exception happens only when both of them are missing + throw new ArgumentParserException("one of the arguments --bootstrap-server --bootstrap-controller is required", parser); + } try (Admin adminClient = Admin.create(properties)) { switch (command) { @@ -115,12 +128,6 @@ static void execute(String... args) throws Exception { case "disable": handleDisable(namespace, adminClient); break; - case "version-mapping": - handleVersionMapping(namespace, Feature.PRODUCTION_FEATURES); - break; - case "feature-dependencies": - handleFeatureDependencies(namespace, Feature.PRODUCTION_FEATURES); - break; default: throw new TerseException("Unknown command " + command); } From d6679a8b6342a8eabb4c97dadc7697fe78bffb0f Mon Sep 17 00:00:00 2001 From: ChangYu Huang Date: Thu, 2 Oct 2025 12:06:14 -0400 Subject: [PATCH 4/4] Use error message from initializeBootstrapProperties --- tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java | 2 +- .../test/java/org/apache/kafka/tools/FeatureCommandTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java index e3a4ca8b55749..ff29936481fe0 100644 --- a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java +++ b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java @@ -111,7 +111,7 @@ static void execute(String... args) throws Exception { } catch (Exception e) { // bootstrap_server and bootstrap_controller are in a mutually exclusive group, // so the exception happens only when both of them are missing - throw new ArgumentParserException("one of the arguments --bootstrap-server --bootstrap-controller is required", parser); + throw new ArgumentParserException(e.getMessage(), parser); } try (Admin adminClient = Admin.create(properties)) { diff --git a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java index f1ca441992cf9..01433dc674550 100644 --- a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java +++ b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java @@ -471,7 +471,8 @@ public void testHandleVersionMappingWithoutBootstrap() { @Test public void testHandleRemoteCommandWithoutBootstrap() { String errorMessage = ToolsTestUtils.grabConsoleError(() -> FeatureCommand.mainNoExit("upgrade")); - assertTrue(errorMessage.contains("one of the arguments --bootstrap-server --bootstrap-controller is required")); + assertTrue(errorMessage.contains("You must specify either --bootstrap-controller " + + "or --bootstrap-server.")); } @Test