diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java index a0b83cb36ee..9cf327c08aa 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java @@ -55,9 +55,9 @@ import org.elasticsearch.client.indices.GetFieldMappingsRequest; import org.elasticsearch.client.indices.GetFieldMappingsResponse; import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.client.indices.GetIndexResponse; +import org.elasticsearch.client.indices.GetIndexTemplateV2Request; import org.elasticsearch.client.indices.GetIndexTemplatesRequest; import org.elasticsearch.client.indices.GetIndexTemplatesResponse; -import org.elasticsearch.client.indices.GetIndexTemplateV2Request; import org.elasticsearch.client.indices.GetIndexTemplatesV2Response; import org.elasticsearch.client.indices.GetMappingsRequest; import org.elasticsearch.client.indices.GetMappingsResponse; @@ -70,6 +70,8 @@ import org.elasticsearch.client.indices.ReloadAnalyzersRequest; import org.elasticsearch.client.indices.ReloadAnalyzersResponse; import org.elasticsearch.client.indices.ResizeRequest; import org.elasticsearch.client.indices.ResizeResponse; +import org.elasticsearch.client.indices.SimulateIndexTemplateRequest; +import org.elasticsearch.client.indices.SimulateIndexTemplateResponse; import org.elasticsearch.client.indices.UnfreezeIndexRequest; import org.elasticsearch.client.indices.rollover.RolloverRequest; import org.elasticsearch.client.indices.rollover.RolloverResponse; @@ -1324,6 +1326,40 @@ public final class IndicesClient { options, AcknowledgedResponse::fromXContent, listener, emptySet()); } + /** + * Simulates matching index name against the existing index templates in the system. + * See Index Templates API + * on elastic.co + * + * @param simulateIndexTemplateRequest the request + * @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 SimulateIndexTemplateResponse simulateIndexTemplate(SimulateIndexTemplateRequest simulateIndexTemplateRequest, + RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(simulateIndexTemplateRequest, + IndicesRequestConverters::simulateIndexTemplate, options, SimulateIndexTemplateResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously simulates matching index name against the existing index templates in the system. + * See Index Templates API + * on elastic.co + * + * @param simulateIndexTemplateRequest the request + * @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 + * @return cancellable that may be used to cancel the request + */ + public Cancellable simulateIndexTemplateAsync(SimulateIndexTemplateRequest simulateIndexTemplateRequest, + RequestOptions options, ActionListener listener) { + return restHighLevelClient.performRequestAsyncAndParseEntity(simulateIndexTemplateRequest, + IndicesRequestConverters::simulateIndexTemplate, options, SimulateIndexTemplateResponse::fromXContent, listener, emptySet()); + } + /** * Validate a potentially expensive query without executing it. *

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java index 5c410805191..d6dc4704be5 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java @@ -46,8 +46,8 @@ import org.elasticsearch.client.indices.DeleteIndexTemplateV2Request; import org.elasticsearch.client.indices.FreezeIndexRequest; import org.elasticsearch.client.indices.GetFieldMappingsRequest; import org.elasticsearch.client.indices.GetIndexRequest; -import org.elasticsearch.client.indices.GetIndexTemplatesRequest; import org.elasticsearch.client.indices.GetIndexTemplateV2Request; +import org.elasticsearch.client.indices.GetIndexTemplatesRequest; import org.elasticsearch.client.indices.GetMappingsRequest; import org.elasticsearch.client.indices.IndexTemplateV2ExistRequest; import org.elasticsearch.client.indices.IndexTemplatesExistRequest; @@ -56,6 +56,7 @@ import org.elasticsearch.client.indices.PutIndexTemplateV2Request; import org.elasticsearch.client.indices.PutMappingRequest; import org.elasticsearch.client.indices.ReloadAnalyzersRequest; import org.elasticsearch.client.indices.ResizeRequest; +import org.elasticsearch.client.indices.SimulateIndexTemplateRequest; import org.elasticsearch.client.indices.UnfreezeIndexRequest; import org.elasticsearch.client.indices.rollover.RolloverRequest; import org.elasticsearch.cluster.metadata.IndexMetadata; @@ -603,6 +604,26 @@ final class IndicesRequestConverters { return request; } + static Request simulateIndexTemplate(SimulateIndexTemplateRequest simulateIndexTemplateRequest) throws IOException { + String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_index_template", "_simulate_index") + .addPathPart(simulateIndexTemplateRequest.indexName()).build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(); + params.withMasterTimeout(simulateIndexTemplateRequest.masterNodeTimeout()); + PutIndexTemplateV2Request putIndexTemplateV2Request = simulateIndexTemplateRequest.indexTemplateV2Request(); + if (putIndexTemplateV2Request != null) { + if (putIndexTemplateV2Request.create()) { + params.putParam("create", Boolean.TRUE.toString()); + } + if (Strings.hasText(putIndexTemplateV2Request.cause())) { + params.putParam("cause", putIndexTemplateV2Request.cause()); + } + request.setEntity(RequestConverters.createEntity(putIndexTemplateV2Request, RequestConverters.REQUEST_BODY_CONTENT_TYPE)); + } + request.addParameters(params.asMap()); + return request; + } + static Request validateQuery(ValidateQueryRequest validateQueryRequest) throws IOException { String[] indices = validateQueryRequest.indices() == null ? Strings.EMPTY_ARRAY : validateQueryRequest.indices(); String[] types = validateQueryRequest.types() == null || indices.length <= 0 ? Strings.EMPTY_ARRAY : validateQueryRequest.types(); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateRequest.java new file mode 100644 index 00000000000..c7147f569a6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateRequest.java @@ -0,0 +1,77 @@ +/* + * 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.indices; + +import org.elasticsearch.client.TimedRequest; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; + +/** + * A request to simulate matching a provided index name and an optional new index template against the existing index templates. + */ +public class SimulateIndexTemplateRequest extends TimedRequest { + + private String indexName; + + @Nullable + private PutIndexTemplateV2Request indexTemplateV2Request; + + public SimulateIndexTemplateRequest(String indexName) { + if (Strings.isNullOrEmpty(indexName)) { + throw new IllegalArgumentException("index name cannot be null or empty"); + } + this.indexName = indexName; + } + + /** + * Return the index name for which we simulate the index template matching. + */ + public String indexName() { + return indexName; + } + + /** + * Set the index name to simulate template matching against the index templates in the system. + */ + public SimulateIndexTemplateRequest indexName(String indexName) { + if (Strings.isNullOrEmpty(indexName)) { + throw new IllegalArgumentException("index name cannot be null or empty"); + } + this.indexName = indexName; + return this; + } + + /** + * An optional new template request will be part of the index template simulation. + */ + @Nullable + public PutIndexTemplateV2Request indexTemplateV2Request() { + return indexTemplateV2Request; + } + + /** + * Optionally, define a new template request which will included in the index simulation as if it was an index template stored in the + * system. The new template will be validated just as a regular, standalone, live, new index template request. + */ + public SimulateIndexTemplateRequest indexTemplateV2Request(@Nullable PutIndexTemplateV2Request indexTemplateV2Request) { + this.indexTemplateV2Request = indexTemplateV2Request; + return this; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateResponse.java new file mode 100644 index 00000000000..161e2f54a8f --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/indices/SimulateIndexTemplateResponse.java @@ -0,0 +1,129 @@ +/* + * 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.indices; + +import org.elasticsearch.cluster.metadata.Template; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class SimulateIndexTemplateResponse { + + private static final ParseField TEMPLATE = new ParseField("template"); + private static final ParseField OVERLAPPING = new ParseField("overlapping"); + private static final ParseField NAME = new ParseField("name"); + private static final ParseField INDEX_PATTERNS = new ParseField("index_patterns"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("simulate_index_templates_response", false, + a -> new SimulateIndexTemplateResponse( + a[0] != null ? (Template) a[0] : null, + a[1] != null ? + ((List) a[1]).stream() + .collect(Collectors.toMap(IndexTemplateAndPatterns::name, IndexTemplateAndPatterns::indexPatterns)) : null + ) + ); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser INNER_PARSER = + new ConstructingObjectParser<>("index_template_and_patterns", false, + a -> new IndexTemplateAndPatterns((String) a[0], (List) a[1])); + + private static class IndexTemplateAndPatterns { + String name; + List indexPatterns; + + IndexTemplateAndPatterns(String name, List indexPatterns) { + this.name = name; + this.indexPatterns = indexPatterns; + } + + public String name() { + return name; + } + + public List indexPatterns() { + return indexPatterns; + } + } + + static { + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), Template.PARSER, TEMPLATE); + INNER_PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME); + INNER_PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), INDEX_PATTERNS); + PARSER.declareObjectArray(ConstructingObjectParser.optionalConstructorArg(), INNER_PARSER, OVERLAPPING); + } + + @Nullable + // the resolved settings, mappings and aliases for the matched templates, if any + private Template resolvedTemplate; + + @Nullable + // a map of template names and their index patterns that would overlap when matching the given index name + private Map> overlappingTemplates; + + SimulateIndexTemplateResponse(@Nullable Template resolvedTemplate, @Nullable Map> overlappingTemplates) { + this.resolvedTemplate = resolvedTemplate; + this.overlappingTemplates = overlappingTemplates; + } + + public static SimulateIndexTemplateResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + public Template resolvedTemplate() { + return resolvedTemplate; + } + + public Map> overlappingTemplates() { + return overlappingTemplates; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimulateIndexTemplateResponse that = (SimulateIndexTemplateResponse) o; + return Objects.equals(resolvedTemplate, that.resolvedTemplate) + && Objects.deepEquals(overlappingTemplates, that.overlappingTemplates); + } + + @Override + public int hashCode() { + return Objects.hash(resolvedTemplate, overlappingTemplates); + } + + @Override + public String toString() { + return "SimulateIndexTemplateResponse{" + "resolved template=" + resolvedTemplate + ", overlapping templates=" + + String.join("|", overlappingTemplates.keySet()) + "}"; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java index f157624cc23..e2e9af835c2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java @@ -68,9 +68,9 @@ import org.elasticsearch.client.indices.GetFieldMappingsRequest; import org.elasticsearch.client.indices.GetFieldMappingsResponse; import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.client.indices.GetIndexResponse; +import org.elasticsearch.client.indices.GetIndexTemplateV2Request; import org.elasticsearch.client.indices.GetIndexTemplatesRequest; import org.elasticsearch.client.indices.GetIndexTemplatesResponse; -import org.elasticsearch.client.indices.GetIndexTemplateV2Request; import org.elasticsearch.client.indices.GetIndexTemplatesV2Response; import org.elasticsearch.client.indices.GetMappingsRequest; import org.elasticsearch.client.indices.GetMappingsResponse; @@ -82,6 +82,8 @@ import org.elasticsearch.client.indices.PutIndexTemplateV2Request; import org.elasticsearch.client.indices.PutMappingRequest; import org.elasticsearch.client.indices.ReloadAnalyzersRequest; import org.elasticsearch.client.indices.ReloadAnalyzersResponse; +import org.elasticsearch.client.indices.SimulateIndexTemplateRequest; +import org.elasticsearch.client.indices.SimulateIndexTemplateResponse; import org.elasticsearch.client.indices.UnfreezeIndexRequest; import org.elasticsearch.client.indices.rollover.RolloverRequest; import org.elasticsearch.client.indices.rollover.RolloverResponse; @@ -2075,4 +2077,41 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase { assertFalse(exist); } + + public void testSimulateIndexTemplate() throws Exception { + String templateName = "my-template"; + Settings settings = Settings.builder().put("index.number_of_shards", 1).build(); + CompressedXContent mappings = new CompressedXContent("{\"properties\":{\"host_name\":{\"type\":\"keyword\"}}}"); + AliasMetadata alias = AliasMetadata.builder("alias").writeIndex(true).build(); + Template template = new Template(settings, mappings, org.elasticsearch.common.collect.Map.of("alias", alias)); + List pattern = org.elasticsearch.common.collect.List.of("pattern"); + IndexTemplateV2 indexTemplate = new IndexTemplateV2(pattern, template, Collections.emptyList(), 1L, 1L, new HashMap<>()); + PutIndexTemplateV2Request putIndexTemplateV2Request = + new PutIndexTemplateV2Request().name(templateName).create(true).indexTemplate(indexTemplate); + + AcknowledgedResponse response = execute(putIndexTemplateV2Request, + highLevelClient().indices()::putIndexTemplate, highLevelClient().indices()::putIndexTemplateAsync); + assertThat(response.isAcknowledged(), equalTo(true)); + + SimulateIndexTemplateRequest simulateIndexTemplateRequest = new SimulateIndexTemplateRequest("pattern"); + AliasMetadata simulationAlias = AliasMetadata.builder("simulation-alias").writeIndex(true).build(); + IndexTemplateV2 simulationTemplate = new IndexTemplateV2(pattern, new Template(null, null, + org.elasticsearch.common.collect.Map.of("simulation-alias", simulationAlias)), Collections.emptyList(), 2L, 1L, + new HashMap<>()); + PutIndexTemplateV2Request newIndexTemplateReq = + new PutIndexTemplateV2Request().name("used-for-simulation").create(true).indexTemplate(indexTemplate); + newIndexTemplateReq.indexTemplate(simulationTemplate); + simulateIndexTemplateRequest.indexTemplateV2Request(newIndexTemplateReq); + + SimulateIndexTemplateResponse simulateResponse = execute(simulateIndexTemplateRequest, + highLevelClient().indices()::simulateIndexTemplate, highLevelClient().indices()::simulateIndexTemplateAsync); + + Map aliases = simulateResponse.resolvedTemplate().aliases(); + assertThat(aliases, is(notNullValue())); + assertThat("the template we provided for the simulation has a higher priority than the one in the system", + aliases.get("simulation-alias"), is(notNullValue())); + assertThat(aliases.get("simulation-alias").getAlias(), is("simulation-alias")); + assertThat("existing template overlaps the higher priority template we provided for the simulation", + simulateResponse.overlappingTemplates().get("my-template").get(0), is("pattern")); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 58e45341812..c9ddb1708fb 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -860,8 +860,7 @@ public class RestHighLevelClientTests extends ESTestCase { "scripts_painless_execute", "indices.create_data_stream", "indices.get_data_streams", - "indices.delete_data_stream", - "indices.simulate_index_template" + "indices.delete_data_stream" }; //These API are not required for high-level client feature completeness String[] notRequiredApi = new String[] { diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.simulate_index_template.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.simulate_index_template.json index c9a966e3fa1..889310e9771 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.simulate_index_template.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.simulate_index_template.json @@ -22,6 +22,16 @@ ] }, "params":{ + "create":{ + "type":"boolean", + "description":"Whether the index template we optionally defined in the body should only be dry-run added if new or can also replace an existing one", + "default":false + }, + "cause":{ + "type":"string", + "description":"User defined reason for dry-run creating the new template for simulation purposes", + "default":false + }, "master_timeout":{ "type":"time", "description":"Specify timeout for connection to master" diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java index 2497e986c97..c4ecaa0e8f3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java @@ -53,7 +53,7 @@ public class Template extends AbstractDiffable