Add type parameter to start_trial api (elastic/x-pack-elasticsearch#4102)

This is related to elastic/x-pack-elasticsearch#3877. This commit adds a paramer type to the
start_trial api. This parameter allows the user to pass a type (trial,
gold, or platinum) of license that will be generated. No matter what
type is choosen, you can only generate one per major version.

Original commit: elastic/x-pack-elasticsearch@b42234cbb5
This commit is contained in:
Tim Brooks 2018-03-13 19:28:11 -06:00 committed by GitHub
parent 3c82f24637
commit 498c110073
10 changed files with 228 additions and 77 deletions

View File

@ -5,7 +5,6 @@
*/ */
package org.elasticsearch.license; package org.elasticsearch.license;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListener;
@ -13,11 +12,9 @@ import org.elasticsearch.cluster.AckedClusterStateUpdateTask;
import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.cluster.ack.ClusterStateUpdateResponse; import org.elasticsearch.cluster.ack.ClusterStateUpdateResponse;
import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.Lifecycle;
import org.elasticsearch.common.joda.FormatDateTimeFormatter; import org.elasticsearch.common.joda.FormatDateTimeFormatter;
@ -36,12 +33,14 @@ import org.elasticsearch.xpack.core.scheduler.SchedulerEngine;
import java.time.Clock; import java.time.Clock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/** /**
@ -65,6 +64,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
// pkg private for tests // pkg private for tests
static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24);
static final Set<String> VALID_TRIAL_TYPES = new HashSet<>(Arrays.asList("trial", "platinum", "gold"));
/** /**
* Duration of grace period after a license has expired * 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; return license == LicensesMetaData.LICENSE_TOMBSTONE ? null : license;
} }
void startSelfGeneratedTrialLicense(final ActionListener<PostStartTrialResponse> listener) { void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostStartTrialResponse> listener) {
clusterService.submitStateUpdateTask("started self generated trial license", if (VALID_TRIAL_TYPES.contains(request.getType()) == false) {
new ClusterStateUpdateTask() { throw new IllegalArgumentException("Cannot start trial of type [" + request.getType() + "]. Valid trial types are "
@Override + VALID_TRIAL_TYPES + ".");
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));
} }
} StartTrialClusterTask task = new StartTrialClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener);
clusterService.submitStateUpdateTask("started trial license", task);
@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 startBasicLicense(PostStartBasicRequest request, final ActionListener<PostStartBasicResponse> listener) { void startBasicLicense(PostStartBasicRequest request, final ActionListener<PostStartBasicResponse> listener) {

View File

@ -7,6 +7,7 @@ package org.elasticsearch.license;
import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.rest.action.RestBuilderListener;
public class LicensingClient { public class LicensingClient {
@ -48,7 +49,7 @@ public class LicensingClient {
return new GetTrialStatusRequestBuilder(client, GetTrialStatusAction.INSTANCE); return new GetTrialStatusRequestBuilder(client, GetTrialStatusAction.INSTANCE);
} }
public void postUpgradeToTrial(PostStartTrialRequest request, ActionListener<PostStartTrialResponse> listener) { public void postStartTrial(PostStartTrialRequest request, ActionListener<PostStartTrialResponse> listener) {
client.execute(PostStartTrialAction.INSTANCE, request, listener); client.execute(PostStartTrialAction.INSTANCE, request, listener);
} }

View File

@ -5,13 +5,53 @@
*/ */
package org.elasticsearch.license; package org.elasticsearch.license;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.master.MasterNodeRequest; 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<PostStartTrialRequest> { public class PostStartTrialRequest extends MasterNodeRequest<PostStartTrialRequest> {
private String type;
@Override @Override
public ActionRequestValidationException validate() { public ActionRequestValidationException validate() {
return null; 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() + "].");
}
}
} }

View File

@ -8,32 +8,55 @@ package org.elasticsearch.license;
import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.rest.RestStatus;
import java.io.IOException; import java.io.IOException;
class PostStartTrialResponse extends ActionResponse { class PostStartTrialResponse extends ActionResponse {
enum STATUS { enum Status {
UPGRADED_TO_TRIAL, UPGRADED_TO_TRIAL(true, null, RestStatus.OK),
TRIAL_ALREADY_ACTIVATED 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;
} }
private STATUS status; boolean isTrialStarted() {
return isTrialStarted;
}
String getErrorMessage() {
return errorMessage;
}
RestStatus getRestStatus() {
return restStatus;
}
}
private Status status;
PostStartTrialResponse() { PostStartTrialResponse() {
} }
PostStartTrialResponse(STATUS status) { PostStartTrialResponse(Status status) {
this.status = status; this.status = status;
} }
public STATUS getStatus() { public Status getStatus() {
return status; return status;
} }
@Override @Override
public void readFrom(StreamInput in) throws IOException { public void readFrom(StreamInput in) throws IOException {
status = in.readEnum(STATUS.class); status = in.readEnum(Status.class);
} }
@Override @Override

View File

@ -11,7 +11,6 @@ import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.core.XPackClient; import org.elasticsearch.xpack.core.XPackClient;
import org.elasticsearch.xpack.core.rest.XPackRestHandler; import org.elasticsearch.xpack.core.rest.XPackRestHandler;
@ -29,23 +28,26 @@ public class RestPostStartTrialLicense extends XPackRestHandler {
@Override @Override
protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException { 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<PostStartTrialResponse>(channel) { new RestBuilderListener<PostStartTrialResponse>(channel) {
@Override @Override
public RestResponse buildResponse(PostStartTrialResponse response, XContentBuilder builder) throws Exception { public RestResponse buildResponse(PostStartTrialResponse response, XContentBuilder builder) throws Exception {
PostStartTrialResponse.STATUS status = response.getStatus(); PostStartTrialResponse.Status status = response.getStatus();
if (status == PostStartTrialResponse.STATUS.TRIAL_ALREADY_ACTIVATED) { if (status.isTrialStarted()) {
builder.startObject()
.field("trial_was_started", true)
.field("type", startTrialRequest.getType())
.endObject();
} else {
builder.startObject() builder.startObject()
.field("trial_was_started", false) .field("trial_was_started", false)
.field("error_message", "Operation failed: Trial was already activated.") .field("error_message", status.getErrorMessage())
.endObject(); .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);
} }
}); });
} }

View File

@ -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<PostStartTrialResponse> listener;
private final Clock clock;
StartTrialClusterTask(Logger logger, String clusterName, Clock clock, PostStartTrialRequest request,
ActionListener<PostStartTrialResponse> 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);
}
}

View File

@ -44,7 +44,7 @@ public class TransportPostStartTrialAction extends TransportMasterNodeAction<Pos
@Override @Override
protected void masterOperation(PostStartTrialRequest request, ClusterState state, protected void masterOperation(PostStartTrialRequest request, ClusterState state,
ActionListener<PostStartTrialResponse> listener) throws Exception { ActionListener<PostStartTrialResponse> listener) throws Exception {
licenseService.startSelfGeneratedTrialLicense(listener); licenseService.startTrialLicense(request, listener);
} }
@Override @Override

View File

@ -46,11 +46,9 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
return Arrays.asList(XPackClientPlugin.class, Netty4Plugin.class); return Arrays.asList(XPackClientPlugin.class, Netty4Plugin.class);
} }
public void testUpgradeToTrial() throws Exception { public void testStartTrial() throws Exception {
LicensingClient licensingClient = new LicensingClient(client()); LicensingClient licensingClient = new LicensingClient(client());
GetLicenseResponse getLicenseResponse = licensingClient.prepareGetLicense().get(); ensureStartingWithBasic();
assertEquals("basic", getLicenseResponse.license().type());
RestClient restClient = getRestClient(); RestClient restClient = getRestClient();
Response response = restClient.performRequest("GET", "/_xpack/license/trial_status"); Response response = restClient.performRequest("GET", "/_xpack/license/trial_status");
@ -58,22 +56,58 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals("{\"eligible_to_start_trial\":true}", body); 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)); String body2 = Streams.copyToString(new InputStreamReader(response2.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response2.getStatusLine().getStatusCode()); 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"); Response response3 = restClient.performRequest("GET", "/_xpack/license/trial_status");
String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8)); String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response3.getStatusLine().getStatusCode()); assertEquals(200, response3.getStatusLine().getStatusCode());
assertEquals("{\"eligible_to_start_trial\":false}", body3); assertEquals("{\"eligible_to_start_trial\":false}", body3);
String secondAttemptType = randomFrom(LicenseService.VALID_TRIAL_TYPES);
ResponseException ex = expectThrows(ResponseException.class, 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(); Response response4 = ex.getResponse();
String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8)); String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(403, response4.getStatusLine().getStatusCode()); assertEquals(403, response4.getStatusLine().getStatusCode());
assertTrue(body4.contains("\"trial_was_started\":false")); assertTrue(body4.contains("\"trial_was_started\":false"));
assertTrue(body4.contains("\"error_message\":\"Operation failed: Trial was already activated.\"")); 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());
});
}
} }

View File

@ -8,6 +8,10 @@
"parts" : { "parts" : {
}, },
"params": { "params": {
"type": {
"type" : "string",
"description" : "The type of trial license to generate (default: \"trial\")"
}
} }
}, },
"body": null "body": null

View File

@ -127,6 +127,12 @@ teardown:
- match: { trial_was_started: false } - match: { trial_was_started: false }
- match: { error_message: "Operation failed: Trial was already activated." } - 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": "Can start basic license if do not already have basic":
- do: - do:
xpack.license.get_basic_status: {} xpack.license.get_basic_status: {}