Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.license.internal.MutableLicenseService;
import org.elasticsearch.license.internal.TrialLicenseVersion;
import org.elasticsearch.license.internal.XPackLicenseStatus;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
Expand Down Expand Up @@ -249,7 +250,7 @@ public ClusterState execute(ClusterState currentState) throws Exception {
}
Metadata currentMetadata = currentState.metadata();
LicensesMetadata licensesMetadata = currentMetadata.custom(LicensesMetadata.TYPE);
Version trialVersion = null;
TrialLicenseVersion trialVersion = null;
if (licensesMetadata != null) {
trialVersion = licensesMetadata.getMostRecentTrialVersion();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

import org.elasticsearch.TransportVersion;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.AbstractNamedDiffable;
import org.elasticsearch.cluster.NamedDiff;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.common.collect.Iterators;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.license.internal.TrialLicenseVersion;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentParser;

Expand Down Expand Up @@ -57,31 +57,31 @@ public class LicensesMetadata extends AbstractNamedDiffable<Metadata.Custom> imp
// is null, then no trial has been exercised. We keep the version to leave open the possibility that we
// may eventually allow a cluster to exercise a trial every time they upgrade to a new major version.
@Nullable
private Version trialVersion;
private TrialLicenseVersion trialLicenseVersion;

public LicensesMetadata(License license, Version trialVersion) {
public LicensesMetadata(License license, TrialLicenseVersion trialLicenseVersion) {
this.license = license;
this.trialVersion = trialVersion;
this.trialLicenseVersion = trialLicenseVersion;
}

public License getLicense() {
return license;
}

boolean isEligibleForTrial() {
if (trialVersion == null) {
if (trialLicenseVersion == null) {
return true;
}
return Version.CURRENT.major > trialVersion.major;
return trialLicenseVersion.ableToStartNewTrialSince(TrialLicenseVersion.CURRENT);
}

Version getMostRecentTrialVersion() {
return trialVersion;
TrialLicenseVersion getMostRecentTrialVersion() {
return trialLicenseVersion;
}

@Override
public String toString() {
return "LicensesMetadata{" + "license=" + license + ", trialVersion=" + trialVersion + '}';
return "LicensesMetadata{" + "license=" + license + ", trialVersion=" + trialLicenseVersion + '}';
}

@Override
Expand All @@ -91,13 +91,13 @@ public boolean equals(Object o) {

LicensesMetadata that = (LicensesMetadata) o;

return Objects.equals(license, that.license) && Objects.equals(trialVersion, that.trialVersion);
return Objects.equals(license, that.license) && Objects.equals(trialLicenseVersion, that.trialLicenseVersion);
}

@Override
public int hashCode() {
int result = license != null ? license.hashCode() : 0;
result = 31 * result + (trialVersion != null ? trialVersion.hashCode() : 0);
result = 31 * result + (trialLicenseVersion != null ? trialLicenseVersion.hashCode() : 0);
return result;
}

Expand All @@ -118,7 +118,7 @@ public EnumSet<Metadata.XContentContext> context() {

public static LicensesMetadata fromXContent(XContentParser parser) throws IOException {
License license = LICENSE_TOMBSTONE;
Version trialLicense = null;
TrialLicenseVersion trialLicense = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
Expand All @@ -133,7 +133,7 @@ public static LicensesMetadata fromXContent(XContentParser parser) throws IOExce
}
} else if (fieldName.equals(Fields.TRIAL_LICENSE)) {
parser.nextToken();
trialLicense = Version.fromString(parser.text());
trialLicense = TrialLicenseVersion.fromXContent(parser.text());
}
}
}
Expand All @@ -151,8 +151,8 @@ public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params ignore
license.toInnerXContent(builder, params);
builder.endObject();
}
if (trialVersion != null) {
builder.field(Fields.TRIAL_LICENSE, trialVersion.toString());
if (trialLicenseVersion != null) {
builder.field(Fields.TRIAL_LICENSE, trialLicenseVersion.toString());
}
return builder;
}));
Expand All @@ -166,11 +166,11 @@ public void writeTo(StreamOutput streamOutput) throws IOException {
streamOutput.writeBoolean(true); // has a license
license.writeTo(streamOutput);
}
if (trialVersion == null) {
if (trialLicenseVersion == null) {
streamOutput.writeBoolean(false);
} else {
streamOutput.writeBoolean(true);
Version.writeVersion(trialVersion, streamOutput);
trialLicenseVersion.writeTo(streamOutput);
}
}

Expand All @@ -182,7 +182,7 @@ public LicensesMetadata(StreamInput streamInput) throws IOException {
}
boolean hasExercisedTrial = streamInput.readBoolean();
if (hasExercisedTrial) {
this.trialVersion = Version.readVersion(streamInput);
this.trialLicenseVersion = new TrialLicenseVersion(streamInput);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
package org.elasticsearch.license;

import org.apache.logging.log4j.Logger;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateTaskExecutor;
import org.elasticsearch.cluster.ClusterStateTaskListener;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.license.internal.TrialLicenseVersion;
import org.elasticsearch.xpack.core.XPackPlugin;

import java.time.Clock;
Expand Down Expand Up @@ -78,7 +78,7 @@ public LicensesMetadata execute(
return currentLicensesMetadata;
}
}
Version trialVersion = currentLicensesMetadata != null ? currentLicensesMetadata.getMostRecentTrialVersion() : null;
TrialLicenseVersion trialVersion = currentLicensesMetadata != null ? currentLicensesMetadata.getMostRecentTrialVersion() : null;
updatedLicensesMetadata = new LicensesMetadata(selfGeneratedLicense, trialVersion);
} else {
updatedLicensesMetadata = currentLicensesMetadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
package org.elasticsearch.license;

import org.apache.logging.log4j.Logger;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateTaskExecutor;
import org.elasticsearch.cluster.ClusterStateTaskListener;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.license.internal.TrialLicenseVersion;
import org.elasticsearch.xpack.core.XPackPlugin;

import java.time.Clock;
Expand Down Expand Up @@ -62,6 +62,15 @@ private LicensesMetadata execute(
ClusterStateTaskExecutor.TaskContext<StartTrialClusterTask> taskContext
) {
assert taskContext.getTask() == this;
if (discoveryNodes.getMaxNodeVersion().after(discoveryNodes.getSmallestNonClientNodeVersion())) {
throw new IllegalStateException(
"Please ensure all nodes are on the same version before starting your trial, the highest node version in this cluster is ["
+ discoveryNodes.getMaxNodeVersion()
+ "] and the lowest node version is ["
+ discoveryNodes.getMinNodeVersion()
+ "]"
);
}
final var listener = ActionListener.runBefore(this.listener, () -> {
logger.debug("started self generated trial license: {}", currentLicensesMetadata);
});
Expand All @@ -88,7 +97,7 @@ private LicensesMetadata execute(
specBuilder.maxNodes(LicenseSettings.SELF_GENERATED_LICENSE_MAX_NODES);
}
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, discoveryNodes);
LicensesMetadata newLicensesMetadata = new LicensesMetadata(selfGeneratedLicense, Version.CURRENT);
LicensesMetadata newLicensesMetadata = new LicensesMetadata(selfGeneratedLicense, TrialLicenseVersion.CURRENT);
taskContext.success(() -> listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.UPGRADED_TO_TRIAL)));
return newLicensesMetadata;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.license.internal.TrialLicenseVersion;
import org.elasticsearch.xpack.core.XPackPlugin;

import java.time.Clock;
Expand Down Expand Up @@ -87,7 +87,7 @@ private ClusterState updateLicenseSignature(ClusterState currentState, LicensesM
.type(type)
.expiryDate(expiryDate);
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentState.nodes());
Version trialVersion = currentLicenseMetadata.getMostRecentTrialVersion();
TrialLicenseVersion trialVersion = currentLicenseMetadata.getMostRecentTrialVersion();
LicensesMetadata newLicenseMetadata = new LicensesMetadata(selfGeneratedLicense, trialVersion);
mdBuilder.putCustom(LicensesMetadata.TYPE, newLicenseMetadata);
logger.info(
Expand Down Expand Up @@ -129,7 +129,7 @@ private LicensesMetadata createBasicLicenseFromExistingLicense(LicensesMetadata
.type(License.LicenseType.BASIC)
.expiryDate(LicenseSettings.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS);
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentLicense.version());
Version trialVersion = currentLicenseMetadata.getMostRecentTrialVersion();
TrialLicenseVersion trialVersion = currentLicenseMetadata.getMostRecentTrialVersion();
return new LicensesMetadata(selfGeneratedLicense, trialVersion);
}

Expand All @@ -152,7 +152,7 @@ private ClusterState updateWithLicense(ClusterState currentState, License.Licens
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentState.nodes());
LicensesMetadata licensesMetadata;
if (License.LicenseType.TRIAL.equals(type)) {
licensesMetadata = new LicensesMetadata(selfGeneratedLicense, Version.CURRENT);
licensesMetadata = new LicensesMetadata(selfGeneratedLicense, TrialLicenseVersion.CURRENT);
} else {
licensesMetadata = new LicensesMetadata(selfGeneratedLicense, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ protected void masterOperation(
ClusterState state,
ActionListener<PostStartTrialResponse> listener
) throws Exception {
if (state.nodes().getMaxNodeVersion().after(state.nodes().getSmallestNonClientNodeVersion())) {
throw new IllegalStateException(
"Please ensure all nodes are on the same version before starting your trial, the highest node version in this cluster is ["
+ state.nodes().getMaxNodeVersion()
+ "] and the lowest node version is ["
+ state.nodes().getMinNodeVersion()
+ "]"
);
}
licenseService.startTrialLicense(request, listener);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.license.internal;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.xcontent.ToXContentFragment;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Objects;

/**
* Sometimes we release a version with a bunch of cool new features, and we want people to be able to start a new trial license in a cluster
* that's already used a trial and let it expire. This class controls when we do that. The serialization of this class is designed to
* maintain compatibility with old-school Elasticsearch versions (specifically the {@link org.elasticsearch.Version} class).
*/
public class TrialLicenseVersion implements ToXContentFragment, Writeable {

// This was the highest version at the time we cut over to having a specific version for the trial license, rather than reusing the
// generic Elasticsearch version. While it's derived from the Elasticsearch version formula for BWC, it is independent of it going
// forward. When we want users to be able to start a new trial, increment this number.
// Pkg-private for testing only.
static final int TRIAL_VERSION_CUTOVER = 8_12_00_99;
public static final TrialLicenseVersion CURRENT = new TrialLicenseVersion(TRIAL_VERSION_CUTOVER);

// The most recently released major version when we cut over. Here for maintaining BWC behavior.
static final int TRIAL_VERSION_CUTOVER_MAJOR = 8;

private final int trialVersion;

public TrialLicenseVersion(int trialVersion) {
this.trialVersion = trialVersion;
}

public TrialLicenseVersion(StreamInput in) throws IOException {
this.trialVersion = in.readVInt();
}

public static TrialLicenseVersion fromXContent(String from) {
try {
return new TrialLicenseVersion(Integer.parseInt(from));
} catch (NumberFormatException ex) {
return new TrialLicenseVersion(parseVersionString(from));
}
}

// copied from Version and simplified, for backwards compatibility parsing old version strings in LicensesMetadata XContent
private static int parseVersionString(String version) {
final boolean snapshot = version.endsWith("-SNAPSHOT"); // this is some BWC for 2.x and before indices
if (snapshot) {
version = version.substring(0, version.length() - 9);
}
String[] parts = version.split("[.-]");
if (parts.length != 3) {
throw new IllegalArgumentException("unable to parse trial license version: " + version);
}

try {
final int rawMajor = Integer.parseInt(parts[0]);
// we reverse the version id calculation based on some assumption as we can't reliably reverse the modulo
final int major = rawMajor * 1000000;
final int minor = Integer.parseInt(parts[1]) * 10000;
final int revision = Integer.parseInt(parts[2]) * 100;

// 99 is leftover from alpha/beta/rc, it should be removed
return major + minor + revision + 99;

} catch (NumberFormatException e) {
throw new IllegalArgumentException("unable to parse trial license version: " + version, e);
}
}

int asInt() {
return trialVersion;
}

public boolean ableToStartNewTrialSince(TrialLicenseVersion since) {
if (since.asInt() < TRIAL_VERSION_CUTOVER) {
int sinceMajorVersion = since.asInt() / 1_000_000; // integer division is intentional
return sinceMajorVersion < TRIAL_VERSION_CUTOVER_MAJOR;
}
return since.asInt() < trialVersion;
}

@Override
public String toString() {
return Integer.toString(trialVersion);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(trialVersion); // suffix added for BWC
}

// pkg-private for testing
String asVersionString() {
return this + ".0.0";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use this bwc format only in bwc cases? Does the license maintain the version bit as a string or version id?

Copy link
Contributor Author

@gwbrown gwbrown Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A string, unfortunately:

builder.field(Fields.TRIAL_LICENSE, trialVersion.toString());

(⬆️ code is from main, to be clear).

Note this is LicensesMetadata, rather than in the license itself, so we only store one of these for the whole cluster but it's still in the persistent cluster state. We can probably switch to emitting an integer, are there are any cases where we'd need an old version of ES to be able to parse XContent emitted by a newer version? The main case I'm aware of where we need to read a LicensesMetadata XContent is restoring a snapshot, and you can't restore a snapshot taken by a newer version than you're currently on AFAIK.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we only upgrade the license metadata format after the min version of the cluster has stabilized in the upgrade, we should be ok.

}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TrialLicenseVersion that = (TrialLicenseVersion) o;
return trialVersion == that.trialVersion;
}

@Override
public int hashCode() {
return Objects.hash(trialVersion);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeVInt(trialVersion);
}
}
Loading