Skip to content

add start trial API to HLRC #33406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.StartTrialRequest;
import org.elasticsearch.client.license.StartTrialResponse;
import org.elasticsearch.client.license.StartBasicRequest;
import org.elasticsearch.client.license.StartBasicResponse;
import org.elasticsearch.common.Strings;
Expand All @@ -44,6 +46,7 @@
import java.nio.charset.StandardCharsets;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;

/**
* A wrapper for the {@link RestHighLevelClient} that provides methods for
Expand Down Expand Up @@ -123,6 +126,30 @@ public void deleteLicenseAsync(DeleteLicenseRequest request, RequestOptions opti
AcknowledgedResponse::fromXContent, listener, emptySet());
}

/**
* Starts a trial license on 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 StartTrialResponse startTrial(StartTrialRequest request, RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, LicenseRequestConverters::startTrial, options,
StartTrialResponse::fromXContent, singleton(403));
}

/**
* Asynchronously starts a trial license on the 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 startTrialAsync(StartTrialRequest request,
RequestOptions options,
ActionListener<StartTrialResponse> listener) {

restHighLevelClient.performRequestAsyncAndParseEntity(request, LicenseRequestConverters::startTrial, options,
StartTrialResponse::fromXContent, listener, singleton(403));
}

/**
* Initiates an indefinite basic license.
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@
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.StartTrialRequest;
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;

public class LicenseRequestConverters {
static Request putLicense(PutLicenseRequest putLicenseRequest) {
String endpoint = new RequestConverters.EndpointBuilder()
.addPathPartAsIs("_xpack")
.addPathPartAsIs("license")
.build();
String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great now, ty for cleaning these up as well.

Request request = new Request(HttpPut.METHOD_NAME, endpoint);
RequestConverters.Params parameters = new RequestConverters.Params(request);
parameters.withTimeout(putLicenseRequest.timeout());
Expand All @@ -46,24 +44,34 @@ static Request putLicense(PutLicenseRequest putLicenseRequest) {
}

static Request getLicense(GetLicenseRequest getLicenseRequest) {
String endpoint = new RequestConverters.EndpointBuilder()
.addPathPartAsIs("_xpack")
.addPathPartAsIs("license")
.build();
String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
Request request = new Request(HttpGet.METHOD_NAME, endpoint);
RequestConverters.Params parameters = new RequestConverters.Params(request);
parameters.withLocal(getLicenseRequest.local());
return request;
}

static Request deleteLicense(DeleteLicenseRequest deleteLicenseRequest) {
Request request = new Request(HttpDelete.METHOD_NAME, "/_xpack/license");
String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
RequestConverters.Params parameters = new RequestConverters.Params(request);
parameters.withTimeout(deleteLicenseRequest.timeout());
parameters.withMasterTimeout(deleteLicenseRequest.masterNodeTimeout());
return request;
}

static Request startTrial(StartTrialRequest startTrialRequest) {
final String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license", "start_trial").build();
final Request request = new Request(HttpPost.METHOD_NAME, endpoint);

RequestConverters.Params parameters = new RequestConverters.Params(request);
parameters.putParam("acknowledge", Boolean.toString(startTrialRequest.isAcknowledge()));
if (startTrialRequest.getLicenseType() != null) {
parameters.putParam("type", startTrialRequest.getLicenseType());
}
return request;
}

static Request startBasic(StartBasicRequest startBasicRequest) {
String endpoint = new RequestConverters.EndpointBuilder()
.addPathPartAsIs("_xpack", "license", "start_basic")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ EndpointBuilder addCommaSeparatedPathParts(String[] parts) {
return this;
}

EndpointBuilder addPathPartAsIs(String ... parts) {
EndpointBuilder addPathPartAsIs(String... parts) {
for (String part : parts) {
if (Strings.hasLength(part)) {
joiner.add(part);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.Validatable;
import org.elasticsearch.common.Nullable;

public class StartTrialRequest implements Validatable {

private final boolean acknowledge;
private final String licenseType;

public StartTrialRequest() {
this(false);
}

public StartTrialRequest(boolean acknowledge) {
this(acknowledge, null);
}

public StartTrialRequest(boolean acknowledge, @Nullable String licenseType) {
this.acknowledge = acknowledge;
this.licenseType = licenseType;
}

public boolean isAcknowledge() {
return acknowledge;
}

public String getLicenseType() {
return licenseType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;

public class StartTrialResponse {

private static final ConstructingObjectParser<StartTrialResponse, Void> PARSER = new ConstructingObjectParser<>(
"start_trial_response",
true,
(Object[] arguments, Void aVoid) -> {
final boolean acknowledged = (boolean) arguments[0];
final boolean trialWasStarted = (boolean) arguments[1];
final String licenseType = (String) arguments[2];
final String errorMessage = (String) arguments[3];

@SuppressWarnings("unchecked")
final Tuple<String, Map<String, String[]>> acknowledgeDetails = (Tuple<String, Map<String, String[]>>) arguments[4];
final String acknowledgeHeader;
final Map<String, String[]> acknowledgeMessages;

if (acknowledgeDetails != null) {
acknowledgeHeader = acknowledgeDetails.v1();
acknowledgeMessages = acknowledgeDetails.v2();
} else {
acknowledgeHeader = null;
acknowledgeMessages = null;
}

return new StartTrialResponse(acknowledged, trialWasStarted, licenseType, errorMessage, acknowledgeHeader,
acknowledgeMessages);
}
);

static {
PARSER.declareBoolean(constructorArg(), new ParseField("acknowledged"));
PARSER.declareBoolean(constructorArg(), new ParseField("trial_was_started"));
PARSER.declareString(optionalConstructorArg(), new ParseField("type"));
PARSER.declareString(optionalConstructorArg(), new ParseField("error_message"));
// todo consolidate this parsing with the parsing in PutLicenseResponse
PARSER.declareObject(optionalConstructorArg(), (parser, aVoid) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know a ton about this data structure here, but is it possible to use the parser's built in .map functions to return a Map<String, Object> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, I'm not super knowledgeable about the parser facility. I'll give it a shot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like XContentFactory#map works - somehow that version seems even uglier than the old school parsing code though, let me know if that's not what you meant.

It's a little tricky because the object looks like this, where the header ("message") is parsed as part of the map but is its own particular field that we pull out

{
  "acknowledge": {
    "message": "you need to acknowledge blah blah",
    "security": [ "security won't be enabled blah blah" ],
    "watcher": [ "another acknowledge message "],
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HAH, ill have a look.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that its a bit nicer, given that you cant actually parse something incorrectly. Sure its manipulating a bunch of k/v stuff in that map, but the actual parsing logic is way trappier and more subject to fail if slightly messed up. mucking with a map of list of lists is pretty low from a trappy perspective.

final Map<String, String[]> acknowledgeMessages = new HashMap<>();
String message = null;

final Map<String, Object> parsedMap = parser.map();
for (Map.Entry<String, Object> entry : parsedMap.entrySet()) {
if (entry.getKey().equals("message")) {
if (entry.getValue() instanceof String) {
message = (String) entry.getValue();
} else {
throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement header type");
}
} else {
if (entry.getValue() instanceof List) {
final List<String> messageStrings = new ArrayList<>();
@SuppressWarnings("unchecked")
final List<Object> messageObjects = (List<Object>) entry.getValue();
for (Object messageObject : messageObjects) {
if (messageObject instanceof String) {
messageStrings.add((String) messageObject);
} else {
throw new XContentParseException(parser.getTokenLocation(), "expected text in acknowledgement message");
}
}

acknowledgeMessages.put(entry.getKey(), messageStrings.toArray(new String[messageStrings.size()]));
} else {
throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement message type");
}
}
}

if (message == null) {
throw new XContentParseException(parser.getTokenLocation(), "expected acknowledgement header");
}

return new Tuple<>(message, acknowledgeMessages);

}, new ParseField("acknowledge"));
}

public static StartTrialResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.apply(parser, null);
}

private final boolean acknowledged;
private final boolean trialWasStarted;
private final String licenseType;
private final String errorMessage;
private final String acknowledgeHeader;
private final Map<String, String[]> acknowledgeMessages;

public StartTrialResponse(boolean acknowledged,
boolean trialWasStarted,
String licenseType,
String errorMessage,
String acknowledgeHeader,
Map<String, String[]> acknowledgeMessages) {

this.acknowledged = acknowledged;
this.trialWasStarted = trialWasStarted;
this.licenseType = licenseType;
this.errorMessage = errorMessage;
this.acknowledgeHeader = acknowledgeHeader;
this.acknowledgeMessages = acknowledgeMessages;
}

/**
* Returns true if the request that corresponds to this response acknowledged license changes that would occur as a result of starting
* a trial license
*/
public boolean isAcknowledged() {
return acknowledged;
}

/**
* Returns true if a trial license was started as a result of the request corresponding to this response. Returns false if the cluster
* did not start a trial, or a trial had already been started before the corresponding request was made
*/
public boolean isTrialWasStarted() {
return trialWasStarted;
}

/**
* If a trial license was started as a result of the request corresponding to this response (see {@link #isTrialWasStarted()}) then
* returns the type of license that was started on the cluster. Returns null otherwise
*/
public String getLicenseType() {
return licenseType;
}

/**
* If a trial license was not started as a result of the request corresponding to this response (see {@link #isTrialWasStarted()} then
* returns a brief message explaining why the trial could not be started. Returns false otherwise
*/
public String getErrorMessage() {
return errorMessage;
}

/**
* If the request corresponding to this response did not acknowledge licensing changes that would result from starting a trial license
* (see {@link #isAcknowledged()}), returns a message describing how the user must acknowledge licensing changes as a result of
* such a request. Returns null otherwise
*/
public String getAcknowledgeHeader() {
return acknowledgeHeader;
}

/**
* If the request corresponding to this response did not acknowledge licensing changes that would result from starting a trial license
* (see {@link #isAcknowledged()}, returns a map. The map's keys are names of commercial Elasticsearch features, and their values are
* messages about how those features will be affected by licensing changes as a result of starting a trial license
*/
public Map<String, String[]> getAcknowledgeMessages() {
return acknowledgeMessages;
}
}
Loading