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;
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<String> 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<PostStartTrialResponse> 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));
void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostStartTrialResponse> 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 + ".");
}
}
@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);
}
});
StartTrialClusterTask task = new StartTrialClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener);
clusterService.submitStateUpdateTask("started trial license", task);
}
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.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<PostStartTrialResponse> listener) {
public void postStartTrial(PostStartTrialRequest request, ActionListener<PostStartTrialResponse> listener) {
client.execute(PostStartTrialAction.INSTANCE, request, listener);
}

View File

@ -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<PostStartTrialRequest> {
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() + "].");
}
}
}

View File

@ -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;
}
private STATUS status;
boolean isTrialStarted() {
return isTrialStarted;
}
String getErrorMessage() {
return errorMessage;
}
RestStatus getRestStatus() {
return restStatus;
}
}
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

View File

@ -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<PostStartTrialResponse>(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);
}
});
}

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
protected void masterOperation(PostStartTrialRequest request, ClusterState state,
ActionListener<PostStartTrialResponse> listener) throws Exception {
licenseService.startSelfGeneratedTrialLicense(listener);
licenseService.startTrialLicense(request, listener);
}
@Override

View File

@ -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());
});
}
}

View File

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

View File

@ -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: {}