Browse Source

Add REST APIs for IndexTemplateV2Metadata CRUD (#54039)

* Add REST APIs for IndexTemplateV2Metadata CRUD

This commit adds the get/put/delete APIs for interacting with the now v2 versions of index
templates.

These APIs are behind the existing `es.itv2_feature_flag_registered` system property feature flag.

Relates to #53101

* Add exceptions for HLRC tests

* Add skips for 7.x versions

* Use index_template instead of template_v2 in action names

* Add test for MetaDataIndexTemplateService.addIndexTemplateV2

* Move removal to static method and add test

* Add unit tests for request classes (implement hashCode & equals)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Lee Hinman 5 years ago
parent
commit
e89e916738
24 changed files with 1597 additions and 4 deletions
  1. 3 1
      client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
  2. 35 0
      rest-api-spec/src/main/resources/rest-api-spec/api/indices.delete_index_template.json
  3. 45 0
      rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_index_template.json
  4. 45 0
      rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_index_template.json
  5. 66 0
      rest-api-spec/src/main/resources/rest-api-spec/test/indices.get_index_template/10_basic.yml
  6. 20 0
      rest-api-spec/src/main/resources/rest-api-spec/test/indices.get_index_template/20_get_missing.yml
  7. 112 0
      rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/10_basic.yml
  8. 15 0
      server/src/main/java/org/elasticsearch/action/ActionModule.java
  9. 108 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/DeleteIndexTemplateV2Action.java
  10. 79 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteIndexTemplateV2Action.java
  11. 186 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2Action.java
  12. 94 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/get/TransportGetIndexTemplateV2Action.java
  13. 181 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateV2Action.java
  14. 89 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateV2Action.java
  15. 102 0
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java
  16. 53 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestDeleteIndexTemplateV2Action.java
  17. 81 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndexTemplateV2Action.java
  18. 60 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutIndexTemplateV2Action.java
  19. 42 0
      server/src/test/java/org/elasticsearch/action/admin/indices/template/delete/DeleteIndexTemplateV2RequestTests.java
  20. 42 0
      server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2RequestTests.java
  21. 54 0
      server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2ResponseTests.java
  22. 47 0
      server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateV2RequestTests.java
  23. 32 0
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateServiceTests.java
  24. 6 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

+ 3 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

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

+ 35 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/indices.delete_index_template.json

@@ -0,0 +1,35 @@
+{
+  "indices.delete_index_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html",
+      "description":"Deletes an index template."
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_index_template/{name}",
+          "methods":[
+            "DELETE"
+          ],
+          "parts":{
+            "name":{
+              "type":"string",
+              "description":"The name of the template"
+            }
+          }
+        }
+      ]
+    },
+    "params":{
+      "timeout":{
+        "type":"time",
+        "description":"Explicit operation timeout"
+      },
+      "master_timeout":{
+        "type":"time",
+        "description":"Specify timeout for connection to master"
+      }
+    }
+  }
+}

+ 45 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_index_template.json

@@ -0,0 +1,45 @@
+{
+  "indices.get_index_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html",
+      "description":"Returns an index template."
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_index_template",
+          "methods":[
+            "GET"
+          ]
+        },
+        {
+          "path":"/_index_template/{name}",
+          "methods":[
+            "GET"
+          ],
+          "parts":{
+            "name":{
+              "type":"list",
+              "description":"The comma separated names of the index templates"
+            }
+          }
+        }
+      ]
+    },
+    "params":{
+      "flat_settings":{
+        "type":"boolean",
+        "description":"Return settings in flat format (default: false)"
+      },
+      "master_timeout":{
+        "type":"time",
+        "description":"Explicit operation timeout for connection to master node"
+      },
+      "local":{
+        "type":"boolean",
+        "description":"Return local information, do not retrieve the state from master node (default: false)"
+      }
+    }
+  }
+}

+ 45 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_index_template.json

@@ -0,0 +1,45 @@
+{
+  "indices.put_index_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html",
+      "description":"Creates or updates an index template."
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_index_template/{name}",
+          "methods":[
+            "PUT",
+            "POST"
+          ],
+          "parts":{
+            "name":{
+              "type":"string",
+              "description":"The name of the template"
+            }
+          }
+        }
+      ]
+    },
+    "params":{
+      "order":{
+        "type":"number",
+        "description":"The order for this template when merging multiple matching ones (higher numbers are merged later, overriding the lower numbers)"
+      },
+      "create":{
+        "type":"boolean",
+        "description":"Whether the index template should only be added if new or can also replace an existing one",
+        "default":false
+      },
+      "master_timeout":{
+        "type":"time",
+        "description":"Specify timeout for connection to master"
+      }
+    },
+    "body":{
+      "description":"The template definition",
+      "required":true
+    }
+  }
+}

+ 66 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/indices.get_index_template/10_basic.yml

@@ -0,0 +1,66 @@
+setup:
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.put_index_template:
+        name: test
+        body:
+          index_patterns: test-*
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+            mappings:
+              properties:
+                field:
+                  type: keyword
+
+---
+"Get index template":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.get_index_template:
+        name: test
+
+  - match: {index_templates.0.name: test}
+  - match: {index_templates.0.index_template.index_patterns: ["test-*"]}
+  - match: {index_templates.0.index_template.template.settings: {index: {number_of_shards: '1', number_of_replicas: '0'}}}
+  - match: {index_templates.0.index_template.template.mappings: {properties: {field: {type: keyword}}}}
+
+---
+"Get all tindex emplates":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.put_index_template:
+        name: test2
+        body:
+          index_patterns: test2-*
+          template:
+            settings:
+              number_of_shards:   1
+
+  - do:
+      indices.get_index_template: {}
+
+  - length: {index_templates: 2}
+
+---
+"Get index template with local flag":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.get_index_template:
+        name: test
+        local: true
+
+  - match: {index_templates.0.name: test}

+ 20 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/indices.get_index_template/20_get_missing.yml

@@ -0,0 +1,20 @@
+setup:
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.delete_index_template:
+        name:   '*'
+        ignore: 404
+---
+"Get missing template":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      catch:  missing
+      indices.get_index_template:
+        name: test
+

+ 112 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/10_basic.yml

@@ -0,0 +1,112 @@
+---
+"Put index template":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.put_index_template:
+        name: test
+        body:
+          index_patterns: test-*
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+            mappings:
+              properties:
+                field:
+                  type: keyword
+
+  - do:
+      indices.get_index_template:
+        name: test
+
+  - match: {index_templates.0.name: "test"}
+  - match: {index_templates.0.index_template.index_patterns: ["test-*"]}
+  - match: {index_templates.0.index_template.template.settings.index: {number_of_shards: '1', number_of_replicas: '0'}}
+  - match: {index_templates.0.index_template.template.mappings: {properties: {field: {type: keyword}}}}
+
+---
+"Put multiple index templates":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.put_index_template:
+        name: test
+        body:
+          index_patterns: [test-*, test2-*]
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+            mappings:
+              properties:
+                field:
+                  type: text
+            aliases:
+              test_alias: {}
+              test_blias: { routing: b }
+              test_clias: { filter: { term: { user: kimchy }}}
+
+  - do:
+      indices.get_index_template:
+        name: test
+
+  - match: {index_templates.0.index_template.index_patterns: ["test-*", "test2-*"]}
+  - match: {index_templates.0.index_template.template.settings.index: {number_of_shards: '1', number_of_replicas: '0'}}
+  - match: {index_templates.0.index_template.template.mappings: {properties: {field: {type: text}}}}
+  - length: {index_templates.0.index_template.template.aliases: 3}
+  - is_true: index_templates.0.index_template.template.aliases.test_alias
+  - match: {index_templates.0.index_template.template.aliases.test_blias.index_routing: "b" }
+  - match: {index_templates.0.index_template.template.aliases.test_blias.search_routing: "b" }
+  - match: {index_templates.0.index_template.template.aliases.test_clias.filter.term.user: "kimchy" }
+
+---
+"Put index template with 'create' flag":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      indices.put_index_template:
+        name: test2
+        body:
+          index_patterns: test-*
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+
+  - do:
+      indices.get_index_template:
+        name: test2
+
+  - match: {index_templates.0.index_template.index_patterns: ["test-*"]}
+  - match: {index_templates.0.index_template.template.settings.index: {number_of_shards: '1', number_of_replicas: '0'}}
+
+  - do:
+      catch: bad_request
+      indices.put_index_template:
+        name: test2
+        create: true
+        body:
+          index_patterns: test-*
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+
+---
+"Put index template without index_patterns":
+  - skip:
+      version: " - 7.99.99"
+      reason: "index template v2 API has not been backported"
+
+  - do:
+      catch: bad_request
+      indices.put_index_template:
+        name: test
+        body: {}

+ 15 - 0
server/src/main/java/org/elasticsearch/action/ActionModule.java

@@ -148,16 +148,22 @@ import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction;
 import org.elasticsearch.action.admin.indices.stats.TransportIndicesStatsAction;
 import org.elasticsearch.action.admin.indices.template.delete.DeleteComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateV2Action;
 import org.elasticsearch.action.admin.indices.template.delete.TransportDeleteComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.delete.TransportDeleteIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.delete.TransportDeleteIndexTemplateV2Action;
 import org.elasticsearch.action.admin.indices.template.get.GetComponentTemplateAction;
+import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplateV2Action;
 import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesAction;
 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.put.PutComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateV2Action;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.put.TransportPutIndexTemplateV2Action;
 import org.elasticsearch.action.admin.indices.upgrade.get.TransportUpgradeStatusAction;
 import org.elasticsearch.action.admin.indices.upgrade.get.UpgradeStatusAction;
 import org.elasticsearch.action.admin.indices.upgrade.post.TransportUpgradeAction;
@@ -286,12 +292,14 @@ import org.elasticsearch.rest.action.admin.indices.RestCreateIndexAction;
 import org.elasticsearch.rest.action.admin.indices.RestDeleteComponentTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexAction;
 import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexTemplateAction;
+import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexTemplateV2Action;
 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.RestGetFieldMappingAction;
 import org.elasticsearch.rest.action.admin.indices.RestGetIndexTemplateAction;
+import org.elasticsearch.rest.action.admin.indices.RestGetIndexTemplateV2Action;
 import org.elasticsearch.rest.action.admin.indices.RestGetIndicesAction;
 import org.elasticsearch.rest.action.admin.indices.RestGetMappingAction;
 import org.elasticsearch.rest.action.admin.indices.RestGetSettingsAction;
@@ -304,6 +312,7 @@ import org.elasticsearch.rest.action.admin.indices.RestIndicesStatsAction;
 import org.elasticsearch.rest.action.admin.indices.RestOpenIndexAction;
 import org.elasticsearch.rest.action.admin.indices.RestPutComponentTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction;
+import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateV2Action;
 import org.elasticsearch.rest.action.admin.indices.RestPutMappingAction;
 import org.elasticsearch.rest.action.admin.indices.RestRecoveryAction;
 import org.elasticsearch.rest.action.admin.indices.RestRefreshAction;
@@ -538,6 +547,9 @@ public class ActionModule extends AbstractModule {
             actions.register(PutComponentTemplateAction.INSTANCE, TransportPutComponentTemplateAction.class);
             actions.register(GetComponentTemplateAction.INSTANCE, TransportGetComponentTemplateAction.class);
             actions.register(DeleteComponentTemplateAction.INSTANCE, TransportDeleteComponentTemplateAction.class);
+            actions.register(PutIndexTemplateV2Action.INSTANCE, TransportPutIndexTemplateV2Action.class);
+            actions.register(GetIndexTemplateV2Action.INSTANCE, TransportGetIndexTemplateV2Action.class);
+            actions.register(DeleteIndexTemplateV2Action.INSTANCE, TransportDeleteIndexTemplateV2Action.class);
         }
         actions.register(ValidateQueryAction.INSTANCE, TransportValidateQueryAction.class);
         actions.register(RefreshAction.INSTANCE, TransportRefreshAction.class);
@@ -685,6 +697,9 @@ public class ActionModule extends AbstractModule {
             registerHandler.accept(new RestPutComponentTemplateAction());
             registerHandler.accept(new RestGetComponentTemplateAction());
             registerHandler.accept(new RestDeleteComponentTemplateAction());
+            registerHandler.accept(new RestPutIndexTemplateV2Action());
+            registerHandler.accept(new RestGetIndexTemplateV2Action());
+            registerHandler.accept(new RestDeleteIndexTemplateV2Action());
         }
 
         registerHandler.accept(new RestPutMappingAction());

+ 108 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/DeleteIndexTemplateV2Action.java

@@ -0,0 +1,108 @@
+/*
+ * 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.delete;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+public class DeleteIndexTemplateV2Action extends ActionType<AcknowledgedResponse> {
+
+    public static final DeleteIndexTemplateV2Action INSTANCE = new DeleteIndexTemplateV2Action();
+    public static final String NAME = "indices:admin/index_template/delete";
+
+    private DeleteIndexTemplateV2Action() {
+        super(NAME, AcknowledgedResponse::new);
+    }
+
+    public static class Request extends MasterNodeRequest<Request> {
+
+        private String name;
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+            name = in.readString();
+        }
+
+        public Request() { }
+
+        /**
+         * Constructs a new delete template request for the specified name.
+         */
+        public Request(String name) {
+            this.name = name;
+        }
+
+        /**
+         * Set the index template name to delete.
+         */
+        public Request name(String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            ActionRequestValidationException validationException = null;
+            if (name == null) {
+                validationException = addValidationError("name is missing", validationException);
+            }
+            return validationException;
+        }
+
+        /**
+         * The index template name to delete.
+         */
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            out.writeString(name);
+        }
+
+        @Override
+        public int hashCode() {
+            return name.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            Request other = (Request) obj;
+            return Objects.equals(other.name, this.name);
+        }
+    }
+}

+ 79 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteIndexTemplateV2Action.java

@@ -0,0 +1,79 @@
+/*
+ * 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.delete;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.io.IOException;
+
+public class TransportDeleteIndexTemplateV2Action
+    extends TransportMasterNodeAction<DeleteIndexTemplateV2Action.Request, AcknowledgedResponse> {
+
+    private static final Logger logger = LogManager.getLogger(TransportDeleteIndexTemplateV2Action.class);
+
+    private final MetaDataIndexTemplateService indexTemplateService;
+
+    @Inject
+    public TransportDeleteIndexTemplateV2Action(TransportService transportService, ClusterService clusterService,
+                                                  ThreadPool threadPool, MetaDataIndexTemplateService indexTemplateService,
+                                                  ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(DeleteIndexTemplateV2Action.NAME, transportService, clusterService, threadPool, actionFilters,
+            DeleteIndexTemplateV2Action.Request::new, indexNameExpressionResolver);
+        this.indexTemplateService = indexTemplateService;
+    }
+
+    @Override
+    protected String executor() {
+        // we go async right away
+        return ThreadPool.Names.SAME;
+    }
+
+    @Override
+    protected AcknowledgedResponse read(StreamInput in) throws IOException {
+        return new AcknowledgedResponse(in);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(DeleteIndexTemplateV2Action.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
+    }
+
+    @Override
+    protected void masterOperation(Task task, final DeleteIndexTemplateV2Action.Request request, final ClusterState state,
+                                   final ActionListener<AcknowledgedResponse> listener) {
+        indexTemplateService.removeIndexTemplateV2(request.name(), request.masterNodeTimeout(), listener);
+    }
+}

+ 186 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2Action.java

@@ -0,0 +1,186 @@
+/*
+ * 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.get;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.support.master.MasterNodeReadRequest;
+import org.elasticsearch.cluster.metadata.IndexTemplateV2;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+public class GetIndexTemplateV2Action extends ActionType<GetIndexTemplateV2Action.Response> {
+
+    public static final GetIndexTemplateV2Action INSTANCE = new GetIndexTemplateV2Action();
+    public static final String NAME = "indices:admin/index_template/get";
+
+    private GetIndexTemplateV2Action() {
+        super(NAME, GetIndexTemplateV2Action.Response::new);
+    }
+
+    /**
+     * Request that to retrieve one or more index templates
+     */
+    public static class Request extends MasterNodeReadRequest<Request> {
+
+        private String[] names;
+
+        public Request() { }
+
+        public Request(String... names) {
+            this.names = names;
+        }
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+            names = in.readStringArray();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            out.writeStringArray(names);
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            ActionRequestValidationException validationException = null;
+            if (names == null) {
+                validationException = addValidationError("names is null or empty", validationException);
+            } else {
+                for (String name : names) {
+                    if (name == null || Strings.hasText(name) == false) {
+                        validationException = addValidationError("name is missing", validationException);
+                    }
+                }
+            }
+            return validationException;
+        }
+
+        /**
+         * Sets the names of the index templates.
+         */
+        public Request names(String... names) {
+            this.names = names;
+            return this;
+        }
+
+        /**
+         * The names of the index templates.
+         */
+        public String[] names() {
+            return this.names;
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(names);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            Request other = (Request) obj;
+            return Arrays.equals(other.names, this.names);
+        }
+    }
+
+    public static class Response extends ActionResponse implements ToXContentObject {
+        public static final ParseField NAME = new ParseField("name");
+        public static final ParseField INDEX_TEMPLATES = new ParseField("index_templates");
+        public static final ParseField INDEX_TEMPLATE = new ParseField("index_template");
+
+        private final Map<String, IndexTemplateV2> indexTemplates;
+
+        public Response(StreamInput in) throws IOException {
+            super(in);
+            int size = in.readVInt();
+            indexTemplates = new HashMap<>();
+            for (int i = 0 ; i < size ; i++) {
+                indexTemplates.put(in.readString(), new IndexTemplateV2(in));
+            }
+        }
+
+        public Response(Map<String, IndexTemplateV2> indexTemplates) {
+            this.indexTemplates = indexTemplates;
+        }
+
+        public Map<String, IndexTemplateV2> indexTemplates() {
+            return indexTemplates;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeVInt(indexTemplates.size());
+            for (Map.Entry<String, IndexTemplateV2> indexTemplate : indexTemplates.entrySet()) {
+                out.writeString(indexTemplate.getKey());
+                indexTemplate.getValue().writeTo(out);
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            GetIndexTemplateV2Action.Response that = (GetIndexTemplateV2Action.Response) o;
+            return Objects.equals(indexTemplates, that.indexTemplates);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(indexTemplates);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.startArray(INDEX_TEMPLATES.getPreferredName());
+            for (Map.Entry<String, IndexTemplateV2> indexTemplate : this.indexTemplates.entrySet()) {
+                builder.startObject();
+                builder.field(NAME.getPreferredName(), indexTemplate.getKey());
+                builder.field(INDEX_TEMPLATE.getPreferredName(), indexTemplate.getValue());
+                builder.endObject();
+            }
+            builder.endArray();
+            builder.endObject();
+            return builder;
+        }
+
+    }
+
+}

+ 94 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/get/TransportGetIndexTemplateV2Action.java

@@ -0,0 +1,94 @@
+/*
+ * 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.get;
+
+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.IndexTemplateV2;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.regex.Regex;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TransportGetIndexTemplateV2Action
+    extends TransportMasterNodeReadAction<GetIndexTemplateV2Action.Request, GetIndexTemplateV2Action.Response> {
+
+    @Inject
+    public TransportGetIndexTemplateV2Action(TransportService transportService, ClusterService clusterService,
+                                               ThreadPool threadPool, ActionFilters actionFilters,
+                                               IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(GetIndexTemplateV2Action.NAME, transportService, clusterService, threadPool, actionFilters,
+            GetIndexTemplateV2Action.Request::new, indexNameExpressionResolver);
+    }
+
+    @Override
+    protected String executor() {
+        return ThreadPool.Names.SAME;
+    }
+
+    @Override
+    protected GetIndexTemplateV2Action.Response read(StreamInput in) throws IOException {
+        return new GetIndexTemplateV2Action.Response(in);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(GetIndexTemplateV2Action.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
+    }
+
+    @Override
+    protected void masterOperation(Task task, GetIndexTemplateV2Action.Request request, ClusterState state,
+                                   ActionListener<GetIndexTemplateV2Action.Response> listener) {
+        Map<String, IndexTemplateV2> allTemplates = state.metaData().templatesV2();
+
+        // If we did not ask for a specific name, then we return all templates
+        if (request.names().length == 0) {
+            listener.onResponse(new GetIndexTemplateV2Action.Response(allTemplates));
+            return;
+        }
+
+        final Map<String, IndexTemplateV2> results = new HashMap<>();
+        for (String name : request.names()) {
+            if (Regex.isSimpleMatchPattern(name)) {
+                for (Map.Entry<String, IndexTemplateV2> entry : allTemplates.entrySet()) {
+                    if (Regex.simpleMatch(name, entry.getKey())) {
+                        results.put(entry.getKey(), entry.getValue());
+                    }
+                }
+            } else if (allTemplates.containsKey(name)) {
+                results.put(name, allTemplates.get(name));
+            }
+        }
+
+        listener.onResponse(new GetIndexTemplateV2Action.Response(results));
+    }
+}

+ 181 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateV2Action.java

@@ -0,0 +1,181 @@
+/*
+ * 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.put;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.IndicesRequest;
+import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+import org.elasticsearch.cluster.metadata.IndexTemplateV2;
+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;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+public class PutIndexTemplateV2Action extends ActionType<AcknowledgedResponse> {
+
+    public static final PutIndexTemplateV2Action INSTANCE = new PutIndexTemplateV2Action();
+    public static final String NAME = "indices:admin/index_template/put";
+
+    private PutIndexTemplateV2Action() {
+        super(NAME, AcknowledgedResponse::new);
+    }
+
+    /**
+     * A request for putting a single index template into the cluster state
+     */
+    public static class Request extends MasterNodeRequest<Request> implements IndicesRequest {
+        private final String name;
+        @Nullable
+        private String cause;
+        private boolean create;
+        private IndexTemplateV2 indexTemplate;
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+            this.name = in.readString();
+            this.cause = in.readOptionalString();
+            this.create = in.readBoolean();
+            this.indexTemplate = new IndexTemplateV2(in);
+        }
+
+        /**
+         * Constructs a new put index template request with the provided name.
+         */
+        public Request(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            out.writeString(name);
+            out.writeOptionalString(cause);
+            out.writeBoolean(create);
+            this.indexTemplate.writeTo(out);
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            ActionRequestValidationException validationException = null;
+            if (name == null || Strings.hasText(name) == false) {
+                validationException = addValidationError("name is missing", validationException);
+            }
+            if (indexTemplate == null) {
+                validationException = addValidationError("an index template is required", validationException);
+            }
+            return validationException;
+        }
+
+        /**
+         * The name of the index template.
+         */
+        public String name() {
+            return this.name;
+        }
+
+        /**
+         * Set to {@code true} to force only creation, not an update of an index template. If it already
+         * exists, it will fail with an {@link IllegalArgumentException}.
+         */
+        public Request create(boolean create) {
+            this.create = create;
+            return this;
+        }
+
+        public boolean create() {
+            return create;
+        }
+
+        /**
+         * The cause for this index template creation.
+         */
+        public Request cause(@Nullable String cause) {
+            this.cause = cause;
+            return this;
+        }
+
+        @Nullable
+        public String cause() {
+            return this.cause;
+        }
+
+        /**
+         * The index template that will be inserted into the cluster state
+         */
+        public Request indexTemplate(IndexTemplateV2 template) {
+            this.indexTemplate = template;
+            return this;
+        }
+
+        public IndexTemplateV2 indexTemplate() {
+            return this.indexTemplate;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder("PutTemplateV2Request[");
+            sb.append("name=").append(name);
+            sb.append(", cause=").append(cause);
+            sb.append(", create=").append(create);
+            sb.append(", index_template=").append(indexTemplate);
+            sb.append("]");
+            return sb.toString();
+        }
+
+        @Override
+        public String[] indices() {
+            return indexTemplate.indexPatterns().toArray(Strings.EMPTY_ARRAY);
+        }
+
+        @Override
+        public IndicesOptions indicesOptions() {
+            return IndicesOptions.strictExpand();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(name, cause, create, indexTemplate);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            Request other = (Request) obj;
+            return Objects.equals(this.name, other.name) &&
+                Objects.equals(this.cause, other.cause) &&
+                Objects.equals(this.indexTemplate, other.indexTemplate) &&
+                this.create == other.create;
+        }
+    }
+
+}

+ 89 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateV2Action.java

@@ -0,0 +1,89 @@
+/*
+ * 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.put;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.metadata.IndexTemplateV2;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService;
+import org.elasticsearch.cluster.metadata.Template;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.io.IOException;
+
+public class TransportPutIndexTemplateV2Action
+    extends TransportMasterNodeAction<PutIndexTemplateV2Action.Request, AcknowledgedResponse> {
+
+    private final MetaDataIndexTemplateService indexTemplateService;
+
+    @Inject
+    public TransportPutIndexTemplateV2Action(TransportService transportService, ClusterService clusterService,
+                                               ThreadPool threadPool, MetaDataIndexTemplateService indexTemplateService,
+                                               ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(PutIndexTemplateV2Action.NAME, transportService, clusterService, threadPool, actionFilters,
+            PutIndexTemplateV2Action.Request::new, indexNameExpressionResolver);
+        this.indexTemplateService = indexTemplateService;
+    }
+
+    @Override
+    protected String executor() {
+        // we go async right away
+        return ThreadPool.Names.SAME;
+    }
+
+    @Override
+    protected AcknowledgedResponse read(StreamInput in) throws IOException {
+        return new AcknowledgedResponse(in);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(PutIndexTemplateV2Action.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
+    }
+
+    @Override
+    protected void masterOperation(Task task, final PutIndexTemplateV2Action.Request request, final ClusterState state,
+                                   final ActionListener<AcknowledgedResponse> listener) {
+        IndexTemplateV2 indexTemplate = request.indexTemplate();
+        Template template = indexTemplate.template();
+        // Normalize the index settings if necessary
+        if (template.settings() != null) {
+            Settings.Builder settings = Settings.builder().put(template.settings()).normalizePrefix(IndexMetaData.INDEX_SETTING_PREFIX);
+            template = new Template(settings.build(), template.mappings(), template.aliases());
+            indexTemplate = new IndexTemplateV2(indexTemplate.indexPatterns(), template, indexTemplate.composedOf(),
+                indexTemplate.priority(), indexTemplate.version(), indexTemplate.metadata());
+        }
+        indexTemplateService.putIndexTemplateV2(request.cause(), request.create(), request.name(), request.masterNodeTimeout(),
+            indexTemplate, listener);
+    }
+}

+ 102 - 0
server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java

@@ -234,6 +234,108 @@ public class MetaDataIndexTemplateService {
             });
     }
 
+    /**
+     * Add the given component template to the cluster state. If {@code create} is true, an
+     * exception will be thrown if the component template already exists
+     */
+    public void putIndexTemplateV2(final String cause, final boolean create, final String name, final TimeValue masterTimeout,
+                                   final IndexTemplateV2 template, final ActionListener<AcknowledgedResponse> listener) {
+        clusterService.submitStateUpdateTask("create-index-template-v2 [" + name + "], cause [" + cause + "]",
+            new ClusterStateUpdateTask(Priority.URGENT) {
+
+                @Override
+                public TimeValue timeout() {
+                    return masterTimeout;
+                }
+
+                @Override
+                public void onFailure(String source, Exception e) {
+                    listener.onFailure(e);
+                }
+
+                @Override
+                public ClusterState execute(ClusterState currentState) {
+                    return addIndexTemplateV2(currentState, create, name, template);
+                }
+
+                @Override
+                public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+                    listener.onResponse(new AcknowledgedResponse(true));
+                }
+            });
+    }
+
+    // Package visible for testing
+    static ClusterState addIndexTemplateV2(final ClusterState currentState, final boolean create,
+                                           final String name, final IndexTemplateV2 template) {
+        if (create && currentState.metaData().templatesV2().containsKey(name)) {
+            throw new IllegalArgumentException("index template [" + name + "] already exists");
+        }
+
+        // TODO: validation of index template
+        // validateAndAddTemplate(request, templateBuilder, indicesService, xContentRegistry);
+
+        logger.info("adding index template [{}]", name);
+        return ClusterState.builder(currentState)
+            .metaData(MetaData.builder(currentState.metaData()).put(name, template))
+            .build();
+    }
+
+    /**
+     * Remove the given index template from the cluster state. The index template name
+     * supports simple regex wildcards for removing multiple index templates at a time.
+     */
+    public void removeIndexTemplateV2(final String name, final TimeValue masterTimeout,
+                                      final ActionListener<AcknowledgedResponse> listener) {
+        clusterService.submitStateUpdateTask("remove-index-template-v2 [" + name + "]",
+            new ClusterStateUpdateTask(Priority.URGENT) {
+
+                @Override
+                public TimeValue timeout() {
+                    return masterTimeout;
+                }
+
+                @Override
+                public void onFailure(String source, Exception e) {
+                    listener.onFailure(e);
+                }
+
+                @Override
+                public ClusterState execute(ClusterState currentState) {
+                    return innerRemoveIndexTemplateV2(currentState, name);
+                }
+
+                @Override
+                public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+                    listener.onResponse(new AcknowledgedResponse(true));
+                }
+            });
+    }
+
+    // Package visible for testing
+    static ClusterState innerRemoveIndexTemplateV2(ClusterState currentState, String name) {
+        Set<String> templateNames = new HashSet<>();
+        for (String templateName : currentState.metaData().templatesV2().keySet()) {
+            if (Regex.simpleMatch(name, templateName)) {
+                templateNames.add(templateName);
+            }
+        }
+        if (templateNames.isEmpty()) {
+            // if its a match all pattern, and no templates are found (we have none), don't
+            // fail with index missing...
+            if (Regex.isMatchAllPattern(name)) {
+                return currentState;
+            }
+            throw new IndexTemplateMissingException(name);
+        }
+        MetaData.Builder metaData = MetaData.builder(currentState.metaData());
+        for (String templateName : templateNames) {
+            logger.info("removing index template [{}]", templateName);
+            metaData.removeIndexTemplate(templateName);
+        }
+        return ClusterState.builder(currentState).metaData(metaData).build();
+    }
+
     public void putTemplate(final PutRequest request, final PutListener listener) {
         Settings.Builder updatedSettingsBuilder = Settings.builder();
         updatedSettingsBuilder.put(request.settings).normalizePrefix(IndexMetaData.INDEX_SETTING_PREFIX);

+ 53 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestDeleteIndexTemplateV2Action.java

@@ -0,0 +1,53 @@
+/*
+ * 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.delete.DeleteIndexTemplateV2Action;
+import org.elasticsearch.client.node.NodeClient;
+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.DELETE;
+
+public class RestDeleteIndexTemplateV2Action extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(new Route(DELETE, "/_index_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "delete_index_template_v2_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+
+        DeleteIndexTemplateV2Action.Request deleteReq = new DeleteIndexTemplateV2Action.Request(request.param("name"));
+        deleteReq.masterNodeTimeout(request.paramAsTime("master_timeout", deleteReq.masterNodeTimeout()));
+
+        return channel -> client.execute(DeleteIndexTemplateV2Action.INSTANCE, deleteReq, new RestToXContentListener<>(channel));
+    }
+}

+ 81 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndexTemplateV2Action.java

@@ -0,0 +1,81 @@
+/*
+ * 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.get.GetIndexTemplateV2Action;
+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.RestRequest;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.rest.action.RestToXContentListener;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import static org.elasticsearch.rest.RestRequest.Method.GET;
+import static org.elasticsearch.rest.RestRequest.Method.HEAD;
+import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
+import static org.elasticsearch.rest.RestStatus.OK;
+
+public class RestGetIndexTemplateV2Action extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(
+            new Route(GET, "/_index_template"),
+            new Route(GET, "/_index_template/{name}"),
+            new Route(HEAD, "/_index_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "get_index_template_v2_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+        final String[] names = Strings.splitStringByCommaToArray(request.param("name"));
+
+        final GetIndexTemplateV2Action.Request getRequest = new GetIndexTemplateV2Action.Request(names);
+
+        getRequest.local(request.paramAsBoolean("local", getRequest.local()));
+        getRequest.masterNodeTimeout(request.paramAsTime("master_timeout", getRequest.masterNodeTimeout()));
+
+        final boolean implicitAll = getRequest.names().length == 0;
+
+        return channel ->
+            client.execute(GetIndexTemplateV2Action.INSTANCE, getRequest, new RestToXContentListener<>(channel) {
+                @Override
+                protected RestStatus getStatus(final GetIndexTemplateV2Action.Response response) {
+                    final boolean templateExists = response.indexTemplates().isEmpty() == false;
+                    return (templateExists || implicitAll) ? OK : NOT_FOUND;
+                }
+            });
+    }
+
+    @Override
+    protected Set<String> responseParams() {
+        return Settings.FORMAT_PARAMS;
+    }
+
+}

+ 60 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutIndexTemplateV2Action.java

@@ -0,0 +1,60 @@
+/*
+ * 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.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;
+import static org.elasticsearch.rest.RestRequest.Method.PUT;
+
+public class RestPutIndexTemplateV2Action extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(
+            new Route(POST, "/_index_template/{name}"),
+            new Route(PUT, "/_index_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "put_index_template_v2_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+
+        PutIndexTemplateV2Action.Request putRequest = new PutIndexTemplateV2Action.Request(request.param("name"));
+        putRequest.masterNodeTimeout(request.paramAsTime("master_timeout", putRequest.masterNodeTimeout()));
+        putRequest.create(request.paramAsBoolean("create", false));
+        putRequest.cause(request.param("cause", "api"));
+        putRequest.indexTemplate(IndexTemplateV2.parse(request.contentParser()));
+
+        return channel -> client.execute(PutIndexTemplateV2Action.INSTANCE, putRequest, new RestToXContentListener<>(channel));
+    }
+}

+ 42 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/template/delete/DeleteIndexTemplateV2RequestTests.java

@@ -0,0 +1,42 @@
+/*
+ * 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.delete;
+
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+
+public class DeleteIndexTemplateV2RequestTests extends AbstractWireSerializingTestCase<DeleteIndexTemplateV2Action.Request> {
+    @Override
+    protected Writeable.Reader<DeleteIndexTemplateV2Action.Request> instanceReader() {
+        return DeleteIndexTemplateV2Action.Request::new;
+    }
+
+    @Override
+    protected DeleteIndexTemplateV2Action.Request createTestInstance() {
+        return new DeleteIndexTemplateV2Action.Request(randomAlphaOfLength(5));
+    }
+
+    @Override
+    protected DeleteIndexTemplateV2Action.Request mutateInstance(DeleteIndexTemplateV2Action.Request instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+}

+ 42 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2RequestTests.java

@@ -0,0 +1,42 @@
+/*
+ * 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.get;
+
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+
+public class GetIndexTemplateV2RequestTests extends AbstractWireSerializingTestCase<GetIndexTemplateV2Action.Request> {
+    @Override
+    protected Writeable.Reader<GetIndexTemplateV2Action.Request> instanceReader() {
+        return GetIndexTemplateV2Action.Request::new;
+    }
+
+    @Override
+    protected GetIndexTemplateV2Action.Request createTestInstance() {
+        return new GetIndexTemplateV2Action.Request(generateRandomStringArray(5, 5, false, false));
+    }
+
+    @Override
+    protected GetIndexTemplateV2Action.Request mutateInstance(GetIndexTemplateV2Action.Request instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+}

+ 54 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplateV2ResponseTests.java

@@ -0,0 +1,54 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.template.get;
+
+import org.elasticsearch.cluster.metadata.IndexTemplateV2;
+import org.elasticsearch.cluster.metadata.IndexTemplateV2Tests;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GetIndexTemplateV2ResponseTests extends AbstractWireSerializingTestCase<GetIndexTemplateV2Action.Response> {
+    @Override
+    protected Writeable.Reader<GetIndexTemplateV2Action.Response> instanceReader() {
+        return GetIndexTemplateV2Action.Response::new;
+    }
+
+    @Override
+    protected GetIndexTemplateV2Action.Response createTestInstance() {
+        if (randomBoolean()) {
+            return new GetIndexTemplateV2Action.Response(Collections.emptyMap());
+        }
+        Map<String, IndexTemplateV2> templates = new HashMap<>();
+        for (int i = 0; i < randomIntBetween(1, 4); i++) {
+            templates.put(randomAlphaOfLength(4), IndexTemplateV2Tests.randomInstance());
+        }
+        return new GetIndexTemplateV2Action.Response(templates);
+    }
+
+    @Override
+    protected GetIndexTemplateV2Action.Response mutateInstance(GetIndexTemplateV2Action.Response instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+}

+ 47 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateV2RequestTests.java

@@ -0,0 +1,47 @@
+/*
+ * 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.put;
+
+import org.elasticsearch.cluster.metadata.IndexTemplateV2Tests;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+
+public class PutIndexTemplateV2RequestTests extends AbstractWireSerializingTestCase<PutIndexTemplateV2Action.Request> {
+    @Override
+    protected Writeable.Reader<PutIndexTemplateV2Action.Request> instanceReader() {
+        return PutIndexTemplateV2Action.Request::new;
+    }
+
+    @Override
+    protected PutIndexTemplateV2Action.Request createTestInstance() {
+        PutIndexTemplateV2Action.Request req = new PutIndexTemplateV2Action.Request(randomAlphaOfLength(4));
+        req.cause(randomAlphaOfLength(4));
+        req.create(randomBoolean());
+        req.indexTemplate(IndexTemplateV2Tests.randomInstance());
+        return req;
+    }
+
+    @Override
+    protected PutIndexTemplateV2Action.Request mutateInstance(PutIndexTemplateV2Action.Request instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+}

+ 32 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateServiceTests.java

@@ -32,6 +32,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.index.mapper.MapperParsingException;
+import org.elasticsearch.indices.IndexTemplateMissingException;
 import org.elasticsearch.indices.IndicesService;
 import org.elasticsearch.indices.InvalidIndexTemplateException;
 import org.elasticsearch.test.ESSingleNodeTestCase;
@@ -237,6 +238,37 @@ public class MetaDataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             () -> metaDataIndexTemplateService.addComponentTemplate(throwState, true, "foo2", componentTemplate4));
     }
 
+    public void testAddIndexTemplateV2() {
+        ClusterState state = ClusterState.EMPTY_STATE;
+        IndexTemplateV2 template = IndexTemplateV2Tests.randomInstance();
+        state = MetaDataIndexTemplateService.addIndexTemplateV2(state, false, "foo", template);
+
+        assertNotNull(state.metaData().templatesV2().get("foo"));
+        assertThat(state.metaData().templatesV2().get("foo"), equalTo(template));
+
+        final ClusterState throwState = ClusterState.builder(state).build();
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
+            () -> MetaDataIndexTemplateService.addIndexTemplateV2(throwState, true, "foo", template));
+        assertThat(e.getMessage(), containsString("index template [foo] already exists"));
+
+        state = MetaDataIndexTemplateService.addIndexTemplateV2(state, randomBoolean(), "bar", template);
+        assertNotNull(state.metaData().templatesV2().get("bar"));
+    }
+
+    public void testRemoveIndexTemplateV2() {
+        IndexTemplateV2 template = IndexTemplateV2Tests.randomInstance();
+        IndexTemplateMissingException e = expectThrows(IndexTemplateMissingException.class,
+            () -> MetaDataIndexTemplateService.innerRemoveIndexTemplateV2(ClusterState.EMPTY_STATE, "foo"));
+        assertThat(e.getMessage(), equalTo("index_template [foo] missing"));
+
+        final ClusterState state = MetaDataIndexTemplateService.addIndexTemplateV2(ClusterState.EMPTY_STATE, false, "foo", template);
+        assertNotNull(state.metaData().templatesV2().get("foo"));
+        assertThat(state.metaData().templatesV2().get("foo"), equalTo(template));
+
+        ClusterState updatedState = MetaDataIndexTemplateService.innerRemoveIndexTemplateV2(state, "foo");
+        assertNull(updatedState.metaData().templatesV2().get("foo"));
+    }
+
     private static List<Throwable> putTemplate(NamedXContentRegistry xContentRegistry, PutRequest request) {
         MetaDataCreateIndexService createIndexService = new MetaDataCreateIndexService(
                 Settings.EMPTY,

+ 6 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

@@ -55,13 +55,15 @@ public class ClusterPrivilegeResolver {
     private static final Set<String> MONITOR_TRANSFORM_PATTERN = Set.of("cluster:monitor/data_frame/*", "cluster:monitor/transform/*");
     private static final Set<String> MONITOR_WATCHER_PATTERN = Set.of("cluster:monitor/xpack/watcher/*");
     private static final Set<String> MONITOR_ROLLUP_PATTERN = Set.of("cluster:monitor/xpack/rollup/*");
-    private static final Set<String> ALL_CLUSTER_PATTERN = Set.of("cluster:*", "indices:admin/template/*", "indices:admin/data_stream/*");
+    private static final Set<String> ALL_CLUSTER_PATTERN = Set.of("cluster:*", "indices:admin/template/*", "indices:admin/index_template/*",
+        "indices:admin/data_stream/*");
     private static final Set<String> MANAGE_ML_PATTERN = Set.of("cluster:admin/xpack/ml/*", "cluster:monitor/xpack/ml/*");
     private static final Set<String> MANAGE_TRANSFORM_PATTERN = Set.of("cluster:admin/data_frame/*", "cluster:monitor/data_frame/*",
             "cluster:monitor/transform/*", "cluster:admin/transform/*");
     private static final Set<String> MANAGE_WATCHER_PATTERN = Set.of("cluster:admin/xpack/watcher/*", "cluster:monitor/xpack/watcher/*");
     private static final Set<String> TRANSPORT_CLIENT_PATTERN = Set.of("cluster:monitor/nodes/liveness", "cluster:monitor/state");
-    private static final Set<String> MANAGE_IDX_TEMPLATE_PATTERN = Set.of("indices:admin/template/*");
+    private static final Set<String> MANAGE_IDX_TEMPLATE_PATTERN = Set.of("indices:admin/template/*", "indices:admin/index_template/*",
+        "cluster:admin/component_template/*");
     private static final Set<String> MANAGE_INGEST_PIPELINE_PATTERN = Set.of("cluster:admin/ingest/pipeline/*");
     private static final Set<String> MANAGE_ROLLUP_PATTERN = Set.of("cluster:admin/xpack/rollup/*", "cluster:monitor/xpack/rollup/*");
     private static final Set<String> MANAGE_CCR_PATTERN =
@@ -200,7 +202,8 @@ public class ClusterPrivilegeResolver {
         return actionName.startsWith("cluster:") ||
             actionName.startsWith("indices:admin/template/") ||
             // todo: hack until we implement security of data_streams
-            actionName.startsWith("indices:admin/data_stream/");
+            actionName.startsWith("indices:admin/data_stream/") ||
+            actionName.startsWith("indices:admin/index_template/");
     }
 
     private static String actionToPattern(String text) {