[7.x] Add simulate template composition API _index_template/_simulate_index/{name} (#55686) (#55922)

This adds a new api to simulate matching the given index name against the
 index templates in the system.

The syntax for the new API takes the following form:

POST _index_template/_simulate_index/{index_name}
{
  "index_patterns": ["logs-*"],
  "priority": 15,
  "template": {
	"settings": {
		"number_of_shards": 3
	}
       ...
   }
}

Where the body is optional, but we support the entire body used by the
PUT _index_template/{name} api. When the body is specified we'll simulate
matching the given index against a system that'd have the given index
template together with the index templates that exist in the system.

The response, in both cases, will return the matching template's resolved
settings, mappings and aliases, together with a special field that'll print any
overlapping templates and their corresponding index patterns.

(cherry picked from commit 1a5845edce1f445c58e094e9a3b6792e21e543b0)
Signed-off-by: Andrei Dan <andrei.dan@elastic.co>
This commit is contained in:
Andrei Dan 2020-04-29 14:57:44 +01:00 committed by GitHub
parent 337dc45f5b
commit 6b886b0b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 851 additions and 23 deletions

View File

@ -861,6 +861,7 @@ public class RestHighLevelClientTests extends ESTestCase {
"indices.create_data_stream",
"indices.get_data_streams",
"indices.delete_data_stream",
"indices.simulate_index_template"
};
//These API are not required for high-level client feature completeness
String[] notRequiredApi = new String[] {

View File

@ -0,0 +1,35 @@
{
"indices.simulate_index_template":{
"documentation":{
"url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html",
"description": "Simulate matching the given index name against the index templates in the system"
},
"stability":"stable",
"url":{
"paths":[
{
"path":"/_index_template/_simulate_index/{name}",
"methods":[
"POST"
],
"parts":{
"name":{
"type":"string",
"description":"The name of the index (it must be a concrete index name)"
}
}
}
]
},
"params":{
"master_timeout":{
"type":"time",
"description":"Specify timeout for connection to master"
}
},
"body":{
"description":"New index template definition, which will be included in the simulation, as if it already exists in the system",
"required":false
}
}
}

View File

@ -0,0 +1,181 @@
---
"Simulate index template without new template in the body":
- skip:
version: " - 7.99.99"
reason: "simulate index template API has not been backported"
features: allowed_warnings
- do:
allowed_warnings:
- "index template [test] has index patterns [te*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [test] will take precedence during new index creation"
indices.put_index_template:
name: test
body:
index_patterns: te*
template:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
properties:
field:
type: keyword
- do:
indices.simulate_index_template:
name: test
- match: {template.settings.index.number_of_shards: "1"}
- match: {template.settings.index.number_of_replicas: "0"}
- match: {template.mappings._doc.properties.field.type: "keyword"}
- match: {overlapping: []}
---
"Simulate index template specifying a new template":
- skip:
version: " - 7.99.99"
reason: "simulate index template API has not been backported"
features: allowed_warnings
- do:
allowed_warnings:
- "index template [test] has index patterns [te*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [test] will take precedence during new index creation"
indices.put_index_template:
name: existing_test
body:
index_patterns: te*
priority: 10
template:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
properties:
field:
type: keyword
- do:
cluster.put_component_template:
name: ct
body:
template:
settings:
index.number_of_replicas: 2
mappings:
properties:
ct_field:
type: keyword
- do:
indices.simulate_index_template:
name: test
body:
index_patterns: te*
priority: 15
template:
settings:
index.blocks.write: true
aliases:
test_alias: {}
composed_of: ["ct"]
- match: {template.settings.index.blocks.write: "true"}
- match: {template.settings.index.number_of_replicas: "2"}
- match: {template.mappings._doc.properties.ct_field.type: "keyword"}
- match: {overlapping.0.name: existing_test}
- match: {overlapping.0.index_patterns: ["te*"]}
- length: {template.aliases: 1}
- is_true: template.aliases.test_alias
---
"Simulate index template with index not matching any template":
- skip:
version: " - 7.99.99"
reason: "simulate index template API has not been backported"
features: allowed_warnings
- do:
allowed_warnings:
- "index template [test] has index patterns [te*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [test] will take precedence during new index creation"
indices.put_index_template:
name: test
body:
index_patterns: te*
priority: 10
template:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
properties:
field:
type: keyword
- do:
indices.simulate_index_template:
name: will_not_match
- match: {body: null}
---
"Simulate index matches overlapping V1 and V2 templates":
- skip:
version: " - 7.99.99"
reason: "simulate index template API has not been backported"
features: allowed_warnings
- do:
indices.put_template:
name: v1_template
body:
index_patterns: [t*, t1*]
settings:
number_of_shards: 5
- do:
allowed_warnings:
- "index template [v2_template] has index patterns [te*] matching patterns from existing older templates [v1_template] with patterns
(v1_template => [t*, t1*]); this template [v2_template] will take precedence during new index creation"
indices.put_index_template:
name: v2_template
body:
index_patterns: te*
priority: 10
template:
settings:
number_of_shards: 10
number_of_replicas: 2
mappings:
properties:
field:
type: text
- do:
allowed_warnings:
- "index template [winning_v2_template] has index patterns [te*] matching patterns from existing older templates [v1_template] with patterns
(v1_template => [t*, t1*]); this template [winning_v2_template] will take precedence during new index creation"
indices.put_index_template:
name: winning_v2_template
body:
index_patterns: te*
priority: 20
template:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
properties:
field:
type: keyword
- do:
indices.simulate_index_template:
name: test
- match: {template.settings.index.number_of_shards: "1"}
- match: {template.settings.index.number_of_replicas: "0"}
- match: {template.mappings._doc.properties.field.type: "keyword"}
- match: {overlapping.0.name: v1_template}
- match: {overlapping.0.index_patterns: ["t*", "t1*"]}
- match: {overlapping.1.name: v2_template}
- match: {overlapping.1.index_patterns: ["te*"]}

View File

@ -28,9 +28,6 @@ import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclu
import org.elasticsearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction;
import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction;
import org.elasticsearch.action.admin.cluster.configuration.TransportClearVotingConfigExclusionsAction;
import org.elasticsearch.action.admin.indices.datastream.DeleteDataStreamAction;
import org.elasticsearch.action.admin.indices.datastream.GetDataStreamsAction;
import org.elasticsearch.action.admin.indices.datastream.CreateDataStreamAction;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction;
import org.elasticsearch.action.admin.cluster.node.hotthreads.NodesHotThreadsAction;
@ -109,6 +106,9 @@ import org.elasticsearch.action.admin.indices.close.CloseIndexAction;
import org.elasticsearch.action.admin.indices.close.TransportCloseIndexAction;
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
import org.elasticsearch.action.admin.indices.create.TransportCreateIndexAction;
import org.elasticsearch.action.admin.indices.datastream.CreateDataStreamAction;
import org.elasticsearch.action.admin.indices.datastream.DeleteDataStreamAction;
import org.elasticsearch.action.admin.indices.datastream.GetDataStreamsAction;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
import org.elasticsearch.action.admin.indices.delete.TransportDeleteIndexAction;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsAction;
@ -163,6 +163,8 @@ import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesActi
import org.elasticsearch.action.admin.indices.template.get.TransportGetComponentTemplateAction;
import org.elasticsearch.action.admin.indices.template.get.TransportGetIndexTemplateV2Action;
import org.elasticsearch.action.admin.indices.template.get.TransportGetIndexTemplatesAction;
import org.elasticsearch.action.admin.indices.template.post.SimulateIndexTemplateAction;
import org.elasticsearch.action.admin.indices.template.post.TransportSimulateIndexTemplateAction;
import org.elasticsearch.action.admin.indices.template.put.PutComponentTemplateAction;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateV2Action;
@ -261,11 +263,9 @@ import org.elasticsearch.rest.action.admin.cluster.RestClusterStateAction;
import org.elasticsearch.rest.action.admin.cluster.RestClusterStatsAction;
import org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction;
import org.elasticsearch.rest.action.admin.cluster.RestCreateSnapshotAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteDataStreamAction;
import org.elasticsearch.rest.action.admin.cluster.RestDeleteRepositoryAction;
import org.elasticsearch.rest.action.admin.cluster.RestDeleteSnapshotAction;
import org.elasticsearch.rest.action.admin.cluster.RestDeleteStoredScriptAction;
import org.elasticsearch.rest.action.admin.indices.RestGetDataStreamsAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetRepositoriesAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetScriptContextAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetScriptLanguageAction;
@ -278,7 +278,6 @@ import org.elasticsearch.rest.action.admin.cluster.RestNodesInfoAction;
import org.elasticsearch.rest.action.admin.cluster.RestNodesStatsAction;
import org.elasticsearch.rest.action.admin.cluster.RestNodesUsageAction;
import org.elasticsearch.rest.action.admin.cluster.RestPendingClusterTasksAction;
import org.elasticsearch.rest.action.admin.indices.RestCreateDataStreamAction;
import org.elasticsearch.rest.action.admin.cluster.RestPutRepositoryAction;
import org.elasticsearch.rest.action.admin.cluster.RestPutStoredScriptAction;
import org.elasticsearch.rest.action.admin.cluster.RestReloadSecureSettingsAction;
@ -289,8 +288,10 @@ import org.elasticsearch.rest.action.admin.cluster.RestVerifyRepositoryAction;
import org.elasticsearch.rest.action.admin.indices.RestAnalyzeAction;
import org.elasticsearch.rest.action.admin.indices.RestClearIndicesCacheAction;
import org.elasticsearch.rest.action.admin.indices.RestCloseIndexAction;
import org.elasticsearch.rest.action.admin.indices.RestCreateDataStreamAction;
import org.elasticsearch.rest.action.admin.indices.RestCreateIndexAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteComponentTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteDataStreamAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexTemplateV2Action;
@ -298,6 +299,7 @@ import org.elasticsearch.rest.action.admin.indices.RestFlushAction;
import org.elasticsearch.rest.action.admin.indices.RestForceMergeAction;
import org.elasticsearch.rest.action.admin.indices.RestGetAliasesAction;
import org.elasticsearch.rest.action.admin.indices.RestGetComponentTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestGetDataStreamsAction;
import org.elasticsearch.rest.action.admin.indices.RestGetFieldMappingAction;
import org.elasticsearch.rest.action.admin.indices.RestGetIndexTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestGetIndexTemplateV2Action;
@ -311,6 +313,7 @@ import org.elasticsearch.rest.action.admin.indices.RestIndicesSegmentsAction;
import org.elasticsearch.rest.action.admin.indices.RestIndicesShardStoresAction;
import org.elasticsearch.rest.action.admin.indices.RestIndicesStatsAction;
import org.elasticsearch.rest.action.admin.indices.RestOpenIndexAction;
import org.elasticsearch.rest.action.admin.indices.RestSimulateIndexTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestPutComponentTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction;
import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateV2Action;
@ -561,6 +564,7 @@ public class ActionModule extends AbstractModule {
actions.register(PutIndexTemplateV2Action.INSTANCE, TransportPutIndexTemplateV2Action.class);
actions.register(GetIndexTemplateV2Action.INSTANCE, TransportGetIndexTemplateV2Action.class);
actions.register(DeleteIndexTemplateV2Action.INSTANCE, TransportDeleteIndexTemplateV2Action.class);
actions.register(SimulateIndexTemplateAction.INSTANCE, TransportSimulateIndexTemplateAction.class);
}
actions.register(ValidateQueryAction.INSTANCE, TransportValidateQueryAction.class);
actions.register(RefreshAction.INSTANCE, TransportRefreshAction.class);
@ -703,6 +707,7 @@ public class ActionModule extends AbstractModule {
registerHandler.accept(new RestPutIndexTemplateV2Action());
registerHandler.accept(new RestGetIndexTemplateV2Action());
registerHandler.accept(new RestDeleteIndexTemplateV2Action());
registerHandler.accept(new RestSimulateIndexTemplateAction());
}
registerHandler.accept(new RestPutMappingAction());

View File

@ -0,0 +1,33 @@
/*
* 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.action.admin.indices.template.post;
import org.elasticsearch.action.ActionType;
public class SimulateIndexTemplateAction extends ActionType<SimulateIndexTemplateResponse> {
public static final SimulateIndexTemplateAction INSTANCE = new SimulateIndexTemplateAction();
public static final String NAME = "indices:admin/index_template/simulate_index";
private SimulateIndexTemplateAction() {
super(NAME, SimulateIndexTemplateResponse::new);
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.action.admin.indices.template.post;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateV2Action;
import org.elasticsearch.action.support.master.MasterNodeReadRequest;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import java.util.Objects;
public class SimulateIndexTemplateRequest extends MasterNodeReadRequest<SimulateIndexTemplateRequest> {
private String indexName;
@Nullable
private PutIndexTemplateV2Action.Request indexTemplateRequest;
public SimulateIndexTemplateRequest(String indexName) {
if (Strings.isNullOrEmpty(indexName)) {
throw new IllegalArgumentException("index name cannot be null or empty");
}
this.indexName = indexName;
}
public SimulateIndexTemplateRequest(StreamInput in) throws IOException {
super(in);
indexName = in.readString();
indexTemplateRequest = in.readOptionalWriteable(PutIndexTemplateV2Action.Request::new);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(indexName);
out.writeOptionalWriteable(indexTemplateRequest);
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (indexTemplateRequest != null) {
validationException = indexTemplateRequest.validateIndexTemplate(validationException);
}
return validationException;
}
public String getIndexName() {
return indexName;
}
@Nullable
public PutIndexTemplateV2Action.Request getIndexTemplateRequest() {
return indexTemplateRequest;
}
public SimulateIndexTemplateRequest indexName(String indexName) {
this.indexName = indexName;
return this;
}
public SimulateIndexTemplateRequest indexTemplateRequest(PutIndexTemplateV2Action.Request indexTemplateRequest) {
this.indexTemplateRequest = indexTemplateRequest;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SimulateIndexTemplateRequest that = (SimulateIndexTemplateRequest) o;
return indexName.equals(that.indexName) &&
Objects.equals(indexTemplateRequest, that.indexTemplateRequest);
}
@Override
public int hashCode() {
return Objects.hash(indexName, indexTemplateRequest);
}
}

View File

@ -0,0 +1,133 @@
/*
* 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.action.admin.indices.template.post;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Contains the information on what V2 templates would match a given index.
*/
public class SimulateIndexTemplateResponse extends ActionResponse implements ToXContentObject {
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");
@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<String, List<String>> overlappingTemplates;
public SimulateIndexTemplateResponse(@Nullable Template resolvedTemplate, @Nullable Map<String, List<String>> overlappingTemplates) {
this.resolvedTemplate = resolvedTemplate;
this.overlappingTemplates = overlappingTemplates;
}
public SimulateIndexTemplateResponse(StreamInput in) throws IOException {
super(in);
resolvedTemplate = in.readOptionalWriteable(Template::new);
if (in.readBoolean()) {
int overlappingTemplatesCount = in.readInt();
overlappingTemplates = new HashMap<>(overlappingTemplatesCount, 1L);
for (int i = 0; i < overlappingTemplatesCount; i++) {
String templateName = in.readString();
overlappingTemplates.put(templateName, in.readStringList());
}
} else {
this.overlappingTemplates = null;
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalWriteable(resolvedTemplate);
if (overlappingTemplates != null) {
out.writeBoolean(true);
out.writeInt(overlappingTemplates.size());
for (Map.Entry<String, List<String>> entry : overlappingTemplates.entrySet()) {
out.writeString(entry.getKey());
out.writeStringCollection(entry.getValue());
}
} else {
out.writeBoolean(false);
}
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (this.resolvedTemplate != null) {
builder.field(TEMPLATE.getPreferredName(), this.resolvedTemplate);
}
if (this.overlappingTemplates != null) {
builder.startArray(OVERLAPPING.getPreferredName());
for (Map.Entry<String, List<String>> entry : overlappingTemplates.entrySet()) {
builder.startObject();
builder.field(NAME.getPreferredName(), entry.getKey());
builder.field(INDEX_PATTERNS.getPreferredName(), entry.getValue());
builder.endObject();
}
builder.endArray();
}
builder.endObject();
return builder;
}
@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()) + "}";
}
}

View File

@ -0,0 +1,177 @@
/*
* 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.action.admin.indices.template.post;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.master.TransportMasterNodeReadAction;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.AliasMetadata;
import org.elasticsearch.cluster.metadata.AliasValidator;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.IndexTemplateV2;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.MetadataCreateIndexService;
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.resolveV2Mappings;
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV1Templates;
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV2Templates;
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findV2Template;
import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.resolveSettings;
import static org.elasticsearch.indices.cluster.IndicesClusterStateService.AllocatedIndices.IndexRemovalReason.NO_LONGER_ASSIGNED;
public class TransportSimulateIndexTemplateAction
extends TransportMasterNodeReadAction<SimulateIndexTemplateRequest, SimulateIndexTemplateResponse> {
private final MetadataIndexTemplateService indexTemplateService;
private final NamedXContentRegistry xContentRegistry;
private final IndicesService indicesService;
private AliasValidator aliasValidator;
@Inject
public TransportSimulateIndexTemplateAction(TransportService transportService, ClusterService clusterService,
ThreadPool threadPool, MetadataIndexTemplateService indexTemplateService,
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
NamedXContentRegistry xContentRegistry, IndicesService indicesService) {
super(SimulateIndexTemplateAction.NAME, transportService, clusterService, threadPool, actionFilters,
SimulateIndexTemplateRequest::new, indexNameExpressionResolver);
this.indexTemplateService = indexTemplateService;
this.xContentRegistry = xContentRegistry;
this.indicesService = indicesService;
this.aliasValidator = new AliasValidator();
}
@Override
protected String executor() {
return ThreadPool.Names.SAME;
}
@Override
protected SimulateIndexTemplateResponse read(StreamInput in) throws IOException {
return new SimulateIndexTemplateResponse(in);
}
@Override
protected void masterOperation(SimulateIndexTemplateRequest request, ClusterState state,
ActionListener<SimulateIndexTemplateResponse> listener) throws Exception {
ClusterState simulateOnClusterState = state;
if (request.getIndexTemplateRequest() != null) {
// we'll "locally" add the template defined by the user in the cluster state (as if it existed in the system)
String simulateTemplateToAdd = "simulate_new_template_" + UUIDs.randomBase64UUID();
simulateOnClusterState = indexTemplateService.addIndexTemplateV2(state, request.getIndexTemplateRequest().create(),
simulateTemplateToAdd, request.getIndexTemplateRequest().indexTemplate());
}
String matchingTemplate = findV2Template(simulateOnClusterState.metadata(), request.getIndexName(), false);
if (matchingTemplate == null) {
listener.onResponse(new SimulateIndexTemplateResponse(null, null));
return;
}
Settings settings = resolveSettings(simulateOnClusterState.metadata(), matchingTemplate);
// empty request mapping as the user can't specify any explicit mappings via the simulate api
Map<String, Map<String, Object>> mappings = resolveV2Mappings("{}", simulateOnClusterState, matchingTemplate, xContentRegistry);
String mappingsJson = Strings.toString(XContentFactory.jsonBuilder()
.startObject()
.field(MapperService.SINGLE_MAPPING_NAME, mappings)
.endObject());
List<Map<String, AliasMetadata>> resolvedAliases = MetadataIndexTemplateService.resolveAliases(simulateOnClusterState.metadata(),
matchingTemplate);
// create the index with dummy settings in the cluster state so we can parse and validate the aliases
Settings dummySettings = Settings.builder()
.put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT)
.put(settings)
.put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
.put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
.put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID())
.build();
final IndexMetadata indexMetadata = IndexMetadata.builder(request.getIndexName()).settings(dummySettings).build();
simulateOnClusterState = ClusterState.builder(simulateOnClusterState)
.metadata(Metadata.builder(simulateOnClusterState.metadata())
.put(indexMetadata, true)
.build())
.build();
IndexService tempIndexService = indicesService.createIndex(indexMetadata, Collections.emptyList(), false);
final Index index = tempIndexService.index();
try (Closeable dummy = () -> tempIndexService.close("temp", false)) {
List<AliasMetadata> aliases = MetadataCreateIndexService.resolveAndValidateAliases(request.getIndexName(),
org.elasticsearch.common.collect.Set.of(), resolvedAliases, simulateOnClusterState.metadata(), aliasValidator,
xContentRegistry,
// the context is only used for validation so it's fine to pass fake values for the
// shard id and the current timestamp
tempIndexService.newQueryShardContext(0, null, () -> 0L, null));
IndexTemplateV2 templateV2 = simulateOnClusterState.metadata().templatesV2().get(matchingTemplate);
assert templateV2 != null : "the matched template must exist";
Map<String, List<String>> overlapping = new HashMap<>();
overlapping.putAll(findConflictingV1Templates(simulateOnClusterState, matchingTemplate, templateV2.indexPatterns()));
overlapping.putAll(findConflictingV2Templates(simulateOnClusterState, matchingTemplate, templateV2.indexPatterns()));
Template template = new Template(settings, mappingsJson == null ? null : new CompressedXContent(mappingsJson),
aliases.stream().collect(Collectors.toMap(AliasMetadata::getAlias, Function.identity())));
listener.onResponse(new SimulateIndexTemplateResponse(template, overlapping));
} finally {
if (index != null) {
indicesService.removeIndex(index, NO_LONGER_ASSIGNED,
"created as part of a simulation for an index name matching the index templates in the system");
}
}
}
@Override
protected ClusterBlockException checkBlock(SimulateIndexTemplateRequest request, ClusterState state) {
return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
}
}

View File

@ -87,6 +87,11 @@ public class PutIndexTemplateV2Action extends ActionType<AcknowledgedResponse> {
if (name == null || Strings.hasText(name) == false) {
validationException = addValidationError("name is missing", validationException);
}
validationException = validateIndexTemplate(validationException);
return validationException;
}
public ActionRequestValidationException validateIndexTemplate(@Nullable ActionRequestValidationException validationException) {
if (indexTemplate == null) {
validationException = addValidationError("an index template is required", validationException);
} else {
@ -99,7 +104,6 @@ public class PutIndexTemplateV2Action extends ActionType<AcknowledgedResponse> {
}
}
}
return validationException;
}

View File

@ -522,10 +522,10 @@ public class MetadataCreateIndexService {
Collections.singletonList(templateName), metadataTransformer);
}
static Map<String, Map<String, Object>> resolveV2Mappings(final String requestMappings,
final ClusterState currentState,
final String templateName,
final NamedXContentRegistry xContentRegistry) throws Exception {
public static Map<String, Map<String, Object>> resolveV2Mappings(final String requestMappings,
final ClusterState currentState,
final String templateName,
final NamedXContentRegistry xContentRegistry) throws Exception {
final Map<String, Map<String, Object>> mappings = Collections.unmodifiableMap(parseV2Mappings(requestMappings,
MetadataIndexTemplateService.resolveMappings(currentState, templateName), xContentRegistry));
return mappings;
@ -806,9 +806,10 @@ public class MetadataCreateIndexService {
* @return the list of resolved aliases, with the explicitly provided aliases occurring first (having a higher priority) followed by
* the ones inherited from the templates
*/
static List<AliasMetadata> resolveAndValidateAliases(String index, Set<Alias> aliases, List<Map<String, AliasMetadata>> templateAliases,
Metadata metadata, AliasValidator aliasValidator,
NamedXContentRegistry xContentRegistry, QueryShardContext queryShardContext) {
public static List<AliasMetadata> resolveAndValidateAliases(String index, Set<Alias> aliases,
List<Map<String, AliasMetadata>> templateAliases, Metadata metadata,
AliasValidator aliasValidator, NamedXContentRegistry xContentRegistry,
QueryShardContext queryShardContext) {
List<AliasMetadata> resolvedAliases = new ArrayList<>();
for (Alias alias : aliases) {
aliasValidator.validateAlias(alias, index, metadata);

View File

@ -332,9 +332,8 @@ public class MetadataIndexTemplateService {
}
}
// Package visible for testing
ClusterState addIndexTemplateV2(final ClusterState currentState, final boolean create,
final String name, final IndexTemplateV2 template) throws Exception {
public ClusterState addIndexTemplateV2(final ClusterState currentState, final boolean create,
final String name, final IndexTemplateV2 template) throws Exception {
if (create && currentState.metadata().templatesV2().containsKey(name)) {
throw new IllegalArgumentException("index template [" + name + "] already exists");
}
@ -413,8 +412,8 @@ public class MetadataIndexTemplateService {
* Return a map of v1 template names to their index patterns for v1 templates that would overlap
* with the given v2 template's index patterns.
*/
static Map<String, List<String>> findConflictingV1Templates(final ClusterState state, final String candidateName,
final List<String> indexPatterns) {
public static Map<String, List<String>> findConflictingV1Templates(final ClusterState state, final String candidateName,
final List<String> indexPatterns) {
Automaton v2automaton = Regex.simpleMatchToAutomaton(indexPatterns.toArray(Strings.EMPTY_ARRAY));
Map<String, List<String>> overlappingTemplates = new HashMap<>();
for (ObjectObjectCursor<String, IndexTemplateMetadata> cursor : state.metadata().templates()) {
@ -432,16 +431,22 @@ public class MetadataIndexTemplateService {
/**
* Return a map of v2 template names to their index patterns for v2 templates that would overlap
* with the given v1 template's index patterns.
* with the given template's index patterns.
*/
static Map<String, List<String>> findConflictingV2Templates(final ClusterState state, final String candidateName,
final List<String> indexPatterns) {
public static Map<String, List<String>> findConflictingV2Templates(final ClusterState state, final String candidateName,
final List<String> indexPatterns) {
return findConflictingV2Templates(state, candidateName, indexPatterns, false, null);
}
/**
* Return a map of v2 template names to their index patterns for v2 templates that would overlap
* with the given template's index patterns.
*
* Based on the provided checkPriority and priority parameters this aims to report the overlapping
* index templates regardless of the priority (ie. checkPriority == false) or otherwise overlapping
* templates with the same priority as the given priority parameter (this is useful when trying to
* add a new template, as we don't support multiple overlapping, from an index pattern perspective,
* index templates with the same priority).
*/
static Map<String, List<String>> findConflictingV2Templates(final ClusterState state, final String candidateName,
final List<String> indexPatterns, boolean checkPriority, Long priority) {
@ -459,6 +464,9 @@ public class MetadataIndexTemplateService {
}
}
}
// if the candidate was a V2 template that already exists in the cluster state it will "overlap" with itself so remove it from the
// results
overlappingTemplates.remove(candidateName);
return overlappingTemplates;
}

View File

@ -83,7 +83,7 @@ public class Template extends AbstractDiffable<Template> implements ToXContentOb
this.aliases = aliases;
}
Template(StreamInput in) throws IOException {
public Template(StreamInput in) throws IOException {
if (in.readBoolean()) {
this.settings = Settings.readSettingsFromStream(in);
} else {

View File

@ -0,0 +1,63 @@
/*
* 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.rest.action.admin.indices;
import org.elasticsearch.action.admin.indices.template.post.SimulateIndexTemplateAction;
import org.elasticsearch.action.admin.indices.template.post.SimulateIndexTemplateRequest;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateV2Action;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.cluster.metadata.IndexTemplateV2;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener;
import java.io.IOException;
import java.util.List;
import static org.elasticsearch.rest.RestRequest.Method.POST;
public class RestSimulateIndexTemplateAction extends BaseRestHandler {
@Override
public List<Route> routes() {
return org.elasticsearch.common.collect.List.of(
new Route(POST, "/_index_template/_simulate_index/{name}"));
}
@Override
public String getName() {
return "simulate_index_template_action";
}
@Override
public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
SimulateIndexTemplateRequest simulateIndexTemplateRequest = new SimulateIndexTemplateRequest(request.param("name"));
simulateIndexTemplateRequest.masterNodeTimeout(request.paramAsTime("master_timeout",
simulateIndexTemplateRequest.masterNodeTimeout()));
if (request.hasContent()) {
PutIndexTemplateV2Action.Request indexTemplateRequest = new PutIndexTemplateV2Action.Request("simulating_template");
indexTemplateRequest.indexTemplate(IndexTemplateV2.parse(request.contentParser()));
simulateIndexTemplateRequest.indexTemplateRequest(indexTemplateRequest);
}
return channel -> client.execute(SimulateIndexTemplateAction.INSTANCE, simulateIndexTemplateRequest,
new RestToXContentListener<>(channel));
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.action.admin.indices.template.post;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateV2Action;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.IndexTemplateV2;
import org.elasticsearch.cluster.metadata.IndexTemplateV2Tests;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
public class SimulateIndexTemplateRequestTests extends AbstractWireSerializingTestCase<SimulateIndexTemplateRequest> {
@Override
protected Writeable.Reader<SimulateIndexTemplateRequest> instanceReader() {
return SimulateIndexTemplateRequest::new;
}
@Override
protected SimulateIndexTemplateRequest createTestInstance() {
SimulateIndexTemplateRequest req = new SimulateIndexTemplateRequest(randomAlphaOfLength(10));
PutIndexTemplateV2Action.Request newTemplateRequest = new PutIndexTemplateV2Action.Request(randomAlphaOfLength(4));
newTemplateRequest.indexTemplate(IndexTemplateV2Tests.randomInstance());
req.indexTemplateRequest(newTemplateRequest);
return req;
}
@Override
protected SimulateIndexTemplateRequest mutateInstance(SimulateIndexTemplateRequest instance) throws IOException {
return randomValueOtherThan(instance, this::createTestInstance);
}
public void testIndexNameCannotBeNullOrEmpty() {
expectThrows(IllegalArgumentException.class, () -> new SimulateIndexTemplateRequest((String) null));
expectThrows(IllegalArgumentException.class, () -> new SimulateIndexTemplateRequest(""));
}
public void testAddingGlobalTemplateWithHiddenIndexSettingIsIllegal() {
Template template = new Template(Settings.builder().put(IndexMetadata.SETTING_INDEX_HIDDEN, true).build(), null, null);
IndexTemplateV2 globalTemplate = new IndexTemplateV2(org.elasticsearch.common.collect.List.of("*"), template, null, null, null,
null);
PutIndexTemplateV2Action.Request request = new PutIndexTemplateV2Action.Request("test");
request.indexTemplate(globalTemplate);
SimulateIndexTemplateRequest simulateRequest = new SimulateIndexTemplateRequest("testing");
simulateRequest.indexTemplateRequest(request);
ActionRequestValidationException validationException = simulateRequest.validate();
assertThat(validationException, is(notNullValue()));
List<String> validationErrors = validationException.validationErrors();
assertThat(validationErrors.size(), is(1));
String error = validationErrors.get(0);
assertThat(error, is("global V2 templates may not specify the setting " + IndexMetadata.SETTING_INDEX_HIDDEN));
}
}