diff --git a/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index b523ec17f68..c5690f53266 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.license; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -13,11 +12,9 @@ import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateListener; -import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.ack.ClusterStateUpdateResponse; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.joda.FormatDateTimeFormatter; @@ -36,12 +33,14 @@ import org.elasticsearch.xpack.core.scheduler.SchedulerEngine; import java.time.Clock; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.UUID; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; /** @@ -65,6 +64,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste // pkg private for tests static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); + static final Set VALID_TRIAL_TYPES = new HashSet<>(Arrays.asList("trial", "platinum", "gold")); + /** * Duration of grace period after a license has expired */ @@ -305,52 +306,13 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste return license == LicensesMetaData.LICENSE_TOMBSTONE ? null : license; } - void startSelfGeneratedTrialLicense(final ActionListener listener) { - clusterService.submitStateUpdateTask("started self generated trial license", - new ClusterStateUpdateTask() { - @Override - public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { - LicensesMetaData licensesMetaData = oldState.metaData().custom(LicensesMetaData.TYPE); - logger.debug("started self generated trial license: {}", licensesMetaData); - - if (licensesMetaData == null || licensesMetaData.isEligibleForTrial()) { - listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.STATUS.UPGRADED_TO_TRIAL)); - } else { - listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.STATUS.TRIAL_ALREADY_ACTIVATED)); - } - } - - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - LicensesMetaData currentLicensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE); - - if (currentLicensesMetaData == null || currentLicensesMetaData.isEligibleForTrial()) { - long issueDate = clock.millis(); - MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); - long expiryDate = issueDate + NON_BASIC_SELF_GENERATED_LICENSE_DURATION.getMillis(); - - License.Builder specBuilder = License.builder() - .uid(UUID.randomUUID().toString()) - .issuedTo(clusterService.getClusterName().value()) - .maxNodes(SELF_GENERATED_LICENSE_MAX_NODES) - .issueDate(issueDate) - .type("trial") - .expiryDate(expiryDate); - License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder); - LicensesMetaData newLicensesMetaData = new LicensesMetaData(selfGeneratedLicense, Version.CURRENT); - mdBuilder.putCustom(LicensesMetaData.TYPE, newLicensesMetaData); - return ClusterState.builder(currentState).metaData(mdBuilder).build(); - } else { - return currentState; - } - } - - @Override - public void onFailure(String source, @Nullable Exception e) { - logger.error(new ParameterizedMessage("unexpected failure during [{}]", source), e); - listener.onFailure(e); - } - }); + void startTrialLicense(PostStartTrialRequest request, final ActionListener listener) { + if (VALID_TRIAL_TYPES.contains(request.getType()) == false) { + throw new IllegalArgumentException("Cannot start trial of type [" + request.getType() + "]. Valid trial types are " + + VALID_TRIAL_TYPES + "."); + } + StartTrialClusterTask task = new StartTrialClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener); + clusterService.submitStateUpdateTask("started trial license", task); } void startBasicLicense(PostStartBasicRequest request, final ActionListener listener) { diff --git a/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java b/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java index d95cdfbf6da..d2d4461b931 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java @@ -7,6 +7,7 @@ package org.elasticsearch.license; import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.rest.action.RestBuilderListener; public class LicensingClient { @@ -48,7 +49,7 @@ public class LicensingClient { return new GetTrialStatusRequestBuilder(client, GetTrialStatusAction.INSTANCE); } - public void postUpgradeToTrial(PostStartTrialRequest request, ActionListener listener) { + public void postStartTrial(PostStartTrialRequest request, ActionListener listener) { client.execute(PostStartTrialAction.INSTANCE, request, listener); } diff --git a/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequest.java b/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequest.java index 70377d6a2a7..5174281e8a4 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequest.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequest.java @@ -5,13 +5,53 @@ */ package org.elasticsearch.license; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; public class PostStartTrialRequest extends MasterNodeRequest { + private String type; + @Override public ActionRequestValidationException validate() { return null; } + + public PostStartTrialRequest setType(String type) { + this.type = type; + return this; + } + + public String getType() { + return type; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + // TODO: Change to 6.3 after backport + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + type = in.readString(); + } else { + type = "trial"; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + // TODO: Change to 6.3 after backport + Version version = Version.V_7_0_0_alpha1; + if (out.getVersion().onOrAfter(version)) { + out.writeString(type); + } else { + throw new IllegalArgumentException("All nodes in cluster must be version [" + version + + "] or newer to use `type` parameter. Attempting to write to node with version [" + out.getVersion() + "]."); + } + } } diff --git a/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialResponse.java b/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialResponse.java index 4235a303ec2..dcbdbfb6abd 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialResponse.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialResponse.java @@ -8,32 +8,55 @@ package org.elasticsearch.license; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.rest.RestStatus; import java.io.IOException; class PostStartTrialResponse extends ActionResponse { - enum STATUS { - UPGRADED_TO_TRIAL, - TRIAL_ALREADY_ACTIVATED + enum Status { + UPGRADED_TO_TRIAL(true, null, RestStatus.OK), + TRIAL_ALREADY_ACTIVATED(false, "Operation failed: Trial was already activated.", RestStatus.FORBIDDEN); + + private final boolean isTrialStarted; + private final String errorMessage; + private final RestStatus restStatus; + + Status(boolean isTrialStarted, String errorMessage, RestStatus restStatus) { + this.isTrialStarted = isTrialStarted; + this.errorMessage = errorMessage; + this.restStatus = restStatus; + } + + boolean isTrialStarted() { + return isTrialStarted; + } + + String getErrorMessage() { + return errorMessage; + } + + RestStatus getRestStatus() { + return restStatus; + } } - private STATUS status; + private Status status; PostStartTrialResponse() { } - PostStartTrialResponse(STATUS status) { + PostStartTrialResponse(Status status) { this.status = status; } - public STATUS getStatus() { + public Status getStatus() { return status; } @Override public void readFrom(StreamInput in) throws IOException { - status = in.readEnum(STATUS.class); + status = in.readEnum(Status.class); } @Override diff --git a/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java b/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java index b65d7b085c1..0332eedd69d 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java @@ -11,7 +11,6 @@ import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; -import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.xpack.core.XPackClient; import org.elasticsearch.xpack.core.rest.XPackRestHandler; @@ -29,23 +28,26 @@ public class RestPostStartTrialLicense extends XPackRestHandler { @Override protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException { - return channel -> client.licensing().preparePostUpgradeToTrial().execute( + PostStartTrialRequest startTrialRequest = new PostStartTrialRequest(); + startTrialRequest.setType(request.param("type", "trial")); + return channel -> client.licensing().postStartTrial(startTrialRequest, new RestBuilderListener(channel) { @Override public RestResponse buildResponse(PostStartTrialResponse response, XContentBuilder builder) throws Exception { - PostStartTrialResponse.STATUS status = response.getStatus(); - if (status == PostStartTrialResponse.STATUS.TRIAL_ALREADY_ACTIVATED) { + PostStartTrialResponse.Status status = response.getStatus(); + if (status.isTrialStarted()) { + builder.startObject() + .field("trial_was_started", true) + .field("type", startTrialRequest.getType()) + .endObject(); + } else { builder.startObject() .field("trial_was_started", false) - .field("error_message", "Operation failed: Trial was already activated.") + .field("error_message", status.getErrorMessage()) .endObject(); - return new BytesRestResponse(RestStatus.FORBIDDEN, builder); - } else if (status == PostStartTrialResponse.STATUS.UPGRADED_TO_TRIAL) { - builder.startObject().field("trial_was_started", true).endObject(); - return new BytesRestResponse(RestStatus.OK, builder); - } else { - throw new IllegalArgumentException("Unexpected status for PostStartTrialResponse: [" + status + "]"); + } + return new BytesRestResponse(status.getRestStatus(), builder); } }); } diff --git a/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java b/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java new file mode 100644 index 00000000000..3ca8dbf0eaa --- /dev/null +++ b/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.Nullable; + +import java.time.Clock; +import java.util.UUID; + +public class StartTrialClusterTask extends ClusterStateUpdateTask { + + private final Logger logger; + private final String clusterName; + private final PostStartTrialRequest request; + private final ActionListener listener; + private final Clock clock; + + StartTrialClusterTask(Logger logger, String clusterName, Clock clock, PostStartTrialRequest request, + ActionListener listener) { + this.logger = logger; + this.clusterName = clusterName; + this.request = request; + this.listener = listener; + this.clock = clock; + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + LicensesMetaData oldLicensesMetaData = oldState.metaData().custom(LicensesMetaData.TYPE); + logger.debug("started self generated trial license: {}", oldLicensesMetaData); + + if (oldLicensesMetaData == null || oldLicensesMetaData.isEligibleForTrial()) { + listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.UPGRADED_TO_TRIAL)); + } else { + listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.TRIAL_ALREADY_ACTIVATED)); + } + } + + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + LicensesMetaData currentLicensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE); + + if (currentLicensesMetaData == null || currentLicensesMetaData.isEligibleForTrial()) { + long issueDate = clock.millis(); + MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); + long expiryDate = issueDate + LicenseService.NON_BASIC_SELF_GENERATED_LICENSE_DURATION.getMillis(); + + License.Builder specBuilder = License.builder() + .uid(UUID.randomUUID().toString()) + .issuedTo(clusterName) + .maxNodes(LicenseService.SELF_GENERATED_LICENSE_MAX_NODES) + .issueDate(issueDate) + .type(request.getType()) + .expiryDate(expiryDate); + License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder); + LicensesMetaData newLicensesMetaData = new LicensesMetaData(selfGeneratedLicense, Version.CURRENT); + mdBuilder.putCustom(LicensesMetaData.TYPE, newLicensesMetaData); + return ClusterState.builder(currentState).metaData(mdBuilder).build(); + } else { + return currentState; + } + } + + @Override + public void onFailure(String source, @Nullable Exception e) { + logger.error(new ParameterizedMessage("unexpected failure during [{}]", source), e); + listener.onFailure(e); + } +} diff --git a/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java b/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java index 2b226f75a95..e70662cdc31 100644 --- a/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java +++ b/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java @@ -44,7 +44,7 @@ public class TransportPostStartTrialAction extends TransportMasterNodeAction listener) throws Exception { - licenseService.startSelfGeneratedTrialLicense(listener); + licenseService.startTrialLicense(request, listener); } @Override diff --git a/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java b/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java index 7ebf5b444e6..d673c4e7204 100644 --- a/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java +++ b/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java @@ -46,11 +46,9 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase return Arrays.asList(XPackClientPlugin.class, Netty4Plugin.class); } - public void testUpgradeToTrial() throws Exception { + public void testStartTrial() throws Exception { LicensingClient licensingClient = new LicensingClient(client()); - GetLicenseResponse getLicenseResponse = licensingClient.prepareGetLicense().get(); - - assertEquals("basic", getLicenseResponse.license().type()); + ensureStartingWithBasic(); RestClient restClient = getRestClient(); Response response = restClient.performRequest("GET", "/_xpack/license/trial_status"); @@ -58,22 +56,58 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals("{\"eligible_to_start_trial\":true}", body); - Response response2 = restClient.performRequest("POST", "/_xpack/license/start_trial"); + String type = randomFrom(LicenseService.VALID_TRIAL_TYPES); + + Response response2 = restClient.performRequest("POST", "/_xpack/license/start_trial?type=" + type); String body2 = Streams.copyToString(new InputStreamReader(response2.getEntity().getContent(), StandardCharsets.UTF_8)); assertEquals(200, response2.getStatusLine().getStatusCode()); - assertEquals("{\"trial_was_started\":true}", body2); + assertTrue(body2.contains("\"trial_was_started\":true")); + assertTrue(body2.contains("\"type\":\"" + type + "\"")); + + assertBusy(() -> { + GetLicenseResponse postTrialLicenseResponse = licensingClient.prepareGetLicense().get(); + assertEquals(type, postTrialLicenseResponse.license().type()); + }); Response response3 = restClient.performRequest("GET", "/_xpack/license/trial_status"); String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8)); assertEquals(200, response3.getStatusLine().getStatusCode()); assertEquals("{\"eligible_to_start_trial\":false}", body3); + String secondAttemptType = randomFrom(LicenseService.VALID_TRIAL_TYPES); + ResponseException ex = expectThrows(ResponseException.class, - () -> restClient.performRequest("POST", "/_xpack/license/start_trial")); + () -> restClient.performRequest("POST", "/_xpack/license/start_trial?type=" + secondAttemptType)); Response response4 = ex.getResponse(); String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8)); assertEquals(403, response4.getStatusLine().getStatusCode()); assertTrue(body4.contains("\"trial_was_started\":false")); assertTrue(body4.contains("\"error_message\":\"Operation failed: Trial was already activated.\"")); } + + public void testInvalidType() throws Exception { + ensureStartingWithBasic(); + + ResponseException ex = expectThrows(ResponseException.class, () -> + getRestClient().performRequest("POST", "/_xpack/license/start_trial?type=basic")); + Response response = ex.getResponse(); + String body = Streams.copyToString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); + assertEquals(400, response.getStatusLine().getStatusCode()); + assertTrue(body.contains("\"type\":\"illegal_argument_exception\"")); + assertTrue(body.contains("\"reason\":\"Cannot start trial of type [basic]. Valid trial types are [")); + } + + private void ensureStartingWithBasic() throws Exception { + LicensingClient licensingClient = new LicensingClient(client()); + GetLicenseResponse getLicenseResponse = licensingClient.prepareGetLicense().get(); + + if ("basic".equals(getLicenseResponse.license().type()) == false) { + licensingClient.preparePostStartBasic().setAcknowledge(true).get(); + } + + assertBusy(() -> { + GetLicenseResponse postTrialLicenseResponse = licensingClient.prepareGetLicense().get(); + assertEquals("basic", postTrialLicenseResponse.license().type()); + }); + } } diff --git a/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json b/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json index ea0d294b1d3..688afc7b79b 100644 --- a/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json +++ b/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json @@ -8,6 +8,10 @@ "parts" : { }, "params": { + "type": { + "type" : "string", + "description" : "The type of trial license to generate (default: \"trial\")" + } } }, "body": null diff --git a/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml b/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml index e82a93f8a8d..ae55df60deb 100644 --- a/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml +++ b/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml @@ -127,6 +127,12 @@ teardown: - match: { trial_was_started: false } - match: { error_message: "Operation failed: Trial was already activated." } --- +"Trial license cannot be basic": + - do: + catch: bad_request + xpack.license.post_start_trial: + type: "basic" +--- "Can start basic license if do not already have basic": - do: xpack.license.get_basic_status: {}