diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java index bf8abc21fe135..a5ce0a3cb2774 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java @@ -22,6 +22,8 @@ import org.apache.http.HttpEntity; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.license.StartBasicRequest; +import org.elasticsearch.client.license.StartBasicResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.xcontent.DeprecationHandler; @@ -121,6 +123,28 @@ public void deleteLicenseAsync(DeleteLicenseRequest request, RequestOptions opti AcknowledgedResponse::fromXContent, listener, emptySet()); } + /** + * Initiates an indefinite basic license. + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public StartBasicResponse startBasic(StartBasicRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, LicenseRequestConverters::startBasic, options, + StartBasicResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously initiates an indefinite basic license. + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void startBasicAsync(StartBasicRequest request, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, LicenseRequestConverters::startBasic, options, + StartBasicResponse::fromXContent, listener, emptySet()); + } + /** * Converts an entire response into a json string * diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java index 7c2c049324eab..159ab5690344d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java @@ -21,7 +21,9 @@ import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.license.StartBasicRequest; import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest; import org.elasticsearch.protocol.xpack.license.GetLicenseRequest; import org.elasticsearch.protocol.xpack.license.PutLicenseRequest; @@ -61,4 +63,18 @@ static Request deleteLicense(DeleteLicenseRequest deleteLicenseRequest) { parameters.withMasterTimeout(deleteLicenseRequest.masterNodeTimeout()); return request; } + + static Request startBasic(StartBasicRequest startBasicRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_xpack", "license", "start_basic") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + RequestConverters.Params parameters = new RequestConverters.Params(request); + parameters.withTimeout(startBasicRequest.timeout()); + parameters.withMasterTimeout(startBasicRequest.masterNodeTimeout()); + if (startBasicRequest.isAcknowledge()) { + parameters.putParam("acknowledge", "true"); + } + return request; + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 9c461a404cf8f..57e63791fd30f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -980,9 +980,11 @@ EndpointBuilder addCommaSeparatedPathParts(String[] parts) { return this; } - EndpointBuilder addPathPartAsIs(String part) { - if (Strings.hasLength(part)) { - joiner.add(part); + EndpointBuilder addPathPartAsIs(String ... parts) { + for (String part : parts) { + if (Strings.hasLength(part)) { + joiner.add(part); + } } return this; } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java index af8fbe3e72b37..c26a7ba48ca17 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java @@ -20,6 +20,8 @@ import org.elasticsearch.common.unit.TimeValue; +import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; + /** * A base request for any requests that supply timeouts. * @@ -28,8 +30,11 @@ */ public class TimedRequest implements Validatable { - private TimeValue timeout; - private TimeValue masterTimeout; + public static final TimeValue DEFAULT_ACK_TIMEOUT = timeValueSeconds(30); + public static final TimeValue DEFAULT_MASTER_NODE_TIMEOUT = TimeValue.timeValueSeconds(30); + + private TimeValue timeout = DEFAULT_ACK_TIMEOUT; + private TimeValue masterTimeout = DEFAULT_MASTER_NODE_TIMEOUT; public void setTimeout(TimeValue timeout) { this.timeout = timeout; diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicRequest.java new file mode 100644 index 0000000000000..00a0cc421aa62 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicRequest.java @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.license; + +import org.elasticsearch.client.TimedRequest; + +public class StartBasicRequest extends TimedRequest { + private final boolean acknowledge; + + public StartBasicRequest() { + this(false); + } + + public StartBasicRequest(boolean acknowledge) { + this.acknowledge = acknowledge; + } + + public boolean isAcknowledge() { + return acknowledge; + } +} + diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicResponse.java new file mode 100644 index 0000000000000..f6ab026402462 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartBasicResponse.java @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.license; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.RestStatus; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +public class StartBasicResponse { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "start_basic_response", true, (a, v) -> { + boolean basicWasStarted = (Boolean) a[0]; + String errorMessage = (String) a[1]; + + if (basicWasStarted) { + return new StartBasicResponse(StartBasicResponse.Status.GENERATED_BASIC); + } + StartBasicResponse.Status status = StartBasicResponse.Status.fromErrorMessage(errorMessage); + @SuppressWarnings("unchecked") Tuple> acknowledgements = (Tuple>) a[2]; + return new StartBasicResponse(status, acknowledgements.v2(), acknowledgements.v1()); + }); + + static { + PARSER.declareBoolean(constructorArg(), new ParseField("basic_was_started")); + PARSER.declareString(optionalConstructorArg(), new ParseField("error_message")); + PARSER.declareObject(optionalConstructorArg(), (parser, v) -> { + Map acknowledgeMessages = new HashMap<>(); + String message = null; + XContentParser.Token token; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + if (currentFieldName == null) { + throw new XContentParseException(parser.getTokenLocation(), "expected message header or acknowledgement"); + } + if (new ParseField("message").getPreferredName().equals(currentFieldName)) { + ensureExpectedToken(XContentParser.Token.VALUE_STRING, token, parser::getTokenLocation); + message = parser.text(); + } else { + if (token != XContentParser.Token.START_ARRAY) { + throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement type"); + } + List acknowledgeMessagesList = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + ensureExpectedToken(XContentParser.Token.VALUE_STRING, token, parser::getTokenLocation); + acknowledgeMessagesList.add(parser.text()); + } + acknowledgeMessages.put(currentFieldName, acknowledgeMessagesList.toArray(new String[0])); + } + } + } + return new Tuple<>(message, acknowledgeMessages); + }, + new ParseField("acknowledge")); + } + + private Map acknowledgeMessages; + private String acknowledgeMessage; + + enum Status { + GENERATED_BASIC(true, null, RestStatus.OK), + ALREADY_USING_BASIC(false, "Operation failed: Current license is basic.", RestStatus.FORBIDDEN), + NEED_ACKNOWLEDGEMENT(false, "Operation failed: Needs acknowledgement.", RestStatus.OK); + + private final boolean isBasicStarted; + private final String errorMessage; + private final RestStatus restStatus; + + Status(boolean isBasicStarted, String errorMessage, RestStatus restStatus) { + this.isBasicStarted = isBasicStarted; + this.errorMessage = errorMessage; + this.restStatus = restStatus; + } + + String getErrorMessage() { + return errorMessage; + } + + boolean isBasicStarted() { + return isBasicStarted; + } + + static StartBasicResponse.Status fromErrorMessage(final String errorMessage) { + final StartBasicResponse.Status[] values = StartBasicResponse.Status.values(); + for (StartBasicResponse.Status status : values) { + if (Objects.equals(status.errorMessage, errorMessage)) { + return status; + } + } + throw new IllegalArgumentException("No status for error message ['" + errorMessage + "']"); + } + } + + private StartBasicResponse.Status status; + + public StartBasicResponse() { + } + + StartBasicResponse(StartBasicResponse.Status status) { + this(status, Collections.emptyMap(), null); + } + + StartBasicResponse(StartBasicResponse.Status status, + Map acknowledgeMessages, String acknowledgeMessage) { + this.status = status; + this.acknowledgeMessages = acknowledgeMessages; + this.acknowledgeMessage = acknowledgeMessage; + } + + public boolean isAcknowledged() { + return status != StartBasicResponse.Status.NEED_ACKNOWLEDGEMENT; + } + + public boolean isBasicStarted() { + return status.isBasicStarted; + } + + public String getErrorMessage() { + return status.errorMessage; + } + + public String getAcknowledgeMessage() { + return acknowledgeMessage; + } + + public Map getAcknowledgeMessages() { + return acknowledgeMessages; + } + + public static StartBasicResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/LicenseRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/LicenseRequestConvertersTests.java new file mode 100644 index 0000000000000..9fb8ae033d886 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/LicenseRequestConvertersTests.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client; + +import org.apache.http.client.methods.HttpPost; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.client.license.StartBasicRequest; +import org.elasticsearch.test.ESTestCase; + +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.client.RequestConvertersTests.setRandomMasterTimeout; +import static org.elasticsearch.client.RequestConvertersTests.setRandomTimeout; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; + +public class LicenseRequestConvertersTests extends ESTestCase { + public void testStartBasic() { + final boolean acknowledge = randomBoolean(); + StartBasicRequest startBasicRequest = new StartBasicRequest(acknowledge); + Map expectedParams = new HashMap<>(); + if (acknowledge) { + expectedParams.put("acknowledge", Boolean.TRUE.toString()); + } + + setRandomTimeout(startBasicRequest, AcknowledgedRequest.DEFAULT_ACK_TIMEOUT, expectedParams); + setRandomMasterTimeout(startBasicRequest, expectedParams); + Request request = LicenseRequestConverters.startBasic(startBasicRequest); + + assertThat(request.getMethod(), equalTo(HttpPost.METHOD_NAME)); + assertThat(request.getEndpoint(), equalTo("/_xpack/license/start_basic")); + assertThat(request.getParameters(), equalTo(expectedParams)); + assertThat(request.getEntity(), is(nullValue())); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/LicensingIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/LicensingIT.java new file mode 100644 index 0000000000000..506eb71d66d96 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/LicensingIT.java @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client; + +import org.elasticsearch.Build; +import org.elasticsearch.client.license.StartBasicRequest; +import org.elasticsearch.client.license.StartBasicResponse; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.protocol.xpack.license.LicensesStatus; +import org.elasticsearch.protocol.xpack.license.PutLicenseRequest; +import org.elasticsearch.protocol.xpack.license.PutLicenseResponse; +import org.junit.After; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.empty; + +public class LicensingIT extends ESRestHighLevelClientTestCase { + + @BeforeClass + public static void checkForSnapshot() { + assumeTrue("Trial license used to rollback is only valid when tested against snapshot/test builds", + Build.CURRENT.isSnapshot()); + } + + @After + public void rollbackToTrial() throws IOException { + putTrialLicense(); + } + + public static void putTrialLicense() throws IOException { + assumeTrue("Trial license is only valid when tested against snapshot/test builds", + Build.CURRENT.isSnapshot()); + + // use a hard-coded trial license for 20 yrs to be able to roll back from another licenses + final String licenseDefinition = Strings.toString(jsonBuilder() + .startObject() + .field("licenses", Arrays.asList( + MapBuilder.newMapBuilder() + .put("uid", "96fc37c6-6fc9-43e2-a40d-73143850cd72") + .put("type", "trial") + // 2018-10-16 07:02:48 UTC + .put("issue_date_in_millis", "1539673368158") + // 2038-10-11 07:02:48 UTC, 20 yrs later + .put("expiry_date_in_millis", "2170393368158") + .put("max_nodes", "5") + .put("issued_to", "client_rest-high-level_integTestCluster") + .put("issuer", "elasticsearch") + .put("start_date_in_millis", "-1") + .put("signature", + "AAAABAAAAA3FXON9kGmNqmH+ASDWAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAcdKHL0BfM2uqTgT7BDuFxX5lb" + + "t/bHDVJ421Wwgm5p3IMbw/W13iiAHz0hhDziF7acJbc/y65L+BKGtVC1gSSHeLDHaAD66VrjKxfc7VbGyJIAYBOdujf0rheurmaD3IcNo" + + "/tWDjCdtTwrNziFkorsGcPadBP5Yc6csk3/Q74DlfiYweMBxLUfkBERwxwd5OQS6ujGvl/4bb8p5zXvOw8vMSaAXSXXnExP6lam+0934W" + + "0kHvU7IGk+fCUjOaiSWKSoE4TEcAtVNYj/oRoRtfQ1KQGpdCHxTHs1BimdZaG0nBHDsvhYlVVLSvHN6QzqsHWgFDG6JJxhtU872oTRSUHA=") + .immutableMap())) + .endObject()); + + final PutLicenseRequest request = new PutLicenseRequest(); + request.setAcknowledge(true); + request.setLicenseDefinition(licenseDefinition); + final PutLicenseResponse response = highLevelClient().license().putLicense(request, RequestOptions.DEFAULT); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(response.status(), equalTo(LicensesStatus.VALID)); + } + + public void testStartBasic() throws Exception { + // we don't test the case where we successfully start a basic because the integ test cluster generates one on startup + // and we don't have a good way to prevent that / work around it in this test project + // case where we don't acknowledge basic license conditions + { + final StartBasicRequest request = new StartBasicRequest(); + final StartBasicResponse response = highLevelClient().license().startBasic(request, RequestOptions.DEFAULT); + assertThat(response.isAcknowledged(), equalTo(false)); + assertThat(response.isBasicStarted(), equalTo(false)); + assertThat(response.getErrorMessage(), equalTo("Operation failed: Needs acknowledgement.")); + assertThat(response.getAcknowledgeMessage(), + containsString("This license update requires acknowledgement. " + + "To acknowledge the license, please read the following messages and call /start_basic again")); + assertNotEmptyAcknowledgeMessages(response); + } + // case where we acknowledge and the basic is started successfully + { + final StartBasicRequest request = new StartBasicRequest(true); + final StartBasicResponse response = highLevelClient().license().startBasic(request, RequestOptions.DEFAULT); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(response.isBasicStarted(), equalTo(true)); + assertThat(response.getErrorMessage(), nullValue()); + assertThat(response.getAcknowledgeMessage(), nullValue()); + assertThat(response.getAcknowledgeMessages().size(), equalTo(0)); + } + } + + private static void assertNotEmptyAcknowledgeMessages(StartBasicResponse response) { + assertThat(response.getAcknowledgeMessages().entrySet(), not(empty())); + for (Map.Entry entry : response.getAcknowledgeMessages().entrySet()) { + assertThat(entry.getKey(), not(isEmptyOrNullString())); + final List messages = Arrays.asList(entry.getValue()); + for (String message : messages) { + assertThat(message, not(isEmptyOrNullString())); + } + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 3801dfe71de9c..8887bed226ca1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -1572,6 +1572,12 @@ static void setRandomLocal(MasterNodeReadRequest request, Map setRandomLocal(request::local, expectedParams); } + static void setRandomTimeout(TimedRequest request, TimeValue defaultTimeout, Map expectedParams) { + setRandomTimeout(s -> + request.setTimeout(TimeValue.parseTimeValue(s, request.getClass().getName() + ".timeout")), + defaultTimeout, expectedParams); + } + static void setRandomTimeout(Consumer setter, TimeValue defaultTimeout, Map expectedParams) { if (randomBoolean()) { String timeout = randomTimeValue(); @@ -1583,9 +1589,19 @@ static void setRandomTimeout(Consumer setter, TimeValue defaultTimeout, } static void setRandomMasterTimeout(MasterNodeRequest request, Map expectedParams) { + setRandomMasterTimeout(request::masterNodeTimeout, expectedParams); + } + + static void setRandomMasterTimeout(TimedRequest request, Map expectedParams) { + setRandomMasterTimeout(s -> + request.setMasterTimeout(TimeValue.parseTimeValue(s, request.getClass().getName() + ".masterNodeTimeout")), + expectedParams); + } + + static void setRandomMasterTimeout(Consumer setter, Map expectedParams) { if (randomBoolean()) { String masterTimeout = randomTimeValue(); - request.masterNodeTimeout(masterTimeout); + setter.accept(masterTimeout); expectedParams.put("master_timeout", masterTimeout); } else { expectedParams.put("master_timeout", MasterNodeRequest.DEFAULT_MASTER_NODE_TIMEOUT.getStringRep()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java index 3737bd70a8dae..d1987420f0200 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java @@ -26,6 +26,8 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.license.StartBasicRequest; +import org.elasticsearch.client.license.StartBasicResponse; import org.elasticsearch.common.Booleans; import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest; import org.elasticsearch.protocol.xpack.license.GetLicenseRequest; @@ -33,11 +35,15 @@ import org.elasticsearch.protocol.xpack.license.LicensesStatus; import org.elasticsearch.protocol.xpack.license.PutLicenseRequest; import org.elasticsearch.protocol.xpack.license.PutLicenseResponse; +import org.junit.After; +import org.junit.BeforeClass; +import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.elasticsearch.client.LicensingIT.putTrialLicense; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.hasSize; @@ -50,8 +56,18 @@ */ public class LicensingDocumentationIT extends ESRestHighLevelClientTestCase { + @BeforeClass + public static void checkForSnapshot() { + assumeTrue("Trial license used to rollback is only valid when tested against snapshot/test builds", + Build.CURRENT.isSnapshot()); + } + + @After + public void rollbackToTrial() throws IOException { + putTrialLicense(); + } + public void testLicense() throws Exception { - assumeTrue("License is only valid when tested against snapshot/test builds", Build.CURRENT.isSnapshot()); RestHighLevelClient client = highLevelClient(); String license = "{\"license\": {\"uid\":\"893361dc-9749-4997-93cb-802e3d7fa4a8\",\"type\":\"gold\"," + "\"issue_date_in_millis\":1411948800000,\"expiry_date_in_millis\":1914278399999,\"max_nodes\":1,\"issued_to\":\"issued_to\"," + @@ -215,4 +231,50 @@ public void onFailure(Exception e) { assertThat(currentLicense, endsWith("}")); } } + + public void testPostStartBasic() throws Exception { + RestHighLevelClient client = highLevelClient(); + { + //tag::start-basic-execute + StartBasicRequest request = new StartBasicRequest(); + + StartBasicResponse response = client.license().startBasic(request, RequestOptions.DEFAULT); + //end::start-basic-execute + + //tag::start-basic-response + boolean acknowledged = response.isAcknowledged(); // <1> + boolean basicStarted = response.isBasicStarted(); // <2> + String errorMessage = response.getErrorMessage(); // <3> + String acknowledgeMessage = response.getAcknowledgeMessage(); // <4> + Map acknowledgeMessages = response.getAcknowledgeMessages(); // <5> + //end::start-basic-response + } + { + StartBasicRequest request = new StartBasicRequest(); + // tag::start-basic-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(StartBasicResponse indexResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::start-basic-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::start-basic-execute-async + client.license().startBasicAsync( + request, RequestOptions.DEFAULT, listener); // <1> + // end::start-basic-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/license/StartBasicResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/license/StartBasicResponseTests.java new file mode 100644 index 0000000000000..0679af8b6fdc8 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/license/StartBasicResponseTests.java @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.license; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.protocol.xpack.common.ProtocolUtils; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; + +public class StartBasicResponseTests extends ESTestCase { + + public void testFromXContent() throws Exception { + StartBasicResponse.Status status = randomFrom(StartBasicResponse.Status.values()); + + boolean acknowledged = status != StartBasicResponse.Status.NEED_ACKNOWLEDGEMENT; + String acknowledgeMessage = null; + Map ackMessages = Collections.emptyMap(); + if (status != StartBasicResponse.Status.GENERATED_BASIC) { + acknowledgeMessage = randomAlphaOfLength(10); + ackMessages = randomAckMessages(); + } + + final StartBasicResponse startBasicResponse = new StartBasicResponse(status, ackMessages, acknowledgeMessage); + + XContentType xContentType = randomFrom(XContentType.values()); + XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + + toXContent(startBasicResponse, builder); + + final StartBasicResponse response = StartBasicResponse.fromXContent(createParser(builder)); + assertThat(response.isAcknowledged(), equalTo(acknowledged)); + assertThat(response.isBasicStarted(), equalTo(status.isBasicStarted())); + assertThat(response.getAcknowledgeMessage(), equalTo(acknowledgeMessage)); + assertThat(ProtocolUtils.equals(response.getAcknowledgeMessages(), ackMessages), equalTo(true)); + } + + private static void toXContent(StartBasicResponse response, XContentBuilder builder) throws IOException { + builder.startObject(); + builder.field("acknowledged", response.isAcknowledged()); + if (response.isBasicStarted()) { + builder.field("basic_was_started", true); + } else { + builder.field("basic_was_started", false); + builder.field("error_message", response.getErrorMessage()); + } + if (response.getAcknowledgeMessages().isEmpty() == false) { + builder.startObject("acknowledge"); + builder.field("message", response.getAcknowledgeMessage()); + for (Map.Entry entry : response.getAcknowledgeMessages().entrySet()) { + builder.startArray(entry.getKey()); + for (String message : entry.getValue()) { + builder.value(message); + } + builder.endArray(); + } + builder.endObject(); + } + builder.endObject(); + } + + private static Map randomAckMessages() { + int nFeatures = randomIntBetween(1, 5); + + Map ackMessages = new HashMap<>(); + + for (int i = 0; i < nFeatures; i++) { + String feature = randomAlphaOfLengthBetween(9, 15); + int nMessages = randomIntBetween(1, 5); + String[] messages = new String[nMessages]; + for (int j = 0; j < nMessages; j++) { + messages[j] = randomAlphaOfLengthBetween(10, 30); + } + ackMessages.put(feature, messages); + } + + return ackMessages; + } + +} diff --git a/docs/java-rest/high-level/licensing/start-basic.asciidoc b/docs/java-rest/high-level/licensing/start-basic.asciidoc new file mode 100644 index 0000000000000..3ff50cfd2db60 --- /dev/null +++ b/docs/java-rest/high-level/licensing/start-basic.asciidoc @@ -0,0 +1,67 @@ +[[java-rest-high-start-basic]] +=== Start Basic License + +[[java-rest-high-start-basic-execution]] +==== Execution + +This API creates and enables a basic license using the `startBasic()` method. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-basic-execute] +-------------------------------------------------- + +[[java-rest-high-start-basic-response]] +==== Response + +The returned `StartBasicResponse` returns a field indicating whether the +basic was started. If it was started, the response returns a the type of +license started. If it was not started, it returns an error message describing +why. + +Acknowledgement messages may also be returned if this API was called without +the `acknowledge` flag set to `true`. In this case you need to display the +messages to the end user and if they agree, resubmit the request with the +`acknowledge` flag set to `true`. Please note that the response will still +return a 200 return code even if it requires an acknowledgement. So, it is +necessary to check the `acknowledged` flag. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-basic-response] +-------------------------------------------------- +<1> Whether or not the request had the `acknowledge` flag set +<2> Whether or not this request caused a basic to start +<3> If this request did not cause a basic to start, a message explaining why +<4> If the user's request did not have the `acknowledge` flag set, a summary +of the user's acknowledgement required for this API +<5> If the user's request did not have the `acknowledge` flag set, contains +keys of commercial features and values of messages describing how they will +be affected by licensing changes as the result of starting a basic + +[[java-rest-high-start-basic-async]] +==== Asynchronous Execution + +This request can be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-basic-execute-async] +-------------------------------------------------- +<1> The `StartBasicResponse` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `StartBasicResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-basic-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument +<2> Called in case of failure. The raised exception is provided as an argument diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 7ee39ac861cc9..9896c6d0552b6 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -214,10 +214,12 @@ The Java High Level REST Client supports the following Licensing APIs: * <> * <> * <> +* <> include::licensing/put-license.asciidoc[] include::licensing/get-license.asciidoc[] include::licensing/delete-license.asciidoc[] +include::licensing/start-basic.asciidoc[] == Machine Learning APIs :upid: {mainid}-x-pack-ml