diff --git a/plugin/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/plugin/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 1ccbe610b59..9edea6222c3 100644 --- a/plugin/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/plugin/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -51,6 +51,9 @@ public class XPackLicenseState { messages.put(XPackPlugin.LOGSTASH, new String[] { "Logstash specific APIs are disabled. You can continue to manage and poll stored configurations" }); + messages.put(XPackPlugin.DEPRECATION, new String[] { + "Deprecation APIs are disabled" + }); EXPIRATION_MESSAGES = Collections.unmodifiableMap(messages); } @@ -432,4 +435,12 @@ public class XPackLicenseState { public boolean isLogstashAllowed() { return status.active; } + + /** + * Deprecation APIs are always allowed as long as there is an active license + * @return {@code true} as long as there is a valid license + */ + public boolean isDeprecationAllowed() { + return status.active; + } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/XPackPlugin.java b/plugin/src/main/java/org/elasticsearch/xpack/XPackPlugin.java index d6bb0c19694..b1c050c60e7 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/XPackPlugin.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/XPackPlugin.java @@ -66,6 +66,7 @@ import org.elasticsearch.xpack.common.http.auth.HttpAuthRegistry; import org.elasticsearch.xpack.common.http.auth.basic.BasicAuth; import org.elasticsearch.xpack.common.http.auth.basic.BasicAuthFactory; import org.elasticsearch.xpack.common.text.TextTemplateEngine; +import org.elasticsearch.xpack.deprecation.Deprecation; import org.elasticsearch.xpack.extensions.XPackExtension; import org.elasticsearch.xpack.extensions.XPackExtensionsService; import org.elasticsearch.xpack.graph.Graph; @@ -148,6 +149,9 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I /** Name constant for the Logstash feature. */ public static final String LOGSTASH = "logstash"; + /** Name constant for the Deprecation API feature. */ + public static final String DEPRECATION = "deprecation"; + // inside of YAML settings we still use xpack do not having handle issues with dashes private static final String SETTINGS_NAME = "xpack"; @@ -198,6 +202,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I protected Graph graph; protected MachineLearning machineLearning; protected Logstash logstash; + protected Deprecation deprecation; public XPackPlugin( final Settings settings, @@ -215,6 +220,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I this.graph = new Graph(settings); this.machineLearning = new MachineLearning(settings, env, licenseState); this.logstash = new Logstash(settings); + this.deprecation = new Deprecation(); // Check if the node is a transport client. if (transportClientMode == false) { this.extensionsService = new XPackExtensionsService(settings, resolveXPackExtensionsFile(env), getExtensions()); @@ -418,6 +424,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I actions.addAll(watcher.getActions()); actions.addAll(graph.getActions()); actions.addAll(machineLearning.getActions()); + actions.addAll(deprecation.getActions()); return actions; } @@ -451,6 +458,8 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I indexNameExpressionResolver, nodesInCluster)); handlers.addAll(machineLearning.getRestHandlers(settings, restController, clusterSettings, indexScopedSettings, settingsFilter, indexNameExpressionResolver, nodesInCluster)); + handlers.addAll(deprecation.getRestHandlers(settings, restController, clusterSettings, indexScopedSettings, settingsFilter, + indexNameExpressionResolver, nodesInCluster)); return handlers; } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java new file mode 100644 index 00000000000..588b0ef8a55 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java @@ -0,0 +1,43 @@ +/* + * 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.xpack.deprecation; + + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * The plugin class for the Deprecation API + */ +public class Deprecation implements ActionPlugin { + @Override + public List> getActions() { + return Collections.singletonList(new ActionHandler<>(DeprecationInfoAction.INSTANCE, DeprecationInfoAction.TransportAction.class)); + } + + @Override + public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster) { + + + return Collections.singletonList(new RestDeprecationInfoAction(settings, restController)); + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java new file mode 100644 index 00000000000..f1dcf25a6b2 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java @@ -0,0 +1,83 @@ +/* + * 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.xpack.deprecation; + +import com.carrotsearch.hppc.cursors.ObjectCursor; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MappingMetaData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Class containing all the cluster, node, and index deprecation checks that will be served + * by the {@link DeprecationInfoAction}. + */ +public class DeprecationChecks { + + private DeprecationChecks() { + } + + static List, ClusterState, DeprecationIssue>> CLUSTER_SETTINGS_CHECKS = + Collections.unmodifiableList(Arrays.asList( + // STUB: TODO(talevy): add checks + )); + + static List, ClusterState, DeprecationIssue>> NODE_SETTINGS_CHECKS = + Collections.unmodifiableList(Arrays.asList( + // STUB: TODO(talevy): add checks + )); + + @SuppressWarnings("unchecked") + static List> INDEX_SETTINGS_CHECKS = + Collections.unmodifiableList(Arrays.asList( + indexMetaData -> { + List issues = new ArrayList<>(); + if (indexMetaData.getCreationVersion().onOrBefore(Version.V_5_6_0)) { + for (ObjectCursor mappingMetaData : indexMetaData.getMappings().values()) { + Map sourceAsMap = mappingMetaData.value.sourceAsMap(); + ((Map) sourceAsMap.getOrDefault("properties", Collections.emptyMap())) + .forEach((key, value) -> { + Map valueMap = ((Map) value); + if ("boolean".equals(valueMap.get("type"))) { + issues.add("type: " + mappingMetaData.value.type() + ", field: " + key); + } + }); + } + } + return new DeprecationIssue(DeprecationIssue.Level.INFO, "Coercion of boolean fields", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + + "breaking_60_mappings_changes.html#_coercion_of_boolean_fields", + Arrays.toString(issues.toArray())); + } + )); + + /** + * helper utility function to reduce repeat of running a specific {@link Set} of checks. + * + * @param checks The functional checks to execute using the mapper function + * @param mapper The function that executes the lambda check with the appropriate arguments + * @param The signature of the check (BiFunction, Function, including the appropriate arguments) + * @return The list of {@link DeprecationIssue} that were found in the cluster + */ + static List filterChecks(List checks, Function mapper) { + return checks.stream().map(mapper).filter(Objects::nonNull).collect(Collectors.toList()); + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java new file mode 100644 index 00000000000..74d95dee98b --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java @@ -0,0 +1,324 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.MasterNodeReadOperationRequestBuilder; +import org.elasticsearch.action.support.master.MasterNodeReadRequest; +import org.elasticsearch.action.support.master.TransportMasterNodeReadAction; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.XPackPlugin; +import org.elasticsearch.xpack.deprecation.DeprecationIssue.Level; +import org.elasticsearch.xpack.security.InternalClient; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.CLUSTER_SETTINGS_CHECKS; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.NODE_SETTINGS_CHECKS; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.filterChecks; + +public class DeprecationInfoAction extends Action { + + public static final DeprecationInfoAction INSTANCE = new DeprecationInfoAction(); + public static final String NAME = "cluster:admin/xpack/deprecation/info"; + + private DeprecationInfoAction() { + super(NAME); + } + + @Override + public RequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new RequestBuilder(client, this); + } + + @Override + public Response newResponse() { + return new Response(); + } + + public static class Response extends ActionResponse implements ToXContentObject { + private List clusterSettingsIssues; + private List nodeSettingsIssues; + private Map> indexSettingsIssues; + + Response() { + } + + public Response(List clusterSettingsIssues, + List nodeSettingsIssues, + Map> indexSettingsIssues) { + this.clusterSettingsIssues = clusterSettingsIssues; + this.nodeSettingsIssues = nodeSettingsIssues; + this.indexSettingsIssues = indexSettingsIssues; + } + + public List getClusterSettingsIssues() { + return clusterSettingsIssues; + } + + public List getNodeSettingsIssues() { + return nodeSettingsIssues; + } + + public Map> getIndexSettingsIssues() { + return indexSettingsIssues; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + clusterSettingsIssues = in.readList(DeprecationIssue::new); + nodeSettingsIssues = in.readList(DeprecationIssue::new); + indexSettingsIssues = in.readMapOfLists(StreamInput::readString, DeprecationIssue::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeList(clusterSettingsIssues); + out.writeList(nodeSettingsIssues); + out.writeMapOfLists(indexSettingsIssues, StreamOutput::writeString, (o, v) -> v.writeTo(o)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .array("cluster_settings", clusterSettingsIssues.toArray()) + .array("node_settings", nodeSettingsIssues.toArray()) + .field("index_settings") + .map(indexSettingsIssues) + .endObject(); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Response response = (Response) o; + return Objects.equals(clusterSettingsIssues, response.clusterSettingsIssues) && + Objects.equals(nodeSettingsIssues, response.nodeSettingsIssues) && + Objects.equals(indexSettingsIssues, response.indexSettingsIssues); + } + + @Override + public int hashCode() { + return Objects.hash(clusterSettingsIssues, nodeSettingsIssues, indexSettingsIssues); + } + + /** + * This is the function that does the bulk of the logic of taking the appropriate ES dependencies + * like {@link NodeInfo}, {@link ClusterState}. Alongside these objects and the list of deprecation checks, + * this function will run through all the checks and build out the final list of issues that exist in the + * cluster. + * + * @param nodesInfo The list of {@link NodeInfo} metadata objects for retrieving node-level information + * @param state The cluster state + * @param indexNameExpressionResolver Used to resolve indices into their concrete names + * @param indices The list of index expressions to evaluate using `indexNameExpressionResolver` + * @param indicesOptions The options to use when resolving and filtering which indices to check + * @param clusterSettingsChecks The list of cluster-level checks + * @param nodeSettingsChecks The list of node-level checks + * @param indexSettingsChecks The list of index-level checks that will be run across all specified + * concrete indices + * @return The list of deprecation issues found in the cluster + */ + static DeprecationInfoAction.Response from(List nodesInfo, ClusterState state, + IndexNameExpressionResolver indexNameExpressionResolver, + String[] indices, IndicesOptions indicesOptions, + List, ClusterState,DeprecationIssue>>clusterSettingsChecks, + List, ClusterState, DeprecationIssue>> nodeSettingsChecks, + List> indexSettingsChecks) { + List clusterSettingsIssues = filterChecks(clusterSettingsChecks, + (c) -> c.apply(nodesInfo, state)); + List nodeSettingsIssues = filterChecks(nodeSettingsChecks, + (c) -> c.apply(nodesInfo, state)); + + String[] concreteIndexNames = indexNameExpressionResolver.concreteIndexNames(state, indicesOptions, indices); + + Map> indexSettingsIssues = new HashMap<>(); + for (String concreteIndex : concreteIndexNames) { + IndexMetaData indexMetaData = state.getMetaData().index(concreteIndex); + List singleIndexIssues = filterChecks(indexSettingsChecks, + c -> c.apply(indexMetaData)); + if (singleIndexIssues.size() > 0) { + indexSettingsIssues.put(concreteIndex, singleIndexIssues); + } + } + + return new DeprecationInfoAction.Response(clusterSettingsIssues, nodeSettingsIssues, indexSettingsIssues); + } + } + + public static class Request extends MasterNodeReadRequest implements IndicesRequest.Replaceable { + + private String[] indices = Strings.EMPTY_ARRAY; + private static final IndicesOptions INDICES_OPTIONS = IndicesOptions.fromOptions(false, true, + true, true); + + public Request() { + } + + public Request(String... indices) { + this.indices = indices; + } + + @Override + public String[] indices() { + return indices; + } + + @Override + public Request indices(String... indices) { + this.indices = indices; + return this; + } + + @Override + public IndicesOptions indicesOptions() { + return INDICES_OPTIONS; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (indices == null) { + validationException = addValidationError("index/indices is missing", validationException); + } + return validationException; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + indices = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(indices); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Arrays.equals(indices, request.indices); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(indices)); + } + + } + + public static class RequestBuilder extends MasterNodeReadOperationRequestBuilder { + + protected RequestBuilder(ElasticsearchClient client, DeprecationInfoAction action) { + super(client, action, new Request()); + } + + public RequestBuilder setIndices(String... indices) { + request.indices(indices); + return this; + } + } + + public static class TransportAction extends TransportMasterNodeReadAction { + + private final XPackLicenseState licenseState; + private final InternalClient client; + private final IndexNameExpressionResolver indexNameExpressionResolver; + + @Inject + public TransportAction(Settings settings, TransportService transportService, ClusterService clusterService, + ThreadPool threadPool, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, + XPackLicenseState licenseState, InternalClient client) { + super(settings, DeprecationInfoAction.NAME, transportService, clusterService, threadPool, actionFilters, + indexNameExpressionResolver, Request::new); + this.licenseState = licenseState; + this.client = client; + this.indexNameExpressionResolver = indexNameExpressionResolver; + } + + @Override + protected String executor() { + return ThreadPool.Names.GENERIC; + } + + @Override + protected Response newResponse() { + return new Response(); + } + + @Override + protected ClusterBlockException checkBlock(Request request, ClusterState state) { + // Cluster is not affected but we look up repositories in metadata + return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ); + } + + @Override + protected final void masterOperation(final Request request, ClusterState state, final ActionListener listener) { + if (licenseState.isDeprecationAllowed()) { + NodesInfoRequest nodesInfoRequest = new NodesInfoRequest("_local").settings(true).plugins(true); + client.admin().cluster().nodesInfo(nodesInfoRequest, ActionListener.wrap(nodesInfoResponse -> { + // if there's a failure, then we failed to work with the + // _local node (guaranteed a single exception) + if (nodesInfoResponse.hasFailures()) { + throw nodesInfoResponse.failures().get(0); + } + + listener.onResponse(Response.from(nodesInfoResponse.getNodes(), state, + indexNameExpressionResolver, request.indices(), request.indicesOptions(), + CLUSTER_SETTINGS_CHECKS, NODE_SETTINGS_CHECKS, INDEX_SETTINGS_CHECKS)); + }, listener::onFailure)); + } else { + listener.onFailure(LicenseUtils.newComplianceException(XPackPlugin.DEPRECATION)); + } + } + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationIssue.java b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationIssue.java new file mode 100644 index 00000000000..125e8bd3d4c --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationIssue.java @@ -0,0 +1,136 @@ +/* + * 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.xpack.deprecation; + + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Locale; +import java.util.Objects; + +/** + * Information about deprecated items + */ +public class DeprecationIssue implements Writeable, ToXContent { + + public enum Level implements Writeable { + NONE, + INFO, + WARNING, + CRITICAL + ; + + public static Level fromString(String value) { + return Level.valueOf(value.toUpperCase(Locale.ROOT)); + } + + public static Level readFromStream(StreamInput in) throws IOException { + int ordinal = in.readVInt(); + if (ordinal < 0 || ordinal >= values().length) { + throw new IOException("Unknown Level ordinal [" + ordinal + "]"); + } + return values()[ordinal]; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(ordinal()); + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + private Level level; + private String message; + private String url; + private String details; + + // pkg-private for tests + DeprecationIssue() { + + } + + public DeprecationIssue(Level level, String message, String url, @Nullable String details) { + this.level = level; + this.message = message; + this.url = url; + this.details = details; + } + + public DeprecationIssue(StreamInput in) throws IOException { + level = Level.readFromStream(in); + message = in.readString(); + url = in.readString(); + details = in.readOptionalString(); + } + + + public Level getLevel() { + return level; + } + + public String getMessage() { + return message; + } + + public String getUrl() { + return url; + } + + public String getDetails() { + return details; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + level.writeTo(out); + out.writeString(message); + out.writeString(url); + out.writeOptionalString(details); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field("level", level) + .field("message", message) + .field("url", url); + if (details != null) { + builder.field("details", details); + } + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeprecationIssue that = (DeprecationIssue) o; + return Objects.equals(level, that.level) && + Objects.equals(message, that.message) && + Objects.equals(url, that.url) && + Objects.equals(details, that.details); + } + + @Override + public int hashCode() { + return Objects.hash(level, message, url, details); + } +} + diff --git a/plugin/src/main/java/org/elasticsearch/xpack/deprecation/RestDeprecationInfoAction.java b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/RestDeprecationInfoAction.java new file mode 100644 index 00000000000..3b8e19c61bf --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/deprecation/RestDeprecationInfoAction.java @@ -0,0 +1,44 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.deprecation.DeprecationInfoAction.Request; + +import java.io.IOException; + +public class RestDeprecationInfoAction extends BaseRestHandler { + public RestDeprecationInfoAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(RestRequest.Method.GET, "/_xpack/migration/deprecations", this); + controller.registerHandler(RestRequest.Method.GET, "/{index}/_xpack/migration/deprecations", this); + } + + @Override + public String getName() { + return "deprecation_info_action"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (request.method().equals(RestRequest.Method.GET)) { + return handleGet(request, client); + } else { + throw new IllegalArgumentException("illegal method [" + request.method() + "] for request [" + request.path() + "]"); + } + } + + private RestChannelConsumer handleGet(final RestRequest request, NodeClient client) { + Request infoRequest = new Request(Strings.splitStringByCommaToArray(request.param("index"))); + return channel -> client.execute(DeprecationInfoAction.INSTANCE, infoRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/transport/KnownActionsTests.java b/plugin/src/test/java/org/elasticsearch/transport/KnownActionsTests.java index 25197ac5b39..f106b895c3e 100644 --- a/plugin/src/test/java/org/elasticsearch/transport/KnownActionsTests.java +++ b/plugin/src/test/java/org/elasticsearch/transport/KnownActionsTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.discovery.TestZenDiscovery; import org.elasticsearch.xpack.XPackPlugin; +import org.elasticsearch.xpack.deprecation.Deprecation; import org.junit.BeforeClass; import java.io.IOException; @@ -137,6 +138,9 @@ public class KnownActionsTests extends SecurityIntegTestCase { // also load stuff from Reindex in org.elasticsearch.index.reindex package loadActions(collectSubClasses(Action.class, ReindexPlugin.class), actions); + // also load stuff from Deprecation in org.elasticsearch.deprecation + loadActions(collectSubClasses(Action.class, Deprecation.class), actions); + return unmodifiableSet(actions); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationChecksTests.java b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationChecksTests.java new file mode 100644 index 00000000000..d6f12c58e9e --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationChecksTests.java @@ -0,0 +1,68 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS; +import static org.hamcrest.core.IsEqual.equalTo; + +public class DeprecationChecksTests extends ESTestCase { + + public void testFilterChecks() throws IOException { + DeprecationIssue issue = DeprecationIssueTests.createTestInstance(); + int numChecksPassed = randomIntBetween(0, 5); + int numChecksFailed = 10 - numChecksPassed; + List> checks = new ArrayList<>(); + for (int i = 0; i < numChecksFailed; i++) { + checks.add(() -> issue); + } + for (int i = 0; i < numChecksPassed; i++) { + checks.add(() -> null); + } + List filteredIssues = DeprecationChecks.filterChecks(checks, Supplier::get); + assertThat(filteredIssues.size(), equalTo(numChecksFailed)); + } + + public void testCoerceBooleanDeprecation() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder(); + mapping.startObject(); { + mapping.startObject("properties"); { + mapping.startObject("my_boolean"); { + mapping.field("type", "boolean"); + } + mapping.endObject(); + } + mapping.endObject(); + } + mapping.endObject(); + + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .putMapping("testBooleanCoercion", mapping.string()) + .settings(settings(Version.V_5_6_0)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.INFO, + "Coercion of boolean fields", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + + "breaking_60_mappings_changes.html#_coercion_of_boolean_fields", + Arrays.toString(new String[] { "type: testBooleanCoercion, field: my_boolean" })); + List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); + assertThat(issues.size(), equalTo(1)); + assertThat(issues.get(0), equalTo(expected)); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionRequestTests.java b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionRequestTests.java new file mode 100644 index 00000000000..2eade4b5b2e --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionRequestTests.java @@ -0,0 +1,21 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.test.AbstractStreamableTestCase; + +public class DeprecationInfoActionRequestTests extends AbstractStreamableTestCase { + + @Override + protected DeprecationInfoAction.Request createTestInstance() { + return new DeprecationInfoAction.Request(randomAlphaOfLength(10)); + } + + @Override + protected DeprecationInfoAction.Request createBlankInstance() { + return new DeprecationInfoAction.Request(); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionResponseTests.java b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionResponseTests.java new file mode 100644 index 00000000000..c27f81a7343 --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationInfoActionResponseTests.java @@ -0,0 +1,123 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.AbstractStreamableTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.core.IsEqual.equalTo; + +public class DeprecationInfoActionResponseTests extends AbstractStreamableTestCase { + + @Override + protected DeprecationInfoAction.Response createTestInstance() { + List clusterIssues = Stream.generate(DeprecationIssueTests::createTestInstance) + .limit(randomIntBetween(0, 10)).collect(Collectors.toList()); + List nodeIssues = Stream.generate(DeprecationIssueTests::createTestInstance) + .limit(randomIntBetween(0, 10)).collect(Collectors.toList()); + Map> indexIssues = new HashMap<>(); + for (int i = 0; i < randomIntBetween(0, 10); i++) { + List perIndexIssues = Stream.generate(DeprecationIssueTests::createTestInstance) + .limit(randomIntBetween(0, 10)).collect(Collectors.toList()); + indexIssues.put(randomAlphaOfLength(10), perIndexIssues); + } + return new DeprecationInfoAction.Response(clusterIssues, nodeIssues, indexIssues); + } + + @Override + protected DeprecationInfoAction.Response createBlankInstance() { + return new DeprecationInfoAction.Response(); + } + + public void testFrom() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("_all"); + mapping.field("enabled", false); + mapping.endObject().endObject(); + + MetaData metadata = MetaData.builder().put(IndexMetaData.builder("test") + .putMapping("testUnderscoreAll", mapping.string()) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0)) + .build(); + + DiscoveryNode discoveryNode = DiscoveryNode.createLocal(Settings.EMPTY, + new TransportAddress(TransportAddress.META_ADDRESS, 9300), "test"); + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metaData(metadata).build(); + List nodeInfos = Collections.singletonList(new NodeInfo(Version.CURRENT, Build.CURRENT, + discoveryNode, null, null, null, null, + null, null, null, null, null, null)); + IndexNameExpressionResolver resolver = new IndexNameExpressionResolver(Settings.EMPTY); + IndicesOptions indicesOptions = IndicesOptions.fromOptions(false, false, + true, true); + boolean clusterIssueFound = randomBoolean(); + boolean nodeIssueFound = randomBoolean(); + boolean indexIssueFound = randomBoolean(); + DeprecationIssue foundIssue = DeprecationIssueTests.createTestInstance(); + List, ClusterState, DeprecationIssue>> clusterSettingsChecks = + Collections.unmodifiableList(Arrays.asList( + (ln, s) -> clusterIssueFound ? foundIssue : null + )); + List, ClusterState, DeprecationIssue>> nodeSettingsChecks = + Collections.unmodifiableList(Arrays.asList( + (ln, s) -> nodeIssueFound ? foundIssue : null + )); + + List> indexSettingsChecks = + Collections.unmodifiableList(Arrays.asList( + (idx) -> indexIssueFound ? foundIssue : null + )); + + DeprecationInfoAction.Response response = DeprecationInfoAction.Response.from(nodeInfos, state, + resolver, Strings.EMPTY_ARRAY, indicesOptions, + clusterSettingsChecks, nodeSettingsChecks, indexSettingsChecks); + + if (clusterIssueFound) { + assertThat(response.getClusterSettingsIssues(), equalTo(Collections.singletonList(foundIssue))); + } else { + assertThat(response.getClusterSettingsIssues(), empty()); + } + + if (nodeIssueFound) { + assertThat(response.getNodeSettingsIssues(), equalTo(Collections.singletonList(foundIssue))); + } else { + assertTrue(response.getNodeSettingsIssues().isEmpty()); + } + + if (indexIssueFound) { + assertThat(response.getIndexSettingsIssues(), equalTo(Collections.singletonMap("test", + Collections.singletonList(foundIssue)))); + } else { + assertTrue(response.getIndexSettingsIssues().isEmpty()); + } + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIssueTests.java b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIssueTests.java new file mode 100644 index 00000000000..d6cc8c50d63 --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIssueTests.java @@ -0,0 +1,66 @@ +/* + * 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.xpack.deprecation; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.ESTestCase; +import org.junit.Before; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xpack.deprecation.DeprecationIssue.Level; +import static org.hamcrest.core.IsEqual.equalTo; + +public class DeprecationIssueTests extends ESTestCase { + DeprecationIssue issue; + + static DeprecationIssue createTestInstance() { + String details = randomBoolean() ? randomAlphaOfLength(10) : null; + return new DeprecationIssue(randomFrom(Level.values()), randomAlphaOfLength(10), + randomAlphaOfLength(10), details); + } + + @Before + public void setup() { + issue = createTestInstance(); + } + + public void testEqualsAndHashCode() { + DeprecationIssue other = new DeprecationIssue(issue.getLevel(), issue.getMessage(), issue.getUrl(), issue.getDetails()); + assertThat(issue, equalTo(other)); + assertThat(other, equalTo(issue)); + assertThat(issue.hashCode(), equalTo(other.hashCode())); + } + + public void testSerialization() throws IOException { + BytesStreamOutput out = new BytesStreamOutput(); + issue.writeTo(out); + StreamInput in = out.bytes().streamInput(); + DeprecationIssue other = new DeprecationIssue(in); + assertThat(issue, equalTo(other)); + } + + public void testToXContent() throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + issue.toXContent(builder, EMPTY_PARAMS); + Map toXContentMap = XContentHelper.convertToMap(builder.bytes(), false, builder.contentType()).v2(); + String level = (String) toXContentMap.get("level"); + String message = (String) toXContentMap.get("message"); + String url = (String) toXContentMap.get("url"); + if (issue.getDetails() != null) { + assertTrue(toXContentMap.containsKey("details")); + } + String details = (String) toXContentMap.get("details"); + DeprecationIssue other = new DeprecationIssue(Level.fromString(level), message, url, details); + assertThat(issue, equalTo(other)); + } +} diff --git a/plugin/src/test/resources/org/elasticsearch/transport/actions b/plugin/src/test/resources/org/elasticsearch/transport/actions index 349bcd150ec..b677df0874c 100644 --- a/plugin/src/test/resources/org/elasticsearch/transport/actions +++ b/plugin/src/test/resources/org/elasticsearch/transport/actions @@ -155,3 +155,4 @@ cluster:admin/reindex/rethrottle indices:data/write/update/byquery indices:data/write/delete/byquery indices:data/write/reindex +cluster:admin/xpack/deprecation/info diff --git a/plugin/src/test/resources/org/elasticsearch/transport/handlers b/plugin/src/test/resources/org/elasticsearch/transport/handlers index 7ffd5460e51..287c1d07972 100644 --- a/plugin/src/test/resources/org/elasticsearch/transport/handlers +++ b/plugin/src/test/resources/org/elasticsearch/transport/handlers @@ -133,3 +133,4 @@ cluster:admin/reindex/rethrottle[n] indices:data/write/update/byquery indices:data/write/delete/byquery indices:data/write/reindex +cluster:admin/xpack/deprecation/info diff --git a/plugin/src/test/resources/rest-api-spec/api/xpack.deprecation.info.json b/plugin/src/test/resources/rest-api-spec/api/xpack.deprecation.info.json new file mode 100644 index 00000000000..7b5e3956a92 --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/api/xpack.deprecation.info.json @@ -0,0 +1,19 @@ +{ + "xpack.deprecation.info": { + "documentation": "http://www.elastic.co/guide/en/migration/current/appendix-api-deprecation-info.html", + "methods": [ "GET" ], + "url": { + "path": "/{index}/_xpack/migration/deprecations", + "paths": ["/_xpack/migration/deprecations", "/{index}/_xpack/migration/deprecations"], + "parts": { + "index": { + "type" : "string", + "description" : "Index pattern" + } + }, + "params": { + } + }, + "body": null + } +} diff --git a/plugin/src/test/resources/rest-api-spec/test/deprecation/10_basic.yml b/plugin/src/test/resources/rest-api-spec/test/deprecation/10_basic.yml new file mode 100644 index 00000000000..705ca9f9692 --- /dev/null +++ b/plugin/src/test/resources/rest-api-spec/test/deprecation/10_basic.yml @@ -0,0 +1,15 @@ +--- +setup: + - do: + cluster.health: + wait_for_status: yellow + +--- +"Test Deprecations": + - do: + xpack.deprecation.info: + index: "*" + - length: { cluster_settings: 0 } + - length: { node_settings: 0 } + - length: { index_settings: 0 } +