Browse Source

HLRC: Add support for XPack Post Start Basic Licence API (#33606)

Relates to #29827
Vladimir Dolzhenko 7 years ago
parent
commit
230ad5339b

+ 24 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java

@@ -22,6 +22,8 @@ package org.elasticsearch.client;
 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 final class LicenseClient {
             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<StartBasicResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, LicenseRequestConverters::startBasic, options,
+            StartBasicResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Converts an entire response into a json string
      *

+ 16 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java

@@ -21,7 +21,9 @@ package org.elasticsearch.client;
 
 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 @@ public class LicenseRequestConverters {
         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;
+    }
 }

+ 5 - 3
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -980,9 +980,11 @@ final class RequestConverters {
             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;
         }

+ 7 - 2
client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java

@@ -20,6 +20,8 @@ package org.elasticsearch.client;
 
 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 @@ import org.elasticsearch.common.unit.TimeValue;
  */
 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;

+ 38 - 0
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;
+    }
+}
+

+ 168 - 0
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<StartBasicResponse, Void> 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<String, Map<String, String[]>> acknowledgements = (Tuple<String, Map<String, String[]>>) 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<String, String[]> 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<String> 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<String, String[]> 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<String, String[]> 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<String, String[]> getAcknowledgeMessages() {
+        return acknowledgeMessages;
+    }
+
+    public static StartBasicResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+}

+ 53 - 0
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<String, String> 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()));
+    }
+}

+ 130 - 0
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.<String, Object>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<String, String[]> entry : response.getAcknowledgeMessages().entrySet()) {
+            assertThat(entry.getKey(), not(isEmptyOrNullString()));
+            final List<String> messages = Arrays.asList(entry.getValue());
+            for (String message : messages) {
+                assertThat(message, not(isEmptyOrNullString()));
+            }
+        }
+    }
+}

+ 17 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -1572,6 +1572,12 @@ public class RequestConvertersTests extends ESTestCase {
         setRandomLocal(request::local, expectedParams);
     }
 
+    static void setRandomTimeout(TimedRequest request, TimeValue defaultTimeout, Map<String, String> expectedParams) {
+        setRandomTimeout(s ->
+                request.setTimeout(TimeValue.parseTimeValue(s, request.getClass().getName() + ".timeout")),
+            defaultTimeout, expectedParams);
+    }
+
     static void setRandomTimeout(Consumer<String> setter, TimeValue defaultTimeout, Map<String, String> expectedParams) {
         if (randomBoolean()) {
             String timeout = randomTimeValue();
@@ -1583,9 +1589,19 @@ public class RequestConvertersTests extends ESTestCase {
     }
 
     static void setRandomMasterTimeout(MasterNodeRequest<?> request, Map<String, String> expectedParams) {
+        setRandomMasterTimeout(request::masterNodeTimeout, expectedParams);
+    }
+
+    static void setRandomMasterTimeout(TimedRequest request, Map<String, String> expectedParams) {
+        setRandomMasterTimeout(s ->
+                request.setMasterTimeout(TimeValue.parseTimeValue(s, request.getClass().getName() + ".masterNodeTimeout")),
+            expectedParams);
+    }
+
+    static void setRandomMasterTimeout(Consumer<String> setter, Map<String, String> 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());

+ 63 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java

@@ -26,6 +26,8 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse;
 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.GetLicenseResponse;
 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 @@ import static org.hamcrest.Matchers.startsWith;
  */
 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 class LicensingDocumentationIT extends ESRestHighLevelClientTestCase {
             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<String, String[]> acknowledgeMessages = response.getAcknowledgeMessages(); // <5>
+            //end::start-basic-response
+        }
+        {
+            StartBasicRequest request = new StartBasicRequest();
+            // tag::start-basic-listener
+            ActionListener<StartBasicResponse> listener = new ActionListener<StartBasicResponse>() {
+                @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));
+        }
+    }
 }

+ 103 - 0
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<String, String[]> 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<String, String[]> 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<String, String[]> randomAckMessages() {
+        int nFeatures = randomIntBetween(1, 5);
+
+        Map<String, String[]> 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;
+    }
+
+}

+ 67 - 0
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

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -214,10 +214,12 @@ The Java High Level REST Client supports the following Licensing APIs:
 * <<java-rest-high-put-license>>
 * <<java-rest-high-get-license>>
 * <<java-rest-high-delete-license>>
+* <<java-rest-high-start-basic>>
 
 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