This is an automated email from the ASF dual-hosted git repository.

chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 3b849ff2bdf KAFKA-19663 Unify kafka-broker-api-versions tool to 
cluster-tool and support bootstrap controllers (#20598)
3b849ff2bdf is described below

commit 3b849ff2bdf245722b462d5f3c92f47088fb772a
Author: TaiJuWu <[email protected]>
AuthorDate: Thu Jul 16 19:37:57 2026 +0800

    KAFKA-19663 Unify kafka-broker-api-versions tool to cluster-tool and 
support bootstrap controllers (#20598)
    
    Unify kafka-broker-api-versions tool to cluster-tool and support
    bootstrap controllers.  This change  reuse  current KafkaAdminClient
    describeFeature to get api response by  introducing
    InternalFeatureDescribeFeatureResult.
    
    Reviewers: Chia-Ping Tsai <[email protected]>
---
 bin/kafka-broker-api-versions.sh                   |  1 +
 bin/windows/kafka-broker-api-versions.bat          |  1 +
 .../kafka/clients/admin/DescribeFeaturesTest.java  | 16 ++++
 .../clients/admin/DescribeFeaturesResult.java      |  8 +-
 .../kafka/clients/admin/KafkaAdminClient.java      | 18 ++++-
 .../internals/InternalDescribeFeaturesResult.java  | 47 ++++++++++++
 docs/getting-started/upgrade.md                    |  1 +
 .../java/org/apache/kafka/tools/ClusterTool.java   | 38 +++++++++-
 .../org/apache/kafka/tools/ClusterToolTest.java    | 85 ++++++++++++++++++++++
 9 files changed, 211 insertions(+), 4 deletions(-)

diff --git a/bin/kafka-broker-api-versions.sh b/bin/kafka-broker-api-versions.sh
index 285a9488af0..8659a17d290 100755
--- a/bin/kafka-broker-api-versions.sh
+++ b/bin/kafka-broker-api-versions.sh
@@ -14,4 +14,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+echo "WARNING: kafka-broker-api-versions.sh is deprecated and will be removed 
in the next major release. Use 'kafka-cluster.sh api-versions' instead." 1>&2
 exec $(dirname $0)/kafka-run-class.sh 
org.apache.kafka.tools.BrokerApiVersionsCommand "$@"
diff --git a/bin/windows/kafka-broker-api-versions.bat 
b/bin/windows/kafka-broker-api-versions.bat
index 7c0444bc06f..ba65e9bcde2 100644
--- a/bin/windows/kafka-broker-api-versions.bat
+++ b/bin/windows/kafka-broker-api-versions.bat
@@ -14,4 +14,5 @@ rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.
 rem See the License for the specific language governing permissions and
 rem limitations under the License.
 
+echo WARNING: kafka-broker-api-versions.bat is deprecated and will be removed 
in the next major release. Use "kafka-cluster.bat api-versions" instead. 1>&2
 %~dp0kafka-run-class.bat org.apache.kafka.tools.BrokerApiVersionsCommand %*
diff --git 
a/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/admin/DescribeFeaturesTest.java
 
b/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/admin/DescribeFeaturesTest.java
index 6d1fe1903cc..1f03e8091d2 100644
--- 
a/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/admin/DescribeFeaturesTest.java
+++ 
b/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/admin/DescribeFeaturesTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.kafka.clients.admin;
 
+import org.apache.kafka.clients.admin.internals.InternalDescribeFeaturesResult;
+import org.apache.kafka.common.message.ApiMessageType;
 import org.apache.kafka.common.test.ClusterInstance;
 import org.apache.kafka.common.test.api.ClusterConfigProperty;
 import org.apache.kafka.common.test.api.ClusterTest;
@@ -35,9 +37,23 @@ import java.util.concurrent.ExecutionException;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class DescribeFeaturesTest {
 
+    @ClusterTest(types = {Type.KRAFT})
+    public void testApiVersions(ClusterInstance clusterInstance) throws 
ExecutionException, InterruptedException {
+        try (Admin admin = clusterInstance.admin()) {
+            var versions = ((InternalDescribeFeaturesResult) 
admin.describeFeatures()).nodeApiVersions().get();
+            versions.allSupportedApiVersions().forEach((key, version) -> 
assertTrue(key.inScope(ApiMessageType.ListenerType.BROKER)));
+        }
+
+        try (Admin admin = clusterInstance.admin(Map.of(), true)) {
+            var versions = ((InternalDescribeFeaturesResult) 
admin.describeFeatures()).nodeApiVersions().get();
+            versions.allSupportedApiVersions().forEach((key, version) -> 
assertTrue(key.inScope(ApiMessageType.ListenerType.CONTROLLER)));
+        }
+    }
+
     @ClusterTest(
         types = {Type.KRAFT},
         metadataVersion = MetadataVersion.IBP_3_7_IV0,
diff --git 
a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeFeaturesResult.java
 
b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeFeaturesResult.java
index 2b8c3fa521e..edd5d365249 100644
--- 
a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeFeaturesResult.java
+++ 
b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeFeaturesResult.java
@@ -30,7 +30,13 @@ public class DescribeFeaturesResult {
 
     private final KafkaFuture<FeatureMetadata> future;
 
-    DescribeFeaturesResult(KafkaFuture<FeatureMetadata> future) {
+    /**
+     * This constructor is {@code protected} only to allow internal subclasses 
(e.g.
+     * {@link 
org.apache.kafka.clients.admin.internals.InternalDescribeFeaturesResult}) to 
reuse it.
+     * It is not part of the public API contract, so binary/source 
compatibility for subclassing
+     * outside of the Kafka clients module is not guaranteed.
+     */
+    protected DescribeFeaturesResult(KafkaFuture<FeatureMetadata> future) {
         this.future = future;
     }
 
diff --git 
a/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java 
b/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
index 8f2dcc0e710..6392a1706f2 100644
--- a/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
@@ -28,6 +28,7 @@ import org.apache.kafka.clients.KafkaClient;
 import org.apache.kafka.clients.LeastLoadedNode;
 import org.apache.kafka.clients.MetadataRecoveryStrategy;
 import org.apache.kafka.clients.NetworkClient;
+import org.apache.kafka.clients.NodeApiVersions;
 import org.apache.kafka.clients.StaleMetadataException;
 import 
org.apache.kafka.clients.admin.CreateTopicsResult.TopicMetadataAndConfig;
 import org.apache.kafka.clients.admin.DeleteAclsResult.FilterResult;
@@ -59,6 +60,7 @@ import 
org.apache.kafka.clients.admin.internals.DescribeShareGroupsHandler;
 import org.apache.kafka.clients.admin.internals.DescribeStreamsGroupsHandler;
 import org.apache.kafka.clients.admin.internals.DescribeTransactionsHandler;
 import org.apache.kafka.clients.admin.internals.FenceProducersHandler;
+import org.apache.kafka.clients.admin.internals.InternalDescribeFeaturesResult;
 import 
org.apache.kafka.clients.admin.internals.ListConsumerGroupOffsetsHandler;
 import org.apache.kafka.clients.admin.internals.ListOffsetsHandler;
 import org.apache.kafka.clients.admin.internals.ListShareGroupOffsetsHandler;
@@ -4548,12 +4550,21 @@ public class KafkaAdminClient extends AdminClient {
     @Override
     public DescribeFeaturesResult describeFeatures(final 
DescribeFeaturesOptions options) {
         final KafkaFutureImpl<FeatureMetadata> future = new 
KafkaFutureImpl<>();
+        final KafkaFutureImpl<NodeApiVersions> nodeApiVersionsFuture = new 
KafkaFutureImpl<>();
         final long now = time.milliseconds();
         final NodeProvider nodeProvider = options.nodeId().isPresent() ?
             new ConstantNodeIdProvider(options.nodeId().getAsInt(), true) : 
new LeastLoadedBrokerOrActiveKController();
         final Call call = new Call(
             "describeFeatures", calcDeadlineMs(now, options.timeoutMs()), 
nodeProvider) {
 
+            private NodeApiVersions createNodeApiVersion(final 
ApiVersionsResponse response) {
+                return new NodeApiVersions(
+                    response.data().apiKeys(),
+                    response.data().supportedFeatures(),
+                    response.data().finalizedFeatures(),
+                    response.data().finalizedFeaturesEpoch());
+            }
+
             private FeatureMetadata createFeatureMetadata(final 
ApiVersionsResponse response) {
                 final Map<String, FinalizedVersionRange> finalizedFeatures = 
new HashMap<>();
                 for (final FinalizedFeatureKey key : 
response.data().finalizedFeatures().valuesSet()) {
@@ -4585,8 +4596,11 @@ public class KafkaAdminClient extends AdminClient {
                 final ApiVersionsResponse apiVersionsResponse = 
(ApiVersionsResponse) response;
                 if (apiVersionsResponse.data().errorCode() == 
Errors.NONE.code()) {
                     
future.complete(createFeatureMetadata(apiVersionsResponse));
+                    
nodeApiVersionsFuture.complete(createNodeApiVersion(apiVersionsResponse));
                 } else {
-                    
future.completeExceptionally(Errors.forCode(apiVersionsResponse.data().errorCode()).exception());
+                    Exception exception = 
Errors.forCode(apiVersionsResponse.data().errorCode()).exception();
+                    future.completeExceptionally(exception);
+                    nodeApiVersionsFuture.completeExceptionally(exception);
                 }
             }
 
@@ -4597,7 +4611,7 @@ public class KafkaAdminClient extends AdminClient {
         };
 
         runnable.call(call, now);
-        return new DescribeFeaturesResult(future);
+        return new InternalDescribeFeaturesResult(future, 
nodeApiVersionsFuture);
     }
 
     @Override
diff --git 
a/clients/src/main/java/org/apache/kafka/clients/admin/internals/InternalDescribeFeaturesResult.java
 
b/clients/src/main/java/org/apache/kafka/clients/admin/internals/InternalDescribeFeaturesResult.java
new file mode 100644
index 00000000000..b7e6cd262d3
--- /dev/null
+++ 
b/clients/src/main/java/org/apache/kafka/clients/admin/internals/InternalDescribeFeaturesResult.java
@@ -0,0 +1,47 @@
+/*
+ * 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.kafka.clients.admin.internals;
+
+import org.apache.kafka.clients.NodeApiVersions;
+import org.apache.kafka.clients.admin.DescribeFeaturesResult;
+import org.apache.kafka.clients.admin.FeatureMetadata;
+import org.apache.kafka.common.KafkaFuture;
+
+/**
+ * Internal result class for describeFeatures that exposes API version 
information.
+ * This class is intended for use by internal Kafka tools that need access to 
the raw API versions
+ * returned in the ApiVersionsResponse.
+ */
+public class InternalDescribeFeaturesResult extends DescribeFeaturesResult {
+
+    private final KafkaFuture<NodeApiVersions> nodeApiVersions;
+
+    public InternalDescribeFeaturesResult(KafkaFuture<FeatureMetadata> 
featureMetadata, KafkaFuture<NodeApiVersions> nodeApiVersions) {
+        super(featureMetadata);
+        this.nodeApiVersions = nodeApiVersions;
+    }
+
+    /**
+     * Returns the node API versions future. This contains the API keys and 
version ranges
+     * supported by the broker, as returned in the ApiVersionsResponse.
+     *
+     * @return KafkaFuture containing the node API versions
+     */
+    public KafkaFuture<NodeApiVersions> nodeApiVersions() {
+        return nodeApiVersions;
+    }
+}
diff --git a/docs/getting-started/upgrade.md b/docs/getting-started/upgrade.md
index f960fc36235..ca6ab9923db 100644
--- a/docs/getting-started/upgrade.md
+++ b/docs/getting-started/upgrade.md
@@ -44,6 +44,7 @@ type: docs
     For further details, please refer to 
[KIP-1301](https://cwiki.apache.org/confluence/x/Z5U8G).
   * The broker-side OAUTHBEARER JWT validator now fails fast at startup when a 
JWKS endpoint (`sasl.oauthbearer.jwks.endpoint.url`) is configured but 
`sasl.oauthbearer.expected.audience` or `sasl.oauthbearer.expected.issuer` is 
not set. Brokers that previously started without these settings will now fail 
to start until they are configured. To intentionally accept tokens regardless 
of their audience or issuer, set the new 
`sasl.oauthbearer.allow.unverified.audience` or `sasl.oauthbearer.a [...]
   * When clients connect to the cluster, they now include cluster and node 
information to enable detection and handling of misrouted connections. For 
further details, please refer to 
[KIP-1242](https://cwiki.apache.org/confluence/x/W4LMFw).
+  * The `kafka-cluster.sh` tool now provides an `api-versions` command to 
display the API versions supported by the brokers or controllers, and it 
accepts both `--bootstrap-server` and `--bootstrap-controller`. As a result, 
`kafka-broker-api-versions.sh` is deprecated and will be removed in the next 
major release; use `kafka-cluster.sh api-versions` instead. For further 
details, please refer to 
[KIP-1220](https://cwiki.apache.org/confluence/x/-QkbFw).
   * Brokers can now record a human-readable description of each streams 
group's processing topology via a pluggable backend, retrievable through 
`Admin#describeStreamsGroups` and `kafka-streams-groups.sh --describe 
--topology`. The feature is disabled unless the new broker configuration 
`group.streams.topology.description.plugin.class` is set to a 
`StreamsGroupTopologyDescriptionPlugin` implementation; on the client side, the 
new Kafka Streams configuration `topology.description.push.ena [...]
 
 ## Upgrading to 4.3.0
diff --git a/tools/src/main/java/org/apache/kafka/tools/ClusterTool.java 
b/tools/src/main/java/org/apache/kafka/tools/ClusterTool.java
index 96346874f9c..4342bb07bf3 100644
--- a/tools/src/main/java/org/apache/kafka/tools/ClusterTool.java
+++ b/tools/src/main/java/org/apache/kafka/tools/ClusterTool.java
@@ -16,8 +16,11 @@
  */
 package org.apache.kafka.tools;
 
+import org.apache.kafka.clients.NodeApiVersions;
 import org.apache.kafka.clients.admin.Admin;
 import org.apache.kafka.clients.admin.DescribeClusterOptions;
+import org.apache.kafka.clients.admin.DescribeFeaturesOptions;
+import org.apache.kafka.clients.admin.internals.InternalDescribeFeaturesResult;
 import org.apache.kafka.common.Node;
 import org.apache.kafka.common.errors.UnsupportedVersionException;
 import org.apache.kafka.common.utils.Utils;
@@ -34,9 +37,12 @@ import net.sourceforge.argparse4j.inf.Subparsers;
 
 import java.io.PrintStream;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
+import java.util.TreeMap;
 import java.util.concurrent.ExecutionException;
 
 import static net.sourceforge.argparse4j.impl.Arguments.store;
@@ -75,7 +81,9 @@ public class ClusterTool {
                 .help("Unregister a broker.");
         Subparser listEndpoints = subparsers.addParser("list-endpoints")
                 .help("List endpoints");
-        for (Subparser subpparser : List.of(clusterIdParser, unregisterParser, 
listEndpoints)) {
+        Subparser apiVersionsParser = subparsers.addParser("api-versions")
+                .help("Get information about the api versions of the brokers 
or controllers.");
+        for (Subparser subpparser : List.of(clusterIdParser, unregisterParser, 
listEndpoints, apiVersionsParser)) {
             MutuallyExclusiveGroup connectionOptions = 
subpparser.addMutuallyExclusiveGroup().required(true);
             connectionOptions.addArgument("--bootstrap-server", "-b")
                     .action(store())
@@ -141,6 +149,12 @@ public class ClusterTool {
                 }
                 break;
             }
+            case "api-versions": {
+                try (Admin adminClient = Admin.create(properties)) {
+                    apiVersionsCommand(System.out, adminClient);
+                }
+                break;
+            }
             default:
                 throw new RuntimeException("Unknown command " + command);
         }
@@ -208,4 +222,26 @@ public class ClusterTool {
             }
         }
     }
+
+    static void apiVersionsCommand(PrintStream stream, Admin adminClient) 
throws Exception {
+        Collection<Node> nodes = adminClient.describeCluster().nodes().get();
+        Map<Node, InternalDescribeFeaturesResult> nodeApiVersions = new 
TreeMap<>(Comparator.comparingInt(Node::id));
+        nodes.forEach(node -> {
+            InternalDescribeFeaturesResult result = 
(InternalDescribeFeaturesResult) adminClient.describeFeatures(
+                    new DescribeFeaturesOptions().nodeId(node.id()));
+            nodeApiVersions.put(node, result);
+        });
+
+        nodeApiVersions.forEach((broker, future) -> {
+            try {
+                NodeApiVersions apiVersions = future.nodeApiVersions().get();
+                stream.print(broker + " -> " + apiVersions.toString(true) + 
"\n");
+            } catch (ExecutionException e) {
+                stream.print(broker + " -> ERROR: " + e.getCause() + "\n");
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                stream.print(broker + " -> ERROR: " + e + "\n");
+            }
+        });
+    }
 }
diff --git a/tools/src/test/java/org/apache/kafka/tools/ClusterToolTest.java 
b/tools/src/test/java/org/apache/kafka/tools/ClusterToolTest.java
index 6207cc740fd..bbd5f2d1c23 100644
--- a/tools/src/test/java/org/apache/kafka/tools/ClusterToolTest.java
+++ b/tools/src/test/java/org/apache/kafka/tools/ClusterToolTest.java
@@ -16,12 +16,19 @@
  */
 package org.apache.kafka.tools;
 
+import org.apache.kafka.clients.NodeApiVersions;
 import org.apache.kafka.clients.admin.Admin;
 import org.apache.kafka.clients.admin.AdminClientConfig;
 import org.apache.kafka.clients.admin.MockAdminClient;
+import org.apache.kafka.common.message.ApiMessageType;
+import org.apache.kafka.common.message.ApiVersionsResponseData.ApiVersion;
+import org.apache.kafka.common.protocol.ApiKeys;
+import org.apache.kafka.common.requests.ApiVersionsResponse;
 import org.apache.kafka.common.test.ClusterInstance;
+import org.apache.kafka.common.test.api.ClusterConfigProperty;
 import org.apache.kafka.common.test.api.ClusterTest;
 import org.apache.kafka.common.test.api.Type;
+import org.apache.kafka.server.config.ServerConfigs;
 import org.apache.kafka.test.TestUtils;
 
 import net.sourceforge.argparse4j.inf.ArgumentParserException;
@@ -36,12 +43,16 @@ import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -187,6 +198,80 @@ public class ClusterToolTest {
         assertEquals("--config and --command-config cannot be specified 
together.", ex.getMessage());
     }
 
+    @ClusterTest(types = {Type.KRAFT, Type.CO_KRAFT}, serverProperties = {
+        @ClusterConfigProperty(key = 
ServerConfigs.UNSTABLE_API_VERSIONS_ENABLE_CONFIG, value = "true"),
+    })
+    public void testApiVersionsCommandOutputUsingBrokerServer(ClusterInstance 
clusterInstance) {
+        testApiVersionsCommandOutput(clusterInstance, false);
+    }
+
+    @ClusterTest(types = {Type.KRAFT, Type.CO_KRAFT}, serverProperties = {
+        @ClusterConfigProperty(key = 
ServerConfigs.UNSTABLE_API_VERSIONS_ENABLE_CONFIG, value = "true"),
+    })
+    public void testApiVersionsCommandOutputUsingController(ClusterInstance 
clusterInstance) {
+        testApiVersionsCommandOutput(clusterInstance, true);
+    }
+
+    private void testApiVersionsCommandOutput(ClusterInstance clusterInstance, 
boolean usingBootstrapController) {
+        String output = ToolsTestUtils.grabConsoleOutput(() -> {
+            if (usingBootstrapController) {
+                assertDoesNotThrow(() -> ClusterTool.execute("api-versions", 
"--bootstrap-controller", clusterInstance.bootstrapControllers()));
+            } else {
+                assertDoesNotThrow(() -> ClusterTool.execute("api-versions", 
"--bootstrap-server", clusterInstance.bootstrapServers()));
+            }
+        });
+        Iterator<String> lineIter = 
Arrays.stream(output.split("\n")).iterator();
+        assertTrue(lineIter.hasNext());
+        ApiMessageType.ListenerType listenerType = usingBootstrapController ?
+                ApiMessageType.ListenerType.CONTROLLER : 
ApiMessageType.ListenerType.BROKER;
+
+        if (usingBootstrapController) {
+            int id = clusterInstance.type() == Type.CO_KRAFT ? 0 : 3000;
+            assertEquals(clusterInstance.bootstrapControllers() + " (id: " + 
id + " rack: null isFenced: false) -> (", lineIter.next());
+        } else {
+            assertEquals(clusterInstance.bootstrapServers() + " (id: 0 rack: 
null isFenced: false) -> (", lineIter.next());
+        }
+
+        EnumSet<ApiKeys> apiKeys = EnumSet.copyOf(ApiKeys.clientApis());
+
+        // Controller will return all apis
+        if (listenerType == ApiMessageType.ListenerType.CONTROLLER) {
+            apiKeys.addAll(ApiKeys.controllerApis());
+        }
+
+        Iterator<ApiKeys> apiKeysIter = apiKeys.iterator();
+
+        NodeApiVersions nodeApiVersions = new NodeApiVersions(
+                ApiVersionsResponse.filterApis(listenerType, true, true),
+                List.of());
+        while (apiKeysIter.hasNext()) {
+            ApiKeys apiKey = apiKeysIter.next();
+            String terminator = apiKeysIter.hasNext() ? "," : "";
+            StringBuilder lineBuilder = new StringBuilder().append("\t");
+            if (apiKey.inScope(listenerType)) {
+                ApiVersion apiVersion = nodeApiVersions.apiVersion(apiKey);
+                assertNotNull(apiVersion, "No apiVersion found for " + apiKey);
+
+                String versionRangeStr = (apiVersion.minVersion() == 
apiVersion.maxVersion()) ?
+                        String.valueOf(apiVersion.minVersion()) :
+                        apiVersion.minVersion() + " to " + 
apiVersion.maxVersion();
+                short usableVersion = 
nodeApiVersions.latestUsableVersion(apiKey);
+                if (apiKey == ApiKeys.GET_TELEMETRY_SUBSCRIPTIONS || apiKey == 
ApiKeys.PUSH_TELEMETRY) {
+                    
lineBuilder.append(apiKey.name).append("(").append(apiKey.id).append("): 
UNSUPPORTED").append(terminator);
+                } else {
+                    
lineBuilder.append(apiKey.name).append("(").append(apiKey.id).append("): 
").append(versionRangeStr).append(" [usable: 
").append(usableVersion).append("]").append(terminator);
+                }
+            } else {
+                
lineBuilder.append(apiKey.name).append("(").append(apiKey.id).append("): 
UNSUPPORTED").append(terminator);
+            }
+            assertTrue(lineIter.hasNext());
+            assertEquals(lineBuilder.toString(), lineIter.next());
+        }
+        assertTrue(lineIter.hasNext());
+        assertEquals(")", lineIter.next());
+        assertFalse(lineIter.hasNext());
+    }
+
     @Test
     public void testPrintClusterId() throws Exception {
         Admin adminClient = new MockAdminClient.Builder().

Reply via email to