Browse Source

Add pluggable BuildVersion in NodeMetadata (#105757)

Here we introduce a BuildVersion interface that can be created with a 
version ID. That Version can be checked to see if it's before the minimum 
compatibility version of the currently running node, or if it comes after 
the current node's version. We will use the existing BuildExtension to 
retrieve implementations of BuildVersion provided by downstream projects.

* Add interface and implementation for pluggable build version
* Use BuildVersion internally in NodeMetadata
* Use static holder class to only load BuildExtension once
William Brafford 1 year ago
parent
commit
9953b12eb7
18 changed files with 369 additions and 73 deletions
  1. 5 0
      docs/changelog/105757.yaml
  2. 2 2
      server/src/internalClusterTest/java/org/elasticsearch/gateway/GatewayIndexStateIT.java
  3. 125 0
      server/src/main/java/org/elasticsearch/env/BuildVersion.java
  4. 77 0
      server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java
  5. 2 3
      server/src/main/java/org/elasticsearch/env/NodeEnvironment.java
  6. 27 26
      server/src/main/java/org/elasticsearch/env/NodeMetadata.java
  7. 1 1
      server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java
  8. 11 3
      server/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java
  9. 3 1
      server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java
  10. 12 0
      server/src/main/java/org/elasticsearch/internal/BuildExtension.java
  11. 2 2
      server/src/main/java/org/elasticsearch/node/Node.java
  12. 44 0
      server/src/test/java/org/elasticsearch/env/BuildVersionTests.java
  13. 24 14
      server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java
  14. 4 4
      server/src/test/java/org/elasticsearch/env/OverrideNodeVersionCommandTests.java
  15. 4 3
      server/src/test/java/org/elasticsearch/gateway/PersistedClusterStateServiceTests.java
  16. 2 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityImplicitBehaviorBootstrapCheck.java
  17. 22 11
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityImplicitBehaviorBootstrapCheckTests.java
  18. 2 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java

+ 5 - 0
docs/changelog/105757.yaml

@@ -0,0 +1,5 @@
+pr: 105757
+summary: Add pluggable `BuildVersion` in `NodeMetadata`
+area: Infra/Core
+type: enhancement
+issues: []

+ 2 - 2
server/src/internalClusterTest/java/org/elasticsearch/gateway/GatewayIndexStateIT.java

@@ -9,7 +9,6 @@
 package org.elasticsearch.gateway;
 
 import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.Version;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
 import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
@@ -32,6 +31,7 @@ import org.elasticsearch.common.Priority;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.CheckedConsumer;
 import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.NodeEnvironment;
 import org.elasticsearch.env.NodeMetadata;
 import org.elasticsearch.index.IndexVersions;
@@ -564,7 +564,7 @@ public class GatewayIndexStateIT extends ESIntegTestCase {
                     .putCustom(IndexGraveyard.TYPE, IndexGraveyard.builder().addTombstone(metadata.index("test").getIndex()).build())
                     .build()
             );
-            NodeMetadata.FORMAT.writeAndCleanup(new NodeMetadata(nodeId, Version.CURRENT, metadata.oldestIndexVersion()), paths);
+            NodeMetadata.FORMAT.writeAndCleanup(new NodeMetadata(nodeId, BuildVersion.current(), metadata.oldestIndexVersion()), paths);
         });
 
         ensureGreen();

+ 125 - 0
server/src/main/java/org/elasticsearch/env/BuildVersion.java

@@ -0,0 +1,125 @@
+/*
+ * 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.env;
+
+import org.elasticsearch.Build;
+import org.elasticsearch.Version;
+import org.elasticsearch.internal.BuildExtension;
+import org.elasticsearch.plugins.ExtensionLoader;
+
+import java.util.ServiceLoader;
+
+/**
+ * A version representing the code of Elasticsearch
+ *
+ * <p>This class allows us to check whether an Elasticsearch release
+ * is "too old" or "too new," using an intentionally minimal API for
+ * comparisons. The static {@link #current()} method returns the current
+ * release version, and {@link #fromVersionId(int)} returns a version
+ * based on some identifier. By default, this identifier matches what the
+ * {@link Version} class uses, but the implementation is pluggable.
+ * If a module provides a {@link BuildExtension} service via Java SPI, this
+ * class's static methods will return a different implementation of {@link BuildVersion},
+ * potentially with different behavior. This allows downstream projects to
+ * provide versions that accommodate different release models or versioning
+ * schemes.</p>
+ */
+public abstract class BuildVersion {
+
+    /**
+     * Check whether this version is on or after a minimum threshold.
+     *
+     * <p>In some cases, the only thing we need to know about a version is whether
+     * it's compatible with the currently-running Elasticsearch. This method checks
+     * the lower bound, and returns false if the version is "too old."</p>
+     *
+     * <p>By default, the minimum compatible version is derived from {@code Version.CURRENT.minimumCompatibilityVersion()},
+     * but this behavior is pluggable.</p>
+     * @return True if this version is on or after the minimum compatible version
+     * for the currently running Elasticsearch, false otherwise.
+     */
+    public abstract boolean onOrAfterMinimumCompatible();
+
+    /**
+     * Check whether this version comes from a release later than the
+     * currently running Elasticsearch.
+     *
+     * <p>This is useful for checking whether a node would be downgraded.</p>
+     *
+     * @return True if this version represents a release of Elasticsearch later
+     * than the one that's running.
+     */
+    public abstract boolean isFutureVersion();
+
+    // temporary
+    // TODO[wrb]: remove from PersistedClusterStateService
+    // TODO[wrb]: remove from security bootstrap checks
+    @Deprecated
+    public Version toVersion() {
+        return null;
+    }
+
+    /**
+     * Create a {@link BuildVersion} from a version ID number.
+     *
+     * <p>By default, this identifier should match the integer ID of a {@link Version};
+     * see that class for details on the default semantic versioning scheme. This behavior
+     * is, of course, pluggable.</p>
+     *
+     * @param versionId An integer identifier for a version
+     * @return a version representing a build or release of Elasticsearch
+     */
+    public static BuildVersion fromVersionId(int versionId) {
+        return CurrentExtensionHolder.BUILD_EXTENSION.fromVersionId(versionId);
+    }
+
+    /**
+     * Get the current build version.
+     *
+     * <p>By default, this value will be different for every public release of Elasticsearch,
+     * but downstream implementations aren't restricted by this condition.</p>
+     *
+     * @return The BuildVersion for Elasticsearch
+     */
+    public static BuildVersion current() {
+        return CurrentExtensionHolder.BUILD_EXTENSION.currentBuildVersion();
+    }
+
+    // only exists for NodeMetadata#toXContent
+    // TODO[wrb]: make this abstract once all downstream classes override it
+    protected int id() {
+        return -1;
+    }
+
+    private static class CurrentExtensionHolder {
+        private static final BuildExtension BUILD_EXTENSION = findExtension();
+
+        private static BuildExtension findExtension() {
+            return ExtensionLoader.loadSingleton(ServiceLoader.load(BuildExtension.class)).orElse(new DefaultBuildExtension());
+        }
+    }
+
+    private static class DefaultBuildExtension implements BuildExtension {
+        @Override
+        public Build getCurrentBuild() {
+            return Build.current();
+        }
+
+        @Override
+        public BuildVersion currentBuildVersion() {
+            return DefaultBuildVersion.CURRENT;
+        }
+
+        @Override
+        public BuildVersion fromVersionId(int versionId) {
+            return new DefaultBuildVersion(versionId);
+        }
+    }
+
+}

+ 77 - 0
server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java

@@ -0,0 +1,77 @@
+/*
+ * 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.env;
+
+import org.elasticsearch.Version;
+
+import java.util.Objects;
+
+/**
+ * A {@link BuildVersion} that uses the same identifiers and compatibility constraints
+ * as {@link Version}.
+ *
+ * <p>This default implementation of BuildVersion delegates to the {@link Version} class.
+ * It's intended to let us check wither a version identifier is "too old" or "too new."
+ * "Too old" is determined by {@code Version.CURRENT.minimumCompatibilityVersion()},
+ * and "too new" is anything that comes after {@code Version.CURRENT}. This lets us
+ * give users simple rules in terms of public-facing release versions for Elasticsearch
+ * compatibility when upgrading nodes and prevents downgrades in place.</p>
+ */
+// TODO[wrb]: make package-private once default implementations are removed in BuildExtension
+public final class DefaultBuildVersion extends BuildVersion {
+
+    public static BuildVersion CURRENT = new DefaultBuildVersion(Version.CURRENT.id());
+
+    private final int versionId;
+    private final Version version;
+
+    public DefaultBuildVersion(int versionId) {
+        assert versionId >= 0 : "Release version IDs must be non-negative integers";
+        this.versionId = versionId;
+        this.version = Version.fromId(versionId);
+    }
+
+    @Override
+    public boolean onOrAfterMinimumCompatible() {
+        return Version.CURRENT.minimumCompatibilityVersion().onOrBefore(version);
+    }
+
+    @Override
+    public boolean isFutureVersion() {
+        return Version.CURRENT.before(version);
+    }
+
+    @Override
+    public int id() {
+        return versionId;
+    }
+
+    @Override
+    public Version toVersion() {
+        return version;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DefaultBuildVersion that = (DefaultBuildVersion) o;
+        return versionId == that.versionId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(versionId);
+    }
+
+    @Override
+    public String toString() {
+        return Version.fromId(versionId).toString();
+    }
+}

+ 2 - 3
server/src/main/java/org/elasticsearch/env/NodeEnvironment.java

@@ -22,7 +22,6 @@ import org.apache.lucene.store.NIOFSDirectory;
 import org.apache.lucene.store.NativeFSLockFactory;
 import org.elasticsearch.Build;
 import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.node.DiscoveryNodeRole;
@@ -628,7 +627,7 @@ public final class NodeEnvironment implements Closeable {
                 assert nodeIds.isEmpty() : nodeIds;
                 // If we couldn't find legacy metadata, we set the latest index version to this version. This happens
                 // when we are starting a new node and there are no indices to worry about.
-                metadata = new NodeMetadata(generateNodeId(settings), Version.CURRENT, IndexVersion.current());
+                metadata = new NodeMetadata(generateNodeId(settings), BuildVersion.current(), IndexVersion.current());
             } else {
                 assert nodeIds.equals(Collections.singleton(legacyMetadata.nodeId())) : nodeIds + " doesn't match " + legacyMetadata;
                 metadata = legacyMetadata;
@@ -636,7 +635,7 @@ public final class NodeEnvironment implements Closeable {
         }
 
         metadata = metadata.upgradeToCurrentVersion();
-        assert metadata.nodeVersion().equals(Version.CURRENT) : metadata.nodeVersion() + " != " + Version.CURRENT;
+        assert metadata.nodeVersion().equals(BuildVersion.current()) : metadata.nodeVersion() + " != " + Build.current();
 
         return metadata;
     }

+ 27 - 26
server/src/main/java/org/elasticsearch/env/NodeMetadata.java

@@ -9,7 +9,6 @@
 package org.elasticsearch.env;
 
 import org.elasticsearch.Build;
-import org.elasticsearch.Version;
 import org.elasticsearch.core.UpdateForV9;
 import org.elasticsearch.gateway.MetadataStateFormat;
 import org.elasticsearch.index.IndexVersion;
@@ -36,26 +35,27 @@ public final class NodeMetadata {
 
     private final String nodeId;
 
-    private final Version nodeVersion;
+    private final BuildVersion nodeVersion;
 
-    private final Version previousNodeVersion;
+    private final BuildVersion previousNodeVersion;
 
     private final IndexVersion oldestIndexVersion;
 
+    @UpdateForV9 // version should be non-null in the node metadata from v9 onwards
     private NodeMetadata(
         final String nodeId,
-        final Version nodeVersion,
-        final Version previousNodeVersion,
+        final BuildVersion buildVersion,
+        final BuildVersion previousBuildVersion,
         final IndexVersion oldestIndexVersion
     ) {
         this.nodeId = Objects.requireNonNull(nodeId);
-        this.nodeVersion = Objects.requireNonNull(nodeVersion);
-        this.previousNodeVersion = Objects.requireNonNull(previousNodeVersion);
+        this.nodeVersion = Objects.requireNonNull(buildVersion);
+        this.previousNodeVersion = Objects.requireNonNull(previousBuildVersion);
         this.oldestIndexVersion = Objects.requireNonNull(oldestIndexVersion);
     }
 
-    public NodeMetadata(final String nodeId, final Version nodeVersion, final IndexVersion oldestIndexVersion) {
-        this(nodeId, nodeVersion, nodeVersion, oldestIndexVersion);
+    public NodeMetadata(final String nodeId, final BuildVersion buildVersion, final IndexVersion oldestIndexVersion) {
+        this(nodeId, buildVersion, buildVersion, oldestIndexVersion);
     }
 
     @Override
@@ -93,7 +93,7 @@ public final class NodeMetadata {
         return nodeId;
     }
 
-    public Version nodeVersion() {
+    public BuildVersion nodeVersion() {
         return nodeVersion;
     }
 
@@ -103,7 +103,7 @@ public final class NodeMetadata {
      * the current version of the node ({@link NodeMetadata#upgradeToCurrentVersion()} before storing the node metadata again on disk.
      * In doing so, {@code previousNodeVersion} refers to the previously last known version that this node was started on.
      */
-    public Version previousNodeVersion() {
+    public BuildVersion previousNodeVersion() {
         return previousNodeVersion;
     }
 
@@ -111,11 +111,12 @@ public final class NodeMetadata {
         return oldestIndexVersion;
     }
 
+    @UpdateForV9
     public void verifyUpgradeToCurrentVersion() {
-        assert (nodeVersion.equals(Version.V_EMPTY) == false) || (Version.CURRENT.major <= Version.V_7_0_0.major + 1)
-            : "version is required in the node metadata from v9 onwards";
+        // Enable the following assertion for V9:
+        // assert (nodeVersion.equals(BuildVersion.empty()) == false) : "version is required in the node metadata from v9 onwards";
 
-        if (nodeVersion.before(Version.CURRENT.minimumCompatibilityVersion())) {
+        if (nodeVersion.onOrAfterMinimumCompatible() == false) {
             throw new IllegalStateException(
                 "cannot upgrade a node from version ["
                     + nodeVersion
@@ -128,7 +129,7 @@ public final class NodeMetadata {
             );
         }
 
-        if (nodeVersion.after(Version.CURRENT)) {
+        if (nodeVersion.isFutureVersion()) {
             throw new IllegalStateException(
                 "cannot downgrade a node from version [" + nodeVersion + "] to version [" + Build.current().version() + "]"
             );
@@ -138,13 +139,15 @@ public final class NodeMetadata {
     public NodeMetadata upgradeToCurrentVersion() {
         verifyUpgradeToCurrentVersion();
 
-        return nodeVersion.equals(Version.CURRENT) ? this : new NodeMetadata(nodeId, Version.CURRENT, nodeVersion, oldestIndexVersion);
+        return nodeVersion.equals(BuildVersion.current())
+            ? this
+            : new NodeMetadata(nodeId, BuildVersion.current(), nodeVersion, oldestIndexVersion);
     }
 
     private static class Builder {
         String nodeId;
-        Version nodeVersion;
-        Version previousNodeVersion;
+        BuildVersion nodeVersion;
+        BuildVersion previousNodeVersion;
         IndexVersion oldestIndexVersion;
 
         public void setNodeId(String nodeId) {
@@ -152,22 +155,20 @@ public final class NodeMetadata {
         }
 
         public void setNodeVersionId(int nodeVersionId) {
-            this.nodeVersion = Version.fromId(nodeVersionId);
+            this.nodeVersion = BuildVersion.fromVersionId(nodeVersionId);
         }
 
         public void setOldestIndexVersion(int oldestIndexVersion) {
             this.oldestIndexVersion = IndexVersion.fromId(oldestIndexVersion);
         }
 
-        private Version getVersionOrFallbackToEmpty() {
-            return Objects.requireNonNullElse(this.nodeVersion, Version.V_EMPTY);
-        }
-
+        @UpdateForV9 // version is required in the node metadata from v9 onwards
         public NodeMetadata build() {
-            @UpdateForV9 // version is required in the node metadata from v9 onwards
-            final Version nodeVersion = getVersionOrFallbackToEmpty();
             final IndexVersion oldestIndexVersion;
 
+            if (this.nodeVersion == null) {
+                nodeVersion = BuildVersion.fromVersionId(0);
+            }
             if (this.previousNodeVersion == null) {
                 previousNodeVersion = nodeVersion;
             }
@@ -207,7 +208,7 @@ public final class NodeMetadata {
         @Override
         public void toXContent(XContentBuilder builder, NodeMetadata nodeMetadata) throws IOException {
             builder.field(NODE_ID_KEY, nodeMetadata.nodeId);
-            builder.field(NODE_VERSION_KEY, nodeMetadata.nodeVersion.id);
+            builder.field(NODE_VERSION_KEY, nodeMetadata.nodeVersion.id());
             builder.field(OLDEST_INDEX_VERSION_KEY, nodeMetadata.oldestIndexVersion.id());
         }
 

+ 1 - 1
server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java

@@ -82,7 +82,7 @@ public class OverrideNodeVersionCommand extends ElasticsearchNodeCommand {
 
         confirm(
             terminal,
-            (nodeMetadata.nodeVersion().before(Version.CURRENT) ? TOO_OLD_MESSAGE : TOO_NEW_MESSAGE).replace(
+            (nodeMetadata.nodeVersion().onOrAfterMinimumCompatible() == false ? TOO_OLD_MESSAGE : TOO_NEW_MESSAGE).replace(
                 "V_OLD",
                 nodeMetadata.nodeVersion().toString()
             ).replace("V_NEW", nodeMetadata.nodeVersion().toString()).replace("V_CUR", Version.CURRENT.toString())

+ 11 - 3
server/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java

@@ -14,7 +14,6 @@ import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ExceptionsHelper;
-import org.elasticsearch.Version;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.coordination.CoordinationMetadata;
@@ -35,6 +34,7 @@ import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.core.UpdateForV9;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.NodeMetadata;
 import org.elasticsearch.index.IndexVersions;
 import org.elasticsearch.node.Node;
@@ -222,7 +222,11 @@ public class GatewayMetaState implements Closeable {
             }
             // write legacy node metadata to prevent accidental downgrades from spawning empty cluster state
             NodeMetadata.FORMAT.writeAndCleanup(
-                new NodeMetadata(persistedClusterStateService.getNodeId(), Version.CURRENT, clusterState.metadata().oldestIndexVersion()),
+                new NodeMetadata(
+                    persistedClusterStateService.getNodeId(),
+                    BuildVersion.current(),
+                    clusterState.metadata().oldestIndexVersion()
+                ),
                 persistedClusterStateService.getDataPaths()
             );
             success = true;
@@ -260,7 +264,11 @@ public class GatewayMetaState implements Closeable {
             metaStateService.deleteAll();
             // write legacy node metadata to prevent downgrades from spawning empty cluster state
             NodeMetadata.FORMAT.writeAndCleanup(
-                new NodeMetadata(persistedClusterStateService.getNodeId(), Version.CURRENT, clusterState.metadata().oldestIndexVersion()),
+                new NodeMetadata(
+                    persistedClusterStateService.getNodeId(),
+                    BuildVersion.current(),
+                    clusterState.metadata().oldestIndexVersion()
+                ),
                 persistedClusterStateService.getDataPaths()
             );
         }

+ 3 - 1
server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java

@@ -69,6 +69,7 @@ import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.NodeEnvironment;
 import org.elasticsearch.env.NodeMetadata;
 import org.elasticsearch.index.IndexVersion;
@@ -377,7 +378,8 @@ public class PersistedClusterStateService {
         if (nodeId == null) {
             return null;
         }
-        return new NodeMetadata(nodeId, version, oldestIndexVersion);
+        // TODO: remove use of Version here (ES-7343)
+        return new NodeMetadata(nodeId, BuildVersion.fromVersionId(version.id()), oldestIndexVersion);
     }
 
     /**

+ 12 - 0
server/src/main/java/org/elasticsearch/internal/BuildExtension.java

@@ -9,6 +9,8 @@
 package org.elasticsearch.internal;
 
 import org.elasticsearch.Build;
+import org.elasticsearch.env.BuildVersion;
+import org.elasticsearch.env.DefaultBuildVersion;
 
 /**
  * Allows plugging in current build info.
@@ -26,4 +28,14 @@ public interface BuildExtension {
     default boolean hasReleaseVersioning() {
         return true;
     }
+
+    // TODO[wrb]: Remove default implementation once downstream BuildExtensions are updated
+    default BuildVersion currentBuildVersion() {
+        return DefaultBuildVersion.CURRENT;
+    }
+
+    // TODO[wrb]: Remove default implementation once downstream BuildExtensions are updated
+    default BuildVersion fromVersionId(int versionId) {
+        return new DefaultBuildVersion(versionId);
+    }
 }

+ 2 - 2
server/src/main/java/org/elasticsearch/node/Node.java

@@ -12,7 +12,6 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchTimeoutException;
-import org.elasticsearch.Version;
 import org.elasticsearch.action.search.TransportSearchAction;
 import org.elasticsearch.bootstrap.BootstrapCheck;
 import org.elasticsearch.bootstrap.BootstrapContext;
@@ -47,6 +46,7 @@ import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.PathUtils;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.env.NodeEnvironment;
 import org.elasticsearch.env.NodeMetadata;
@@ -340,7 +340,7 @@ public class Node implements Closeable {
                     nodeEnvironment.nodeDataPaths()
                 );
                 assert nodeMetadata != null;
-                assert nodeMetadata.nodeVersion().equals(Version.CURRENT);
+                assert nodeMetadata.nodeVersion().equals(BuildVersion.current());
                 assert nodeMetadata.nodeId().equals(localNodeFactory.getNode().getId());
             } catch (IOException e) {
                 assert false : e;

+ 44 - 0
server/src/test/java/org/elasticsearch/env/BuildVersionTests.java

@@ -0,0 +1,44 @@
+/*
+ * 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.env;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.test.ESTestCase;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class BuildVersionTests extends ESTestCase {
+    public void testBuildVersionCurrent() {
+        assertThat(BuildVersion.current(), equalTo(BuildVersion.fromVersionId(Version.CURRENT.id())));
+    }
+
+    public void testBeforeMinimumCompatibleVersion() {
+        BuildVersion beforeMinCompat = BuildVersion.fromVersionId(between(0, Version.CURRENT.minimumCompatibilityVersion().id() - 1));
+        BuildVersion afterMinCompat = BuildVersion.fromVersionId(
+            between(Version.CURRENT.minimumCompatibilityVersion().id(), Version.CURRENT.id())
+        );
+        BuildVersion futureVersion = BuildVersion.fromVersionId(between(Version.CURRENT.id() + 1, Version.CURRENT.id() + 1_000_000));
+
+        assertFalse(beforeMinCompat.onOrAfterMinimumCompatible());
+        assertTrue(afterMinCompat.onOrAfterMinimumCompatible());
+        assertTrue(futureVersion.onOrAfterMinimumCompatible());
+    }
+
+    public void testIsFutureVersion() {
+        BuildVersion beforeMinCompat = BuildVersion.fromVersionId(between(0, Version.CURRENT.minimumCompatibilityVersion().id() - 1));
+        BuildVersion afterMinCompat = BuildVersion.fromVersionId(
+            between(Version.CURRENT.minimumCompatibilityVersion().id(), Version.CURRENT.id())
+        );
+        BuildVersion futureVersion = BuildVersion.fromVersionId(between(Version.CURRENT.id() + 1, Version.CURRENT.id() + 1_000_000));
+
+        assertFalse(beforeMinCompat.isFutureVersion());
+        assertFalse(afterMinCompat.isFutureVersion());
+        assertTrue(futureVersion.isFutureVersion());
+    }
+}

+ 24 - 14
server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java

@@ -36,6 +36,10 @@ public class NodeMetadataTests extends ESTestCase {
         return rarely() ? Version.fromId(randomInt()) : VersionUtils.randomVersion(random());
     }
 
+    private BuildVersion randomBuildVersion() {
+        return BuildVersion.fromVersionId(randomVersion().id());
+    }
+
     private IndexVersion randomIndexVersion() {
         return rarely() ? IndexVersion.fromId(randomInt()) : IndexVersionUtils.randomVersion(random());
     }
@@ -43,7 +47,7 @@ public class NodeMetadataTests extends ESTestCase {
     public void testEqualsHashcodeSerialization() {
         final Path tempDir = createTempDir();
         EqualsHashCodeTestUtils.checkEqualsAndHashCode(
-            new NodeMetadata(randomAlphaOfLength(10), randomVersion(), randomIndexVersion()),
+            new NodeMetadata(randomAlphaOfLength(10), randomBuildVersion(), randomIndexVersion()),
             nodeMetadata -> {
                 final long generation = NodeMetadata.FORMAT.writeAndCleanup(nodeMetadata, tempDir);
                 final Tuple<NodeMetadata, Long> nodeMetadataLongTuple = NodeMetadata.FORMAT.loadLatestStateWithGeneration(
@@ -62,7 +66,7 @@ public class NodeMetadataTests extends ESTestCase {
                 );
                 case 1 -> new NodeMetadata(
                     nodeMetadata.nodeId(),
-                    randomValueOtherThan(nodeMetadata.nodeVersion(), this::randomVersion),
+                    randomValueOtherThan(nodeMetadata.nodeVersion(), this::randomBuildVersion),
                     nodeMetadata.oldestIndexVersion()
                 );
                 default -> new NodeMetadata(
@@ -87,20 +91,17 @@ public class NodeMetadataTests extends ESTestCase {
         Files.copy(resource, stateDir.resolve(NodeMetadata.FORMAT.getStateFileName(between(0, Integer.MAX_VALUE))));
         final NodeMetadata nodeMetadata = NodeMetadata.FORMAT.loadLatestState(logger, xContentRegistry(), tempDir);
         assertThat(nodeMetadata.nodeId(), equalTo("y6VUVMSaStO4Tz-B5BxcOw"));
-        assertThat(nodeMetadata.nodeVersion(), equalTo(Version.V_EMPTY));
+        assertThat(nodeMetadata.nodeVersion(), equalTo(BuildVersion.fromVersionId(0)));
     }
 
     public void testUpgradesLegitimateVersions() {
         final String nodeId = randomAlphaOfLength(10);
         final NodeMetadata nodeMetadata = new NodeMetadata(
             nodeId,
-            randomValueOtherThanMany(
-                v -> v.after(Version.CURRENT) || v.before(Version.CURRENT.minimumCompatibilityVersion()),
-                this::randomVersion
-            ),
+            randomValueOtherThanMany(v -> v.isFutureVersion() || v.onOrAfterMinimumCompatible() == false, this::randomBuildVersion),
             IndexVersion.current()
         ).upgradeToCurrentVersion();
-        assertThat(nodeMetadata.nodeVersion(), equalTo(Version.CURRENT));
+        assertThat(nodeMetadata.nodeVersion(), equalTo(BuildVersion.current()));
         assertThat(nodeMetadata.nodeId(), equalTo(nodeId));
     }
 
@@ -109,7 +110,7 @@ public class NodeMetadataTests extends ESTestCase {
 
         final IllegalStateException illegalStateException = expectThrows(
             IllegalStateException.class,
-            () -> new NodeMetadata(nodeId, Version.V_EMPTY, IndexVersion.current()).upgradeToCurrentVersion()
+            () -> new NodeMetadata(nodeId, BuildVersion.fromVersionId(0), IndexVersion.current()).upgradeToCurrentVersion()
         );
         assertThat(
             illegalStateException.getMessage(),
@@ -122,7 +123,7 @@ public class NodeMetadataTests extends ESTestCase {
     public void testDoesNotUpgradeFutureVersion() {
         final IllegalStateException illegalStateException = expectThrows(
             IllegalStateException.class,
-            () -> new NodeMetadata(randomAlphaOfLength(10), tooNewVersion(), IndexVersion.current()).upgradeToCurrentVersion()
+            () -> new NodeMetadata(randomAlphaOfLength(10), tooNewBuildVersion(), IndexVersion.current()).upgradeToCurrentVersion()
         );
         assertThat(
             illegalStateException.getMessage(),
@@ -133,7 +134,7 @@ public class NodeMetadataTests extends ESTestCase {
     public void testDoesNotUpgradeAncientVersion() {
         final IllegalStateException illegalStateException = expectThrows(
             IllegalStateException.class,
-            () -> new NodeMetadata(randomAlphaOfLength(10), tooOldVersion(), IndexVersion.current()).upgradeToCurrentVersion()
+            () -> new NodeMetadata(randomAlphaOfLength(10), tooOldBuildVersion(), IndexVersion.current()).upgradeToCurrentVersion()
         );
         assertThat(
             illegalStateException.getMessage(),
@@ -153,10 +154,11 @@ public class NodeMetadataTests extends ESTestCase {
     public void testUpgradeMarksPreviousVersion() {
         final String nodeId = randomAlphaOfLength(10);
         final Version version = VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0);
+        final BuildVersion buildVersion = BuildVersion.fromVersionId(version.id());
 
-        final NodeMetadata nodeMetadata = new NodeMetadata(nodeId, version, IndexVersion.current()).upgradeToCurrentVersion();
-        assertThat(nodeMetadata.nodeVersion(), equalTo(Version.CURRENT));
-        assertThat(nodeMetadata.previousNodeVersion(), equalTo(version));
+        final NodeMetadata nodeMetadata = new NodeMetadata(nodeId, buildVersion, IndexVersion.current()).upgradeToCurrentVersion();
+        assertThat(nodeMetadata.nodeVersion(), equalTo(BuildVersion.current()));
+        assertThat(nodeMetadata.previousNodeVersion(), equalTo(buildVersion));
     }
 
     public static Version tooNewVersion() {
@@ -167,7 +169,15 @@ public class NodeMetadataTests extends ESTestCase {
         return IndexVersion.fromId(between(IndexVersion.current().id() + 1, 99999999));
     }
 
+    public static BuildVersion tooNewBuildVersion() {
+        return BuildVersion.fromVersionId(between(Version.CURRENT.id() + 1, 99999999));
+    }
+
     public static Version tooOldVersion() {
         return Version.fromId(between(1, Version.CURRENT.minimumCompatibilityVersion().id - 1));
     }
+
+    public static BuildVersion tooOldBuildVersion() {
+        return BuildVersion.fromVersionId(between(1, Version.CURRENT.minimumCompatibilityVersion().id - 1));
+    }
 }

+ 4 - 4
server/src/test/java/org/elasticsearch/env/OverrideNodeVersionCommandTests.java

@@ -136,7 +136,7 @@ public class OverrideNodeVersionCommandTests extends ESTestCase {
         expectThrows(IllegalStateException.class, () -> mockTerminal.readText(""));
 
         final NodeMetadata nodeMetadata = PersistedClusterStateService.nodeMetadata(dataPaths);
-        assertThat(nodeMetadata.nodeVersion(), equalTo(nodeVersion));
+        assertThat(nodeMetadata.nodeVersion().toVersion(), equalTo(nodeVersion));
     }
 
     public void testWarnsIfTooNew() throws Exception {
@@ -161,7 +161,7 @@ public class OverrideNodeVersionCommandTests extends ESTestCase {
         expectThrows(IllegalStateException.class, () -> mockTerminal.readText(""));
 
         final NodeMetadata nodeMetadata = PersistedClusterStateService.nodeMetadata(dataPaths);
-        assertThat(nodeMetadata.nodeVersion(), equalTo(nodeVersion));
+        assertThat(nodeMetadata.nodeVersion().toVersion(), equalTo(nodeVersion));
     }
 
     public void testOverwritesIfTooOld() throws Exception {
@@ -184,7 +184,7 @@ public class OverrideNodeVersionCommandTests extends ESTestCase {
         expectThrows(IllegalStateException.class, () -> mockTerminal.readText(""));
 
         final NodeMetadata nodeMetadata = PersistedClusterStateService.nodeMetadata(dataPaths);
-        assertThat(nodeMetadata.nodeVersion(), equalTo(Version.CURRENT));
+        assertThat(nodeMetadata.nodeVersion(), equalTo(BuildVersion.current()));
     }
 
     public void testOverwritesIfTooNew() throws Exception {
@@ -206,6 +206,6 @@ public class OverrideNodeVersionCommandTests extends ESTestCase {
         expectThrows(IllegalStateException.class, () -> mockTerminal.readText(""));
 
         final NodeMetadata nodeMetadata = PersistedClusterStateService.nodeMetadata(dataPaths);
-        assertThat(nodeMetadata.nodeVersion(), equalTo(Version.CURRENT));
+        assertThat(nodeMetadata.nodeVersion(), equalTo(BuildVersion.current()));
     }
 }

+ 4 - 3
server/src/test/java/org/elasticsearch/gateway/PersistedClusterStateServiceTests.java

@@ -52,6 +52,7 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeValue;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.env.NodeEnvironment;
 import org.elasticsearch.env.NodeMetadata;
@@ -1439,13 +1440,13 @@ public class PersistedClusterStateServiceTests extends ESTestCase {
 
             }
             NodeMetadata prevMetadata = PersistedClusterStateService.nodeMetadata(persistedClusterStateService.getDataPaths());
-            assertEquals(Version.CURRENT, prevMetadata.nodeVersion());
+            assertEquals(BuildVersion.current(), prevMetadata.nodeVersion());
             PersistedClusterStateService.overrideVersion(Version.V_8_0_0, persistedClusterStateService.getDataPaths());
             NodeMetadata metadata = PersistedClusterStateService.nodeMetadata(persistedClusterStateService.getDataPaths());
-            assertEquals(Version.V_8_0_0, metadata.nodeVersion());
+            assertEquals(BuildVersion.fromVersionId(Version.V_8_0_0.id()), metadata.nodeVersion());
             for (Path p : persistedClusterStateService.getDataPaths()) {
                 NodeMetadata individualMetadata = PersistedClusterStateService.nodeMetadata(p);
-                assertEquals(Version.V_8_0_0, individualMetadata.nodeVersion());
+                assertEquals(BuildVersion.fromVersionId(Version.V_8_0_0.id()), individualMetadata.nodeVersion());
             }
         }
     }

+ 2 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityImplicitBehaviorBootstrapCheck.java

@@ -34,7 +34,8 @@ public class SecurityImplicitBehaviorBootstrapCheck implements BootstrapCheck {
         }
         if (licenseService instanceof ClusterStateLicenseService clusterStateLicenseService) {
             final License license = clusterStateLicenseService.getLicense(context.metadata());
-            final Version lastKnownVersion = nodeMetadata.previousNodeVersion();
+            // TODO[wrb]: Add an "isCurrentMajor" method to BuildVersion?
+            final Version lastKnownVersion = nodeMetadata.previousNodeVersion().toVersion();
             // pre v7.2.0 nodes have Version.EMPTY and its id is 0, so Version#before handles this successfully
             if (lastKnownVersion.before(Version.V_8_0_0)
                 && XPackSettings.SECURITY_ENABLED.exists(context.settings()) == false

+ 22 - 11
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityImplicitBehaviorBootstrapCheckTests.java

@@ -13,6 +13,7 @@ import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.common.ReferenceDocs;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.NodeMetadata;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.license.ClusterStateLicenseService;
@@ -32,9 +33,11 @@ import static org.mockito.Mockito.when;
 public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstrapCheckTestCase {
 
     public void testFailureUpgradeFrom7xWithImplicitSecuritySettings() throws Exception {
-        final Version previousVersion = randomValueOtherThan(
-            Version.V_8_0_0,
-            () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+        final BuildVersion previousVersion = toBuildVersion(
+            randomValueOtherThan(
+                Version.V_8_0_0,
+                () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+            )
         );
         NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(10), previousVersion, IndexVersion.current());
         nodeMetadata = nodeMetadata.upgradeToCurrentVersion();
@@ -67,9 +70,11 @@ public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstr
     }
 
     public void testUpgradeFrom7xWithImplicitSecuritySettingsOnGoldPlus() throws Exception {
-        final Version previousVersion = randomValueOtherThan(
-            Version.V_8_0_0,
-            () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+        final BuildVersion previousVersion = toBuildVersion(
+            randomValueOtherThan(
+                Version.V_8_0_0,
+                () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+            )
         );
         NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(10), previousVersion, IndexVersion.current());
         nodeMetadata = nodeMetadata.upgradeToCurrentVersion();
@@ -88,9 +93,11 @@ public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstr
     }
 
     public void testUpgradeFrom7xWithExplicitSecuritySettings() throws Exception {
-        final Version previousVersion = randomValueOtherThan(
-            Version.V_8_0_0,
-            () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+        final BuildVersion previousVersion = toBuildVersion(
+            randomValueOtherThan(
+                Version.V_8_0_0,
+                () -> VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.V_8_0_0)
+            )
         );
         NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(10), previousVersion, IndexVersion.current());
         nodeMetadata = nodeMetadata.upgradeToCurrentVersion();
@@ -105,7 +112,7 @@ public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstr
     }
 
     public void testUpgradeFrom8xWithImplicitSecuritySettings() throws Exception {
-        final Version previousVersion = VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, null);
+        final BuildVersion previousVersion = toBuildVersion(VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, null));
         NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(10), previousVersion, IndexVersion.current());
         nodeMetadata = nodeMetadata.upgradeToCurrentVersion();
         ClusterStateLicenseService licenseService = mock(ClusterStateLicenseService.class);
@@ -119,7 +126,7 @@ public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstr
     }
 
     public void testUpgradeFrom8xWithExplicitSecuritySettings() throws Exception {
-        final Version previousVersion = VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, null);
+        final BuildVersion previousVersion = toBuildVersion(VersionUtils.randomVersionBetween(random(), Version.V_8_0_0, null));
         NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(10), previousVersion, IndexVersion.current());
         nodeMetadata = nodeMetadata.upgradeToCurrentVersion();
         ClusterStateLicenseService licenseService = mock(ClusterStateLicenseService.class);
@@ -136,4 +143,8 @@ public class SecurityImplicitBehaviorBootstrapCheckTests extends AbstractBootstr
         License license = TestUtils.generateSignedLicense(licenseMode, TimeValue.timeValueHours(2));
         return Metadata.builder().putCustom(LicensesMetadata.TYPE, new LicensesMetadata(license, era)).build();
     }
+
+    private static BuildVersion toBuildVersion(Version version) {
+        return BuildVersion.fromVersionId(version.id());
+    }
 }

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java

@@ -11,7 +11,6 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ElasticsearchSecurityException;
-import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionModule;
 import org.elasticsearch.action.ActionResponse;
@@ -33,6 +32,7 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsModule;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.env.BuildVersion;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.env.NodeMetadata;
 import org.elasticsearch.env.TestEnvironment;
@@ -202,7 +202,7 @@ public class SecurityTests extends ESTestCase {
 
     private Collection<Object> createComponentsUtil(Settings settings) throws Exception {
         Environment env = TestEnvironment.newEnvironment(settings);
-        NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(8), Version.CURRENT, IndexVersion.current());
+        NodeMetadata nodeMetadata = new NodeMetadata(randomAlphaOfLength(8), BuildVersion.current(), IndexVersion.current());
         ThreadPool threadPool = mock(ThreadPool.class);
         ClusterService clusterService = mock(ClusterService.class);
         settings = Security.additionalSettings(settings, true);