Browse Source

Add component info versions to node info in a pluggable way (#99631)

This adds a `ComponentVersionNumber` service interface for modules to provide version numbers for individual components to be reported inside node info. Initial implementations for `MlConfigVersion` and `TransformConfigVersion` are provided.
Simon Cooper 2 years ago
parent
commit
3f32affbb6
24 changed files with 214 additions and 10 deletions
  1. 5 0
      docs/changelog/99631.yaml
  2. 13 0
      docs/reference/cluster/nodes-info.asciidoc
  3. 10 10
      server/src/internalClusterTest/java/org/elasticsearch/nodesinfo/SimpleNodesInfoIT.java
  4. 1 0
      server/src/main/java/module-info.java
  5. 1 0
      server/src/main/java/org/elasticsearch/TransportVersions.java
  6. 26 0
      server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/ComponentVersionNumber.java
  7. 18 0
      server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodeInfo.java
  8. 5 0
      server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoResponse.java
  9. 20 0
      server/src/main/java/org/elasticsearch/node/NodeService.java
  10. 3 0
      server/src/test/java/org/elasticsearch/action/admin/cluster/node/info/NodeInfoTests.java
  11. 3 0
      server/src/test/java/org/elasticsearch/action/admin/cluster/remote/RemoteClusterNodesActionTests.java
  12. 1 0
      server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java
  13. 1 0
      server/src/test/java/org/elasticsearch/action/ingest/ReservedPipelineActionTests.java
  14. 1 0
      server/src/test/java/org/elasticsearch/cluster/service/TransportVersionsFixupListenerTests.java
  15. 6 0
      server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java
  16. 2 0
      server/src/test/java/org/elasticsearch/rest/action/cat/RestPluginsActionTests.java
  17. 2 0
      x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/capacity/nodeinfo/AutoscalingNodesInfoServiceTests.java
  18. 39 0
      x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/nodesinfo/ComponentVersionsNodesInfoIT.java
  19. 5 0
      x-pack/plugin/core/src/main/java/module-info.java
  20. 23 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlConfigVersionComponent.java
  21. 23 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformConfigVersionComponent.java
  22. 2 0
      x-pack/plugin/core/src/main/resources/META-INF/services/org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber
  23. 1 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportNodeEnrollmentActionTests.java
  24. 3 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java

+ 5 - 0
docs/changelog/99631.yaml

@@ -0,0 +1,5 @@
+pr: 99631
+summary: Add component info versions to node info in a pluggable way
+area: Infra/REST API
+type: enhancement
+issues: []

+ 13 - 0
docs/reference/cluster/nodes-info.asciidoc

@@ -139,6 +139,9 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=node-id]
 `index_version`::
 		The most recent index version that this node can read.
 
+`component_versions`::
+		The version numbers of individual components loaded in this node.
+
 The `os` flag can be set to retrieve information that concern the operating
 system:
 
@@ -236,6 +239,10 @@ The API returns the following response:
       "version": "{version}",
       "transport_version": 100000298,
       "index_version": 100000074,
+      "component_versions": {
+        "ml_config_version": 100000162,
+        "transform_config_version": 100000096
+      },
       "build_flavor": "default",
       "build_type": "{build_type}",
       "build_hash": "587409e",
@@ -276,6 +283,7 @@ The API returns the following response:
 // TESTRESPONSE[s/"ip": "192.168.17"/"ip": $body.$_path/]
 // TESTRESPONSE[s/"transport_version": 100000298/"transport_version": $body.$_path/]
 // TESTRESPONSE[s/"index_version": 100000074/"index_version": $body.$_path/]
+// TESTRESPONSE[s/"component_versions": \{[^\}]*\}/"component_versions": $body.$_path/]
 // TESTRESPONSE[s/"build_hash": "587409e"/"build_hash": $body.$_path/]
 // TESTRESPONSE[s/"roles": \[[^\]]*\]/"roles": $body.$_path/]
 // TESTRESPONSE[s/"attributes": \{[^\}]*\}/"attributes": $body.$_path/]
@@ -311,6 +319,10 @@ The API returns the following response:
       "version": "{version}",
       "transport_version": 100000298,
       "index_version": 100000074,
+      "component_versions": {
+        "ml_config_version": 100000162,
+        "transform_config_version": 100000096
+      },
       "build_flavor": "default",
       "build_type": "{build_type}",
       "build_hash": "587409e",
@@ -375,6 +387,7 @@ The API returns the following response:
 // TESTRESPONSE[s/"ip": "192.168.17"/"ip": $body.$_path/]
 // TESTRESPONSE[s/"transport_version": 100000298/"transport_version": $body.$_path/]
 // TESTRESPONSE[s/"index_version": 100000074/"index_version": $body.$_path/]
+// TESTRESPONSE[s/"component_versions": \{[^\}]*\}/"component_versions": $body.$_path/]
 // TESTRESPONSE[s/"build_hash": "587409e"/"build_hash": $body.$_path/]
 // TESTRESPONSE[s/"roles": \[[^\]]*\]/"roles": $body.$_path/]
 // TESTRESPONSE[s/"attributes": \{[^\}]*\}/"attributes": $body.$_path/]

+ 10 - 10
server/src/internalClusterTest/java/org/elasticsearch/nodesinfo/SimpleNodesInfoIT.java

@@ -23,7 +23,7 @@ import java.util.List;
 
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
-import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.notNullValue;
 
 @ClusterScope(scope = Scope.TEST, numDataNodes = 0)
@@ -42,29 +42,29 @@ public class SimpleNodesInfoIT extends ESIntegTestCase {
         logger.info("--> started nodes: {} and {}", server1NodeId, server2NodeId);
 
         NodesInfoResponse response = clusterAdmin().prepareNodesInfo().execute().actionGet();
-        assertThat(response.getNodes().size(), is(2));
+        assertThat(response.getNodes(), hasSize(2));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
         assertThat(response.getNodesMap().get(server2NodeId), notNullValue());
 
         response = clusterAdmin().nodesInfo(new NodesInfoRequest()).actionGet();
-        assertThat(response.getNodes().size(), is(2));
+        assertThat(response.getNodes(), hasSize(2));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
         assertThat(response.getNodesMap().get(server2NodeId), notNullValue());
 
         response = clusterAdmin().nodesInfo(new NodesInfoRequest(server1NodeId)).actionGet();
-        assertThat(response.getNodes().size(), is(1));
+        assertThat(response.getNodes(), hasSize(1));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
 
         response = clusterAdmin().nodesInfo(new NodesInfoRequest(server1NodeId)).actionGet();
-        assertThat(response.getNodes().size(), is(1));
+        assertThat(response.getNodes(), hasSize(1));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
 
         response = clusterAdmin().nodesInfo(new NodesInfoRequest(server2NodeId)).actionGet();
-        assertThat(response.getNodes().size(), is(1));
+        assertThat(response.getNodes(), hasSize(1));
         assertThat(response.getNodesMap().get(server2NodeId), notNullValue());
 
         response = clusterAdmin().nodesInfo(new NodesInfoRequest(server2NodeId)).actionGet();
-        assertThat(response.getNodes().size(), is(1));
+        assertThat(response.getNodes(), hasSize(1));
         assertThat(response.getNodesMap().get(server2NodeId), notNullValue());
     }
 
@@ -81,7 +81,7 @@ public class SimpleNodesInfoIT extends ESIntegTestCase {
         logger.info("--> started nodes: {} and {}", server1NodeId, server2NodeId);
 
         NodesInfoResponse response = clusterAdmin().prepareNodesInfo().execute().actionGet();
-        assertThat(response.getNodes().size(), is(2));
+        assertThat(response.getNodes(), hasSize(2));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
         assertNotNull(response.getNodesMap().get(server1NodeId).getTotalIndexingBuffer());
         assertThat(response.getNodesMap().get(server1NodeId).getTotalIndexingBuffer().getBytes(), greaterThan(0L));
@@ -92,7 +92,7 @@ public class SimpleNodesInfoIT extends ESIntegTestCase {
 
         // again, using only the indices flag
         response = clusterAdmin().prepareNodesInfo().clear().setIndices(true).execute().actionGet();
-        assertThat(response.getNodes().size(), is(2));
+        assertThat(response.getNodes(), hasSize(2));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
         assertNotNull(response.getNodesMap().get(server1NodeId).getTotalIndexingBuffer());
         assertThat(response.getNodesMap().get(server1NodeId).getTotalIndexingBuffer().getBytes(), greaterThan(0L));
@@ -120,7 +120,7 @@ public class SimpleNodesInfoIT extends ESIntegTestCase {
 
         NodesInfoResponse response = clusterAdmin().prepareNodesInfo().execute().actionGet();
 
-        assertThat(response.getNodes().size(), is(2));
+        assertThat(response.getNodes(), hasSize(2));
         assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
         assertThat(response.getNodesMap().get(server2NodeId), notNullValue());
 

+ 1 - 0
server/src/main/java/module-info.java

@@ -399,6 +399,7 @@ module org.elasticsearch.server {
     uses org.elasticsearch.internal.BuildExtension;
     uses org.elasticsearch.plugins.internal.SettingsExtension;
     uses RestExtension;
+    uses org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber;
 
     provides org.apache.lucene.codecs.PostingsFormat
         with

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -144,6 +144,7 @@ public class TransportVersions {
     public static final TransportVersion COMMIT_PRIMARY_TERM_GENERATION = def(8_501_00_1);
     public static final TransportVersion WAIT_FOR_CLUSTER_STATE_IN_RECOVERY_ADDED = def(8_502_00_0);
     public static final TransportVersion RECOVERY_COMMIT_TOO_NEW_EXCEPTION_ADDED = def(8_503_00_0);
+    public static final TransportVersion NODE_INFO_COMPONENT_VERSIONS_ADDED = def(8_504_00_0);
     /*
      * STOP! READ THIS FIRST! No, really,
      *        ____ _____ ___  ____  _        ____  _____    _    ____    _____ _   _ ___ ____    _____ ___ ____  ____ _____ _

+ 26 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/ComponentVersionNumber.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.node.info;
+
+import org.elasticsearch.common.VersionId;
+
+/**
+ * Represents a version number of a subsidiary component to be reported in node info
+ */
+public interface ComponentVersionNumber {
+    /**
+     * Returns the component id to report this number under. This should be in snake_case.
+     */
+    String componentId();
+
+    /**
+     * Returns the version id to report.
+     */
+    VersionId<?> versionNumber();
+}

+ 18 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodeInfo.java

@@ -43,6 +43,7 @@ public class NodeInfo extends BaseNodeResponse {
     private final Version version;
     private final TransportVersion transportVersion;
     private final IndexVersion indexVersion;
+    private final Map<String, Integer> componentVersions;
     private final Build build;
 
     @Nullable
@@ -71,6 +72,11 @@ public class NodeInfo extends BaseNodeResponse {
         } else {
             indexVersion = IndexVersion.fromId(version.id);
         }
+        if (in.getTransportVersion().onOrAfter(TransportVersions.NODE_INFO_COMPONENT_VERSIONS_ADDED)) {
+            componentVersions = in.readImmutableMap(StreamInput::readString, StreamInput::readVInt);
+        } else {
+            componentVersions = Map.of();
+        }
         build = Build.readBuild(in);
         if (in.readBoolean()) {
             totalIndexingBuffer = ByteSizeValue.ofBytes(in.readLong());
@@ -102,6 +108,7 @@ public class NodeInfo extends BaseNodeResponse {
         Version version,
         TransportVersion transportVersion,
         IndexVersion indexVersion,
+        Map<String, Integer> componentVersions,
         Build build,
         DiscoveryNode node,
         @Nullable Settings settings,
@@ -121,6 +128,7 @@ public class NodeInfo extends BaseNodeResponse {
         this.version = version;
         this.transportVersion = transportVersion;
         this.indexVersion = indexVersion;
+        this.componentVersions = componentVersions;
         this.build = build;
         this.settings = settings;
         addInfoIfNonNull(OsInfo.class, os);
@@ -165,6 +173,13 @@ public class NodeInfo extends BaseNodeResponse {
         return indexVersion;
     }
 
+    /**
+     * The version numbers of other installed components
+     */
+    public Map<String, Integer> getComponentVersions() {
+        return componentVersions;
+    }
+
     /**
      * The build version of the node.
      */
@@ -219,6 +234,9 @@ public class NodeInfo extends BaseNodeResponse {
         if (out.getTransportVersion().onOrAfter(TransportVersions.NODE_INFO_INDEX_VERSION_ADDED)) {
             IndexVersion.writeVersion(indexVersion, out);
         }
+        if (out.getTransportVersion().onOrAfter(TransportVersions.NODE_INFO_COMPONENT_VERSIONS_ADDED)) {
+            out.writeMap(componentVersions, StreamOutput::writeString, StreamOutput::writeVInt);
+        }
         Build.writeBuild(build, out);
         if (totalIndexingBuffer == null) {
             out.writeBoolean(false);

+ 5 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoResponse.java

@@ -66,6 +66,11 @@ public class NodesInfoResponse extends BaseNodesResponse<NodeInfo> implements To
             builder.field("version", nodeInfo.getVersion());
             builder.field("transport_version", nodeInfo.getTransportVersion().id());
             builder.field("index_version", nodeInfo.getIndexVersion().id());
+            builder.startObject("component_versions");
+            for (var cv : nodeInfo.getComponentVersions().entrySet()) {
+                builder.field(cv.getKey(), cv.getValue());
+            }
+            builder.endObject();
             builder.field("build_flavor", nodeInfo.getBuild().flavor());
             builder.field("build_type", nodeInfo.getBuild().type().displayName());
             builder.field("build_hash", nodeInfo.getBuild().hash());

+ 20 - 0
server/src/main/java/org/elasticsearch/node/NodeService.java

@@ -11,6 +11,7 @@ package org.elasticsearch.node;
 import org.elasticsearch.Build;
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber;
 import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
 import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
@@ -19,6 +20,7 @@ import org.elasticsearch.cluster.coordination.Coordinator;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsFilter;
+import org.elasticsearch.core.Assertions;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.http.HttpServerTransport;
@@ -37,7 +39,10 @@ import org.elasticsearch.transport.TransportService;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 public class NodeService implements Closeable {
     private final Settings settings;
@@ -116,6 +121,7 @@ public class NodeService implements Closeable {
             Version.CURRENT,
             TransportVersion.current(),
             IndexVersion.current(),
+            findComponentVersions(),
             Build.current(),
             transportService.getLocalNode(),
             settings ? settingsFilter.filter(this.settings) : null,
@@ -133,6 +139,20 @@ public class NodeService implements Closeable {
         );
     }
 
+    private Map<String, Integer> findComponentVersions() {
+        var versions = pluginService.loadServiceProviders(ComponentVersionNumber.class)
+            .stream()
+            .collect(Collectors.toUnmodifiableMap(ComponentVersionNumber::componentId, cvn -> cvn.versionNumber().id()));
+
+        if (Assertions.ENABLED) {
+            var isSnakeCase = Pattern.compile("[a-z_]+").asMatchPredicate();
+            for (String key : versions.keySet()) {
+                assert isSnakeCase.test(key) : "Version component " + key + " should use snake_case";
+            }
+        }
+        return versions;
+    }
+
     public NodeStats stats(
         CommonStatsFlags indices,
         boolean os,

+ 3 - 0
server/src/test/java/org/elasticsearch/action/admin/cluster/node/info/NodeInfoTests.java

@@ -19,6 +19,8 @@ import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.VersionUtils;
 import org.elasticsearch.test.index.IndexVersionUtils;
 
+import java.util.Map;
+
 import static java.util.Collections.emptySet;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
@@ -39,6 +41,7 @@ public class NodeInfoTests extends ESTestCase {
             Version.CURRENT,
             TransportVersion.current(),
             IndexVersion.current(),
+            Map.of(),
             Build.current(),
             DiscoveryNodeUtils.builder("test_node")
                 .roles(emptySet())

+ 3 - 0
server/src/test/java/org/elasticsearch/action/admin/cluster/remote/RemoteClusterNodesActionTests.java

@@ -34,6 +34,7 @@ import org.elasticsearch.transport.TransportService;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -80,6 +81,7 @@ public class RemoteClusterNodesActionTests extends ESTestCase {
                     Version.CURRENT,
                     TransportVersion.current(),
                     IndexVersion.current(),
+                    Map.of(),
                     null,
                     node,
                     null,
@@ -150,6 +152,7 @@ public class RemoteClusterNodesActionTests extends ESTestCase {
                     Version.CURRENT,
                     TransportVersion.current(),
                     IndexVersion.current(),
+                    Map.of(),
                     null,
                     node,
                     null,

+ 1 - 0
server/src/test/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsNodesTests.java

@@ -325,6 +325,7 @@ public class ClusterStatsNodesTests extends ESTestCase {
             Version.CURRENT,
             TransportVersion.current(),
             IndexVersion.current(),
+            Map.of(),
             Build.current(),
             DiscoveryNodeUtils.create(nodeId, buildNewFakeTransportAddress()),
             settings.build(),

+ 1 - 0
server/src/test/java/org/elasticsearch/action/ingest/ReservedPipelineActionTests.java

@@ -103,6 +103,7 @@ public class ReservedPipelineActionTests extends ESTestCase {
             Version.CURRENT,
             TransportVersion.current(),
             IndexVersion.current(),
+            Map.of(),
             Build.current(),
             discoveryNode,
             Settings.EMPTY,

+ 1 - 0
server/src/test/java/org/elasticsearch/cluster/service/TransportVersionsFixupListenerTests.java

@@ -87,6 +87,7 @@ public class TransportVersionsFixupListenerTests extends ESTestCase {
                         e.getValue(),
                         null,
                         null,
+                        null,
                         DiscoveryNodeUtils.create(e.getKey(), new TransportAddress(TransportAddress.META_ADDRESS, 9200)),
                         null,
                         null,

+ 6 - 0
server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java

@@ -49,6 +49,8 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import static java.util.Collections.emptySet;
 import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
@@ -107,6 +109,9 @@ public class NodeInfoStreamingTests extends ESTestCase {
     }
 
     private static NodeInfo createNodeInfo() {
+        Map<String, Integer> componentVersions = IntStream.range(0, randomInt(5))
+            .boxed()
+            .collect(Collectors.toUnmodifiableMap(i -> randomAlphaOfLength(10), i -> randomInt(Integer.MAX_VALUE)));
         Build build = Build.current();
         DiscoveryNode node = DiscoveryNodeUtils.builder("test_node")
             .roles(emptySet())
@@ -233,6 +238,7 @@ public class NodeInfoStreamingTests extends ESTestCase {
             VersionUtils.randomVersion(random()),
             TransportVersionUtils.randomVersion(random()),
             IndexVersionUtils.randomVersion(random()),
+            componentVersions,
             build,
             node,
             settings,

+ 2 - 0
server/src/test/java/org/elasticsearch/rest/action/cat/RestPluginsActionTests.java

@@ -29,6 +29,7 @@ import org.elasticsearch.test.rest.FakeRestRequest;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static org.hamcrest.Matchers.empty;
@@ -66,6 +67,7 @@ public class RestPluginsActionTests extends ESTestCase {
                     Version.CURRENT,
                     TransportVersion.current(),
                     IndexVersion.current(),
+                    Map.of(),
                     null,
                     node(i),
                     null,

+ 2 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/capacity/nodeinfo/AutoscalingNodesInfoServiceTests.java

@@ -53,6 +53,7 @@ import org.junit.Before;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.RejectedExecutionException;
@@ -452,6 +453,7 @@ public class AutoscalingNodesInfoServiceTests extends AutoscalingTestCase {
             Version.CURRENT,
             TransportVersion.current(),
             IndexVersion.current(),
+            Map.of(),
             Build.current(),
             node,
             null,

+ 39 - 0
x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/nodesinfo/ComponentVersionsNodesInfoIT.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.nodesinfo;
+
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
+import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.test.ESIntegTestCase;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.notNullValue;
+
+public class ComponentVersionsNodesInfoIT extends ESIntegTestCase {
+
+    public void testNodesInfoComponentVersions() {
+        List<String> nodesIds = internalCluster().startNodes(1);
+        final String node_1 = nodesIds.get(0);
+
+        ClusterHealthResponse clusterHealth = clusterAdmin().prepareHealth().setWaitForGreenStatus().setWaitForNodes("1").get();
+        logger.info("--> done cluster_health, status {}", clusterHealth.getStatus());
+
+        String server1NodeId = internalCluster().getInstance(ClusterService.class, node_1).state().nodes().getLocalNodeId();
+        logger.info("--> started nodes: {}", server1NodeId);
+
+        NodesInfoResponse response = clusterAdmin().prepareNodesInfo().execute().actionGet();
+        assertThat(response.getNodesMap().get(server1NodeId), notNullValue());
+        assertThat(
+            response.getNodesMap().get(server1NodeId).getComponentVersions().keySet(),
+            containsInAnyOrder("transform_config_version", "ml_config_version")
+        );
+    }
+}

+ 5 - 0
x-pack/plugin/core/src/main/java/module-info.java

@@ -221,4 +221,9 @@ module org.elasticsearch.xcore {
     exports org.elasticsearch.xpack.core.watcher.trigger;
     exports org.elasticsearch.xpack.core.watcher.watch;
     exports org.elasticsearch.xpack.core.watcher;
+
+    provides org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber
+        with
+            org.elasticsearch.xpack.core.ml.MlConfigVersionComponent,
+            org.elasticsearch.xpack.core.transform.TransformConfigVersionComponent;
 }

+ 23 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlConfigVersionComponent.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.ml;
+
+import org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber;
+import org.elasticsearch.common.VersionId;
+
+public class MlConfigVersionComponent implements ComponentVersionNumber {
+    @Override
+    public String componentId() {
+        return "ml_config_version";
+    }
+
+    @Override
+    public VersionId<?> versionNumber() {
+        return MlConfigVersion.CURRENT;
+    }
+}

+ 23 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformConfigVersionComponent.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.transform;
+
+import org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber;
+import org.elasticsearch.common.VersionId;
+
+public class TransformConfigVersionComponent implements ComponentVersionNumber {
+    @Override
+    public String componentId() {
+        return "transform_config_version";
+    }
+
+    @Override
+    public VersionId<?> versionNumber() {
+        return TransformConfigVersion.CURRENT;
+    }
+}

+ 2 - 0
x-pack/plugin/core/src/main/resources/META-INF/services/org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber

@@ -0,0 +1,2 @@
+org.elasticsearch.xpack.core.ml.MlConfigVersionComponent
+org.elasticsearch.xpack.core.transform.TransformConfigVersionComponent

+ 1 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportNodeEnrollmentActionTests.java

@@ -105,6 +105,7 @@ public class TransportNodeEnrollmentActionTests extends ESTestCase {
                     Version.CURRENT,
                     TransportVersion.current(),
                     IndexVersion.current(),
+                    Map.of(),
                     null,
                     n,
                     null,

+ 3 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java

@@ -54,6 +54,7 @@ import java.nio.file.Path;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static org.elasticsearch.test.ActionListenerUtils.anyActionListener;
@@ -233,6 +234,7 @@ public class InternalEnrollmentTokenGeneratorTests extends ESTestCase {
                         Version.CURRENT,
                         TransportVersion.current(),
                         IndexVersion.current(),
+                        Map.of(),
                         null,
                         DiscoveryNodeUtils.builder("1").name("node-name").roles(Set.of()).build(),
                         null,
@@ -267,6 +269,7 @@ public class InternalEnrollmentTokenGeneratorTests extends ESTestCase {
                         Version.CURRENT,
                         TransportVersion.current(),
                         IndexVersion.current(),
+                        Map.of(),
                         null,
                         DiscoveryNodeUtils.builder("1").name("node-name").roles(Set.of()).build(),
                         null,