Rest HL client: Add put license action (#32214)

In the HL REST client we replace the License object with a string, because of 
complexity of this class. It is also not really needed on the client side since 
end-users are not interacting with the license besides passing it as a string 
to the server.

Relates #29827
This commit is contained in:
Igor Motov 2018-07-24 11:19:07 -07:00 committed by GitHub
parent c1cc0cef61
commit 29c802f88e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 889 additions and 258 deletions

View File

@ -0,0 +1,66 @@
/*
* 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.action.ActionListener;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.io.IOException;
import static java.util.Collections.emptySet;
/**
* A wrapper for the {@link RestHighLevelClient} that provides methods for
* accessing the Elastic License-related methods
* <p>
* See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/licensing-apis.html">
* X-Pack Licensing APIs on elastic.co</a> for more information.
*/
public class LicenseClient {
private final RestHighLevelClient restHighLevelClient;
LicenseClient(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
}
/**
* Updates license for the cluster.
* @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 PutLicenseResponse putLicense(PutLicenseRequest request, RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, RequestConverters::putLicense, options,
PutLicenseResponse::fromXContent, emptySet());
}
/**
* Asynchronously updates license for the cluster cluster.
* @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 putLicenseAsync(PutLicenseRequest request, RequestOptions options, ActionListener<PutLicenseResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::putLicense, options,
PutLicenseResponse::fromXContent, listener, emptySet());
}
}

View File

@ -109,6 +109,7 @@ import org.elasticsearch.index.rankeval.RankEvalRequest;
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
import org.elasticsearch.protocol.xpack.XPackUsageRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.rest.action.search.RestSearchAction;
import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
import org.elasticsearch.script.mustache.SearchTemplateRequest;
@ -1139,6 +1140,18 @@ final class RequestConverters {
return request;
}
static Request putLicense(PutLicenseRequest putLicenseRequest) {
Request request = new Request(HttpPut.METHOD_NAME, "/_xpack/license");
Params parameters = new Params(request);
parameters.withTimeout(putLicenseRequest.timeout());
parameters.withMasterTimeout(putLicenseRequest.masterNodeTimeout());
if (putLicenseRequest.isAcknowledge()) {
parameters.putParam("acknowledge", "true");
}
request.setJsonEntity(putLicenseRequest.getLicenseDefinition());
return request;
}
private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException {
BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef();
return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType));

View File

@ -42,10 +42,12 @@ public final class XPackClient {
private final RestHighLevelClient restHighLevelClient;
private final WatcherClient watcherClient;
private final LicenseClient licenseClient;
XPackClient(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
this.watcherClient = new WatcherClient(restHighLevelClient);
this.licenseClient = new LicenseClient(restHighLevelClient);
}
public WatcherClient watcher() {
@ -100,4 +102,15 @@ public final class XPackClient {
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::xpackUsage, options,
XPackUsageResponse::fromXContent, listener, emptySet());
}
/**
* A wrapper for the {@link RestHighLevelClient} that provides methods for
* accessing the Elastic Licensing APIs.
* <p>
* See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/licensing-apis.html">
* X-Pack APIs on elastic.co</a> for more information.
*/
public LicenseClient license() {
return licenseClient;
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.documentation;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
/**
* Documentation for Licensing APIs in the high level java client.
* Code wrapped in {@code tag} and {@code end} tags is included in the docs.
*/
public class LicensingDocumentationIT extends ESRestHighLevelClientTestCase {
public void testPutLicense() throws Exception {
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\"," +
"\"issuer\":\"issuer\",\"signature\":\"AAAAAgAAAA3U8+YmnvwC+CWsV/mRAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSm" +
"kxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0lDalZCS095MXRGN1lIZlpYcVVTTn" +
"FrcTE2dzhJZmZrdFQrN3JQeGwxb0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVBxRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1" +
"JOUml4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG53MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lxOHN5bUErZlNIWkZSVm" +
"ZIWEtaSU9wTTJENDVvT1NCYklacUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZzlvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQ" +
"Be8GfzDm6T537Iuuvjetb3xK5dvg0K5NQapv+rczWcQFxgCuzbF8plkgetP1aAGZP4uRESDQPMlOCsx4d0UqqAm9f7GbBQ3l93P+PogInPFeEH9NvOmaAQovmxVM" +
"9SE6DsDqlX4cXSO+bgWpXPTd2LmpoQc1fXd6BZ8GeuyYpVHVKp9hVU0tAYjw6HzYOE7+zuO1oJYOxElqy66AnIfkvHrvni+flym3tE7tDTgsDRaz7W3iBhaqiSnt" +
"EqabEkvHdPHQdSR99XGaEvnHO1paK01/35iZF6OXHsF7CCj+558GRXiVxzueOe7TsGSSt8g7YjZwV9bRCyU7oB4B/nidgI\"}}";
{
//tag::put-license-execute
PutLicenseRequest request = new PutLicenseRequest();
request.setLicenseDefinition(license); // <1>
request.setAcknowledge(false); // <2>
PutLicenseResponse response = client.xpack().license().putLicense(request, RequestOptions.DEFAULT);
//end::put-license-execute
//tag::put-license-response
LicensesStatus status = response.status(); // <1>
assertEquals(status, LicensesStatus.VALID); // <2>
boolean acknowledged = response.isAcknowledged(); // <3>
String acknowledgeHeader = response.acknowledgeHeader(); // <4>
Map<String, String[]> acknowledgeMessages = response.acknowledgeMessages(); // <5>
//end::put-license-response
assertFalse(acknowledged); // Should fail because we are trying to downgrade from platinum trial to gold
assertThat(acknowledgeHeader, startsWith("This license update requires acknowledgement."));
assertThat(acknowledgeMessages.keySet(), not(hasSize(0)));
}
{
PutLicenseRequest request = new PutLicenseRequest();
// tag::put-license-execute-listener
ActionListener<PutLicenseResponse> listener = new ActionListener<PutLicenseResponse>() {
@Override
public void onResponse(PutLicenseResponse indexResponse) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}
};
// end::put-license-execute-listener
// Replace the empty listener by a blocking listener in test
final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);
// tag::put-license-execute-async
client.xpack().license().putLicenseAsync(
request, RequestOptions.DEFAULT, listener); // <1>
// end::put-license-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
}

View File

@ -0,0 +1,66 @@
[[java-rest-high-put-license]]
=== Update License
[[java-rest-high-put-license-execution]]
==== Execution
The license can be added or updated using the `putLicense()` method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute]
--------------------------------------------------
<1> Set the categories of information to retrieve. The the default is to
return no information which is useful for checking if {xpack} is installed
but not much else.
<2> A JSON document containing the license information.
[[java-rest-high-put-license-response]]
==== Response
The returned `PutLicenseResponse` contains the `LicensesStatus`,
`acknowledged` flag and possible acknowledge messages. The acknowledge messages
are present if you previously had a license with more features than one you
are trying to update and you didn't set the `acknowledge` flag to `true`. In this case
you need to display the messages to the end user and if they agree, resubmit the
license with the `acknowledge` flag set to `true`. Please note that the request will
still return a 200 return code even if requires an acknowledgement. So, it is
necessary to check the `acknowledged` flag.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-response]
--------------------------------------------------
<1> The status of the license
<2> Make sure that the license is valid.
<3> Check the acknowledge flag.
<4> It should be true if license is acknowledge.
<5> Otherwise we can see the acknowledge messages in `acknowledgeHeader()`
<6> and check component-specific messages in `acknowledgeMessages()`.
[[java-rest-high-put-license-async]]
==== Asynchronous Execution
This request can be executed asynchronously:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute-async]
--------------------------------------------------
<1> The `PutLicenseRequest` 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 `PutLicenseResponse` looks like:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute-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

View File

@ -186,3 +186,12 @@ The Java High Level REST Client supports the following Scripts APIs:
include::script/get_script.asciidoc[]
include::script/delete_script.asciidoc[]
== Licensing APIs
The Java High Level REST Client supports the following Licensing APIs:
* <<java-rest-high-put-license>>
include::licensing/put-license.asciidoc[]

View File

@ -28,6 +28,8 @@ import org.elasticsearch.discovery.DiscoveryModule;
import org.elasticsearch.env.Environment;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;

View File

@ -1,34 +0,0 @@
/*
* 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;
public enum LicensesStatus {
VALID((byte) 0),
INVALID((byte) 1),
EXPIRED((byte) 2);
private final byte id;
LicensesStatus(byte id) {
this.id = id;
}
public int id() {
return id;
}
public static LicensesStatus fromId(int id) {
if (id == 0) {
return VALID;
} else if (id == 1) {
return INVALID;
} else if (id == 2) {
return EXPIRED;
} else {
throw new IllegalStateException("no valid LicensesStatus for id=" + id);
}
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.license;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class LicensingClient {

View File

@ -6,6 +6,7 @@
package org.elasticsearch.license;
import org.elasticsearch.action.Action;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class PutLicenseAction extends Action<PutLicenseResponse> {

View File

@ -9,6 +9,7 @@ import org.elasticsearch.action.support.master.AcknowledgedRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
/**
* Register license request builder

View File

@ -1,119 +0,0 @@
/*
* 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.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class PutLicenseResponse extends AcknowledgedResponse {
private LicensesStatus status;
private Map<String, String[]> acknowledgeMessages;
private String acknowledgeHeader;
PutLicenseResponse() {
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status) {
this(acknowledged, status, null, Collections.<String, String[]>emptyMap());
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader,
Map<String, String[]> acknowledgeMessages) {
super(acknowledged);
this.status = status;
this.acknowledgeHeader = acknowledgeHeader;
this.acknowledgeMessages = acknowledgeMessages;
}
public LicensesStatus status() {
return status;
}
public Map<String, String[]> acknowledgeMessages() {
return acknowledgeMessages;
}
public String acknowledgeHeader() {
return acknowledgeHeader;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
status = LicensesStatus.fromId(in.readVInt());
acknowledgeHeader = in.readOptionalString();
int size = in.readVInt();
Map<String, String[]> acknowledgeMessages = new HashMap<>(size);
for (int i = 0; i < size; i++) {
String feature = in.readString();
int nMessages = in.readVInt();
String[] messages = new String[nMessages];
for (int j = 0; j < nMessages; j++) {
messages[j] = in.readString();
}
acknowledgeMessages.put(feature, messages);
}
this.acknowledgeMessages = acknowledgeMessages;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(status.id());
out.writeOptionalString(acknowledgeHeader);
out.writeVInt(acknowledgeMessages.size());
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
out.writeString(entry.getKey());
out.writeVInt(entry.getValue().length);
for (String message : entry.getValue()) {
out.writeString(message);
}
}
}
@Override
protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
switch (status) {
case VALID:
builder.field("license_status", "valid");
break;
case INVALID:
builder.field("license_status", "invalid");
break;
case EXPIRED:
builder.field("license_status", "expired");
break;
default:
throw new IllegalArgumentException("unknown status [" + status + "] found");
}
if (!acknowledgeMessages.isEmpty()) {
builder.startObject("acknowledge");
builder.field("message", acknowledgeHeader);
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
builder.startArray(entry.getKey());
for (String message : entry.getValue()) {
builder.value(message);
}
builder.endArray();
}
builder.endObject();
}
}
@Override
public String toString() {
return Strings.toString(this, true, true);
}
}

View File

@ -16,6 +16,7 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

View File

@ -12,6 +12,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.net.InetAddress;

View File

@ -9,6 +9,8 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import static org.elasticsearch.common.unit.TimeValue.timeValueHours;
import static org.hamcrest.Matchers.equalTo;

View File

@ -11,6 +11,7 @@ import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@ -145,4 +146,4 @@ public class LicensesManagerServiceTests extends ESSingleNodeTestCase {
}
assertThat("remove license(s) failed", success.get(), equalTo(true));
}
}
}

View File

@ -12,6 +12,8 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.node.Node;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@ -230,4 +232,4 @@ public class LicensesTransportTests extends ESSingleNodeTestCase {
assertThat(putLicenseResponse.isAcknowledged(), equalTo(true));
assertThat(putLicenseResponse.status(), equalTo(LicensesStatus.VALID));
}
}
}

View File

@ -1,103 +0,0 @@
/*
* 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.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
public class PutLicenseResponseTests extends ESTestCase {
@SuppressWarnings("unchecked")
public void testSerialization() throws Exception {
boolean acknowledged = randomBoolean();
LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
Map<String, String[]> ackMessages = randomAckMessages();
PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
XContentBuilder contentBuilder = XContentFactory.jsonBuilder();
response.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS);
Map<String, Object> map = XContentHelper.convertToMap(BytesReference.bytes(contentBuilder), false,
contentBuilder.contentType()).v2();
assertThat(map.containsKey("acknowledged"), equalTo(true));
boolean actualAcknowledged = (boolean) map.get("acknowledged");
assertThat(actualAcknowledged, equalTo(acknowledged));
assertThat(map.containsKey("license_status"), equalTo(true));
String actualStatus = (String) map.get("license_status");
assertThat(actualStatus, equalTo(status.name().toLowerCase(Locale.ROOT)));
assertThat(map.containsKey("acknowledge"), equalTo(true));
Map<String, List<String>> actualAckMessages = (Map<String, List<String>>) map.get("acknowledge");
assertTrue(actualAckMessages.containsKey("message"));
actualAckMessages.remove("message");
assertThat(actualAckMessages.keySet(), equalTo(ackMessages.keySet()));
for (Map.Entry<String, List<String>> entry : actualAckMessages.entrySet()) {
assertArrayEquals(entry.getValue().toArray(), ackMessages.get(entry.getKey()));
}
}
public void testStreamSerialization() throws IOException {
boolean acknowledged = randomBoolean();
LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
Map<String, String[]> ackMessages = randomAckMessages();
// write the steam so that we can attempt to read it back
BytesStreamOutput output = new BytesStreamOutput();
PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
// write it out
response.writeTo(output);
StreamInput input = output.bytes().streamInput();
// read it back in
response.readFrom(input);
assertThat(response.isAcknowledged(), equalTo(acknowledged));
assertThat(response.status(), equalTo(status));
assertThat(response.acknowledgeMessages(), not(sameInstance(ackMessages)));
assertThat(response.acknowledgeMessages().size(), equalTo(ackMessages.size()));
for (String key : ackMessages.keySet()) {
assertArrayEquals(ackMessages.get(key), response.acknowledgeMessages().get(key));
}
}
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;
}
}

View File

@ -20,6 +20,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.license.licensor.LicenseSigner;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.hamcrest.MatcherAssert;
import org.joda.time.format.DateTimeFormatter;
import org.junit.Assert;

View File

@ -0,0 +1,72 @@
/*
* 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.protocol.xpack.common;
import java.util.Arrays;
import java.util.Map;
/**
* Common utilities used for XPack protocol classes
*/
public final class ProtocolUtils {
/**
* Implements equals for a map of string arrays
*
* The map of string arrays is used in some XPack protocol classes but does't work with equal.
*/
public static boolean equals(Map<String, String[]> a, Map<String, String[]> b) {
if (a == null) {
return b == null;
}
if (b == null) {
return false;
}
if (a.size() != b.size()) {
return false;
}
for (Map.Entry<String, String[]> entry : a.entrySet()) {
String[] val = entry.getValue();
String key = entry.getKey();
if (val == null) {
if (b.get(key) != null || b.containsKey(key) == false) {
return false;
}
} else {
if (Arrays.equals(val, b.get(key)) == false) {
return false;
}
}
}
return true;
}
/**
* Implements hashCode for map of string arrays
*
* The map of string arrays does't work with hashCode.
*/
public static int hashCode(Map<String, String[]> a) {
int hash = 0;
for (Map.Entry<String, String[]> entry : a.entrySet())
hash += Arrays.hashCode(entry.getValue());
return hash;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.protocol.xpack.license;
import java.util.Locale;
public enum LicensesStatus {
VALID((byte) 0),
INVALID((byte) 1),
EXPIRED((byte) 2);
private final byte id;
LicensesStatus(byte id) {
this.id = id;
}
public int id() {
return id;
}
public static LicensesStatus fromId(int id) {
if (id == 0) {
return VALID;
} else if (id == 1) {
return INVALID;
} else if (id == 2) {
return EXPIRED;
} else {
throw new IllegalStateException("no valid LicensesStatus for id=" + id);
}
}
@Override
public String toString() {
return this.name().toLowerCase(Locale.ROOT);
}
public static LicensesStatus fromString(String value) {
switch (value) {
case "valid":
return VALID;
case "invalid":
return INVALID;
case "expired":
return EXPIRED;
default:
throw new IllegalArgumentException("unknown licenses status [" + value + "]");
}
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.protocol.xpack.license;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.master.AcknowledgedRequest;
public class PutLicenseRequest extends AcknowledgedRequest<PutLicenseRequest> {
private String licenseDefinition;
private boolean acknowledge = false;
public PutLicenseRequest() {
}
@Override
public ActionRequestValidationException validate() {
return null;
}
public void setLicenseDefinition(String licenseDefinition) {
this.licenseDefinition = licenseDefinition;
}
public String getLicenseDefinition() {
return licenseDefinition;
}
public void setAcknowledge(boolean acknowledge) {
this.acknowledge = acknowledge;
}
public boolean isAcknowledge() {
return acknowledge;
}
}

View File

@ -0,0 +1,209 @@
/*
* 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.protocol.xpack.license;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParseException;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.protocol.xpack.common.ProtocolUtils;
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;
public class PutLicenseResponse extends AcknowledgedResponse {
private static final ConstructingObjectParser<PutLicenseResponse, Void> PARSER = new ConstructingObjectParser<>(
"put_license_response", true, (a, v) -> {
boolean acknowledged = (Boolean) a[0];
LicensesStatus licensesStatus = LicensesStatus.fromString((String) a[1]);
@SuppressWarnings("unchecked") Tuple<String, Map<String, String[]>> acknowledgements = (Tuple<String, Map<String, String[]>>) a[2];
if (acknowledgements == null) {
return new PutLicenseResponse(acknowledged, licensesStatus);
} else {
return new PutLicenseResponse(acknowledged, licensesStatus, acknowledgements.v1(), acknowledgements.v2());
}
});
static {
PARSER.declareBoolean(constructorArg(), new ParseField("acknowledged"));
PARSER.declareString(constructorArg(), new ParseField("license_status"));
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 ("message".equals(currentFieldName)) {
if (token != XContentParser.Token.VALUE_STRING) {
throw new XContentParseException(parser.getTokenLocation(), "unexpected message header type");
}
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) {
if (token != XContentParser.Token.VALUE_STRING) {
throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement text");
}
acknowledgeMessagesList.add(parser.text());
}
acknowledgeMessages.put(currentFieldName, acknowledgeMessagesList.toArray(new String[0]));
}
}
}
return new Tuple<>(message, acknowledgeMessages);
},
new ParseField("acknowledge"));
}
private LicensesStatus status;
private Map<String, String[]> acknowledgeMessages;
private String acknowledgeHeader;
public PutLicenseResponse() {
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status) {
this(acknowledged, status, null, Collections.<String, String[]>emptyMap());
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader,
Map<String, String[]> acknowledgeMessages) {
super(acknowledged);
this.status = status;
this.acknowledgeHeader = acknowledgeHeader;
this.acknowledgeMessages = acknowledgeMessages;
}
public LicensesStatus status() {
return status;
}
public Map<String, String[]> acknowledgeMessages() {
return acknowledgeMessages;
}
public String acknowledgeHeader() {
return acknowledgeHeader;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
status = LicensesStatus.fromId(in.readVInt());
acknowledgeHeader = in.readOptionalString();
int size = in.readVInt();
Map<String, String[]> acknowledgeMessages = new HashMap<>(size);
for (int i = 0; i < size; i++) {
String feature = in.readString();
int nMessages = in.readVInt();
String[] messages = new String[nMessages];
for (int j = 0; j < nMessages; j++) {
messages[j] = in.readString();
}
acknowledgeMessages.put(feature, messages);
}
this.acknowledgeMessages = acknowledgeMessages;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(status.id());
out.writeOptionalString(acknowledgeHeader);
out.writeVInt(acknowledgeMessages.size());
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
out.writeString(entry.getKey());
out.writeVInt(entry.getValue().length);
for (String message : entry.getValue()) {
out.writeString(message);
}
}
}
@Override
protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
builder.field("license_status", status.toString());
if (!acknowledgeMessages.isEmpty()) {
builder.startObject("acknowledge");
builder.field("message", acknowledgeHeader);
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
builder.startArray(entry.getKey());
for (String message : entry.getValue()) {
builder.value(message);
}
builder.endArray();
}
builder.endObject();
}
}
@Override
public String toString() {
return Strings.toString(this, true, true);
}
public static PutLicenseResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
PutLicenseResponse that = (PutLicenseResponse) o;
return status == that.status &&
ProtocolUtils.equals(acknowledgeMessages, that.acknowledgeMessages) &&
Objects.equals(acknowledgeHeader, that.acknowledgeHeader);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), status, ProtocolUtils.hashCode(acknowledgeMessages), acknowledgeHeader);
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.protocol.xpack.common;
import org.elasticsearch.test.ESTestCase;
import java.util.HashMap;
import java.util.Map;
public class ProtocolUtilsTests extends ESTestCase {
public void testMapStringEqualsAndHash() {
assertTrue(ProtocolUtils.equals(null, null));
assertFalse(ProtocolUtils.equals(null, new HashMap<>()));
assertFalse(ProtocolUtils.equals(new HashMap<>(), null));
Map<String, String[]> a = new HashMap<>();
a.put("foo", new String[] { "a", "b" });
a.put("bar", new String[] { "b", "c" });
Map<String, String[]> b = new HashMap<>();
b.put("foo", new String[] { "a", "b" });
assertFalse(ProtocolUtils.equals(a, b));
assertFalse(ProtocolUtils.equals(b, a));
b.put("bar", new String[] { "c", "b" });
assertFalse(ProtocolUtils.equals(a, b));
assertFalse(ProtocolUtils.equals(b, a));
b.put("bar", new String[] { "b", "c" });
assertTrue(ProtocolUtils.equals(a, b));
assertTrue(ProtocolUtils.equals(b, a));
assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b));
b.put("baz", new String[] { "b", "c" });
assertFalse(ProtocolUtils.equals(a, b));
assertFalse(ProtocolUtils.equals(b, a));
a.put("non", null);
assertFalse(ProtocolUtils.equals(a, b));
assertFalse(ProtocolUtils.equals(b, a));
b.put("non", null);
b.remove("baz");
assertTrue(ProtocolUtils.equals(a, b));
assertTrue(ProtocolUtils.equals(b, a));
assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b));
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.protocol.xpack.license;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
public class PutLicenseResponseTests extends AbstractStreamableXContentTestCase<PutLicenseResponse> {
@Override
protected boolean supportsUnknownFields() {
return true;
}
@Override
protected Predicate<String> getRandomFieldsExcludeFilter() {
// The structure of the response is such that unknown fields inside acknowledge cannot be supported since they
// are treated as messages from new services
return p -> p.startsWith("acknowledge");
}
@Override
protected PutLicenseResponse createTestInstance() {
boolean acknowledged = randomBoolean();
LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
String messageHeader;
Map<String, String[]> ackMessages;
if (randomBoolean()) {
messageHeader = randomAlphaOfLength(10);
ackMessages = randomAckMessages();
} else {
messageHeader = null;
ackMessages = Collections.emptyMap();
}
return new PutLicenseResponse(acknowledged, status, messageHeader, ackMessages);
}
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;
}
@Override
protected PutLicenseResponse doParseInstance(XContentParser parser) throws IOException {
return PutLicenseResponse.fromXContent(parser);
}
@Override
protected PutLicenseResponse createBlankInstance() {
return new PutLicenseResponse();
}
@Override
protected PutLicenseResponse mutateInstance(PutLicenseResponse response) {
@SuppressWarnings("unchecked")
Function<PutLicenseResponse, PutLicenseResponse> mutator = randomFrom(
r -> new PutLicenseResponse(
r.isAcknowledged() == false,
r.status(),
r.acknowledgeHeader(),
r.acknowledgeMessages()),
r -> new PutLicenseResponse(
r.isAcknowledged(),
mutateStatus(r.status()),
r.acknowledgeHeader(),
r.acknowledgeMessages()),
r -> {
if (r.acknowledgeMessages().isEmpty()) {
return new PutLicenseResponse(
r.isAcknowledged(),
r.status(),
randomAlphaOfLength(10),
randomAckMessages()
);
} else {
return new PutLicenseResponse(r.isAcknowledged(), r.status());
}
}
);
return mutator.apply(response);
}
private LicensesStatus mutateStatus(LicensesStatus status) {
return randomValueOtherThan(status, () -> randomFrom(LicensesStatus.values()));
}
}