1
0
Эх сурвалжийг харах

Add REST API for ComponentTemplate CRUD (#53558)

* Add REST API for ComponentTemplate CRUD

This adds the Put/Get/DeleteComponentTemplate APIs that allow inserting, retrieving, and removing
ComponentTemplateMetadata into the cluster state metadata.

These APIs are currently only available behind a feature flag system property -
`es.itv2_feature_flag_registered`.

Relates to #53101

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Lee Hinman 5 жил өмнө
parent
commit
263e525e49
23 өөрчлөгдсөн 1452 нэмэгдсэн , 164 устгасан
  1. 4 1
      client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
  2. 7 0
      distribution/archives/integ-test-zip/build.gradle
  3. 35 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cluster.delete_component_template.json
  4. 41 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cluster.get_component_template.json
  5. 45 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cluster.put_component_template.json
  6. 47 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cluster.component_template/10_basic.yml
  7. 37 0
      server/src/main/java/org/elasticsearch/action/ActionModule.java
  8. 90 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/DeleteComponentTemplateAction.java
  9. 79 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteComponentTemplateAction.java
  10. 171 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetComponentTemplateAction.java
  11. 94 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/get/TransportGetComponentTemplateAction.java
  12. 151 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutComponentTemplateAction.java
  13. 88 0
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java
  14. 1 148
      server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplate.java
  15. 100 0
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java
  16. 187 0
      server/src/main/java/org/elasticsearch/cluster/metadata/Template.java
  17. 53 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestDeleteComponentTemplateAction.java
  18. 81 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetComponentTemplateAction.java
  19. 60 0
      server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutComponentTemplateAction.java
  20. 54 0
      server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetComponentTemplateResponseTests.java
  21. 7 7
      server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java
  22. 18 6
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateServiceTests.java
  23. 2 2
      server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java

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

@@ -790,7 +790,10 @@ public class RestHighLevelClientTests extends ESTestCase {
             "indices.get_upgrade",
             "indices.put_alias",
             "render_search_template",
-            "scripts_painless_execute"
+            "scripts_painless_execute",
+            "cluster.put_component_template",
+            "cluster.get_component_template",
+            "cluster.delete_component_template"
         };
         //These API are not required for high-level client feature completeness
         String[] notRequiredApi = new String[] {

+ 7 - 0
distribution/archives/integ-test-zip/build.gradle

@@ -1,3 +1,4 @@
+import org.elasticsearch.gradle.info.BuildParams
 /*
  * Licensed to Elasticsearch under one or more contributor
  * license agreements. See the NOTICE file distributed with
@@ -32,3 +33,9 @@ integTest.runner {
     systemProperty 'tests.logfile', '--external--'
   }
 }
+
+testClusters.integTest {
+  if (BuildParams.isSnapshotBuild() == false) {
+    systemProperty 'es.itv2_feature_flag_registered', 'true'
+  }
+}

+ 35 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cluster.delete_component_template.json

@@ -0,0 +1,35 @@
+{
+  "cluster.delete_component_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-templates.html",
+      "description":"Deletes a component template"
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_component_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"
+      }
+    }
+  }
+}

+ 41 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cluster.get_component_template.json

@@ -0,0 +1,41 @@
+{
+  "cluster.get_component_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-templates.html",
+      "description":"Returns one or more component templates"
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_component_template",
+          "methods":[
+            "GET"
+          ]
+        },
+        {
+          "path":"/_component_template/{name}",
+          "methods":[
+            "GET"
+          ],
+          "parts":{
+            "name":{
+              "type":"list",
+              "description":"The comma separated names of the component templates"
+            }
+          }
+        }
+      ]
+    },
+    "params":{
+      "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/cluster.put_component_template.json

@@ -0,0 +1,45 @@
+{
+  "cluster.put_component_template":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-templates.html",
+      "description":"Creates or updates a component template"
+    },
+    "stability":"stable",
+    "url":{
+      "paths":[
+        {
+          "path":"/_component_template/{name}",
+          "methods":[
+            "PUT",
+            "POST"
+          ],
+          "parts":{
+            "name":{
+              "type":"string",
+              "description":"The name of the template"
+            }
+          }
+        }
+      ]
+    },
+    "params":{
+      "create":{
+        "type":"boolean",
+        "description":"Whether the index template should only be added if new or can also replace an existing one",
+        "default":false
+      },
+      "timeout":{
+        "type":"time",
+        "description":"Explicit operation timeout"
+      },
+      "master_timeout":{
+        "type":"time",
+        "description":"Specify timeout for connection to master"
+      }
+    },
+    "body":{
+      "description":"The template definition",
+      "required":true
+    }
+  }
+}

+ 47 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cluster.component_template/10_basic.yml

@@ -0,0 +1,47 @@
+---
+"Basic CRUD":
+  - skip:
+      version: " - 7.99.99"
+      reason: not backported yet
+
+  - do:
+      cluster.put_component_template:
+        name: test
+        body:
+          template:
+            settings:
+              number_of_shards:   1
+              number_of_replicas: 0
+            mappings:
+              properties:
+                field:
+                  type: keyword
+            aliases:
+              aliasname: {}
+          version: 2
+          _meta:
+            foo: bar
+            baz:
+              eggplant: true
+
+  - do:
+      cluster.get_component_template:
+        name: test
+
+  - match: {component_templates.0.name: test}
+  - match: {component_templates.0.component_template.version: 2}
+  - match: {component_templates.0.component_template._meta: {foo: bar, baz: {eggplant: true}}}
+  - match: {component_templates.0.component_template.template.settings: {index: {number_of_shards: '1', number_of_replicas: '0'}}}
+  - match: {component_templates.0.component_template.template.mappings: {properties: {field: {type: keyword}}}}
+  - match: {component_templates.0.component_template.template.aliases: {aliasname: {}}}
+
+  - do:
+      cluster.delete_component_template:
+        name: test
+
+  - do:
+      catch: missing
+      cluster.get_component_template:
+        name: test
+
+  - is_false: test

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

@@ -21,6 +21,7 @@ package org.elasticsearch.action;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.elasticsearch.Build;
 import org.elasticsearch.action.admin.cluster.allocation.ClusterAllocationExplainAction;
 import org.elasticsearch.action.admin.cluster.allocation.TransportClusterAllocationExplainAction;
 import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction;
@@ -142,11 +143,17 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeAction;
 import org.elasticsearch.action.admin.indices.shrink.TransportResizeAction;
 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.TransportDeleteComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.delete.TransportDeleteIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.get.GetComponentTemplateAction;
 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.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.TransportPutComponentTemplateAction;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutIndexTemplateAction;
 import org.elasticsearch.action.admin.indices.upgrade.get.TransportUpgradeStatusAction;
 import org.elasticsearch.action.admin.indices.upgrade.get.UpgradeStatusAction;
@@ -269,11 +276,13 @@ 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.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.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.RestGetIndicesAction;
@@ -286,6 +295,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.RestPutComponentTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.RestPutMappingAction;
 import org.elasticsearch.rest.action.admin.indices.RestRecoveryAction;
@@ -361,6 +371,23 @@ public class ActionModule extends AbstractModule {
 
     private static final Logger logger = LogManager.getLogger(ActionModule.class);
 
+    private static final boolean ITV2_FEATURE_FLAG_REGISTERED;
+
+    static {
+        final String property = System.getProperty("es.itv2_feature_flag_registered");
+        if (Build.CURRENT.isSnapshot() && property != null) {
+            throw new IllegalArgumentException("es.itv2_feature_flag_registered is only supported in non-snapshot builds");
+        }
+        if (Build.CURRENT.isSnapshot() || "true".equals(property)) {
+            ITV2_FEATURE_FLAG_REGISTERED = true;
+        } else if ("false".equals(property) || property == null) {
+            ITV2_FEATURE_FLAG_REGISTERED = false;
+        } else {
+            throw new IllegalArgumentException("expected es.itv2_feature_flag_registered to be unset, true, or false but was [" +
+                property + "]");
+        }
+    }
+
     private final Settings settings;
     private final IndexNameExpressionResolver indexNameExpressionResolver;
     private final IndexScopedSettings indexScopedSettings;
@@ -486,6 +513,11 @@ public class ActionModule extends AbstractModule {
         actions.register(PutIndexTemplateAction.INSTANCE, TransportPutIndexTemplateAction.class);
         actions.register(GetIndexTemplatesAction.INSTANCE, TransportGetIndexTemplatesAction.class);
         actions.register(DeleteIndexTemplateAction.INSTANCE, TransportDeleteIndexTemplateAction.class);
+        if (ITV2_FEATURE_FLAG_REGISTERED) {
+            actions.register(PutComponentTemplateAction.INSTANCE, TransportPutComponentTemplateAction.class);
+            actions.register(GetComponentTemplateAction.INSTANCE, TransportGetComponentTemplateAction.class);
+            actions.register(DeleteComponentTemplateAction.INSTANCE, TransportDeleteComponentTemplateAction.class);
+        }
         actions.register(ValidateQueryAction.INSTANCE, TransportValidateQueryAction.class);
         actions.register(RefreshAction.INSTANCE, TransportRefreshAction.class);
         actions.register(FlushAction.INSTANCE, TransportFlushAction.class);
@@ -621,6 +653,11 @@ public class ActionModule extends AbstractModule {
         registerHandler.accept(new RestGetIndexTemplateAction());
         registerHandler.accept(new RestPutIndexTemplateAction());
         registerHandler.accept(new RestDeleteIndexTemplateAction());
+        if (ITV2_FEATURE_FLAG_REGISTERED) {
+            registerHandler.accept(new RestPutComponentTemplateAction());
+            registerHandler.accept(new RestGetComponentTemplateAction());
+            registerHandler.accept(new RestDeleteComponentTemplateAction());
+        }
 
         registerHandler.accept(new RestPutMappingAction());
         registerHandler.accept(new RestGetMappingAction());

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

@@ -0,0 +1,90 @@
+/*
+ * 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 static org.elasticsearch.action.ValidateActions.addValidationError;
+
+public class DeleteComponentTemplateAction extends ActionType<AcknowledgedResponse> {
+
+    public static final DeleteComponentTemplateAction INSTANCE = new DeleteComponentTemplateAction();
+    public static final String NAME = "cluster:admin/component_template/delete";
+
+    private DeleteComponentTemplateAction() {
+        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 index 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);
+        }
+    }
+}

+ 79 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteComponentTemplateAction.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 TransportDeleteComponentTemplateAction
+    extends TransportMasterNodeAction<DeleteComponentTemplateAction.Request, AcknowledgedResponse> {
+
+    private static final Logger logger = LogManager.getLogger(TransportDeleteComponentTemplateAction.class);
+
+    private final MetaDataIndexTemplateService indexTemplateService;
+
+    @Inject
+    public TransportDeleteComponentTemplateAction(TransportService transportService, ClusterService clusterService,
+                                                  ThreadPool threadPool, MetaDataIndexTemplateService indexTemplateService,
+                                                  ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(DeleteComponentTemplateAction.NAME, transportService, clusterService, threadPool, actionFilters,
+            DeleteComponentTemplateAction.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(DeleteComponentTemplateAction.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
+    }
+
+    @Override
+    protected void masterOperation(Task task, final DeleteComponentTemplateAction.Request request, final ClusterState state,
+                                   final ActionListener<AcknowledgedResponse> listener) {
+        indexTemplateService.removeComponentTemplate(request.name(), request.masterNodeTimeout(), listener);
+    }
+}

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

@@ -0,0 +1,171 @@
+/*
+ * 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.ComponentTemplate;
+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.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+/**
+ * Action to retrieve one or more component templates
+ */
+public class GetComponentTemplateAction extends ActionType<GetComponentTemplateAction.Response> {
+
+    public static final GetComponentTemplateAction INSTANCE = new GetComponentTemplateAction();
+    public static final String NAME = "cluster:admin/component_template/get";
+
+    private GetComponentTemplateAction() {
+        super(NAME, GetComponentTemplateAction.Response::new);
+    }
+
+    /**
+     * Request that to retrieve one or more component 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 component templates.
+         */
+        public Request names(String... names) {
+            this.names = names;
+            return this;
+        }
+
+        /**
+         * The names of the component templates.
+         */
+        public String[] names() {
+            return this.names;
+        }
+    }
+
+    public static class Response extends ActionResponse implements ToXContentObject {
+        public static final ParseField NAME = new ParseField("name");
+        public static final ParseField COMPONENT_TEMPLATES = new ParseField("component_templates");
+        public static final ParseField COMPONENT_TEMPLATE = new ParseField("component_template");
+
+        private final Map<String, ComponentTemplate> componentTemplates;
+
+        public Response(StreamInput in) throws IOException {
+            super(in);
+            int size = in.readVInt();
+            componentTemplates = new HashMap<>();
+            for (int i = 0 ; i < size ; i++) {
+                componentTemplates.put(in.readString(), new ComponentTemplate(in));
+            }
+        }
+
+        public Response(Map<String, ComponentTemplate> componentTemplates) {
+            this.componentTemplates = componentTemplates;
+        }
+
+        public Map<String, ComponentTemplate> getComponentTemplates() {
+            return componentTemplates;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeVInt(componentTemplates.size());
+            for (Map.Entry<String, ComponentTemplate> componentTemplate : componentTemplates.entrySet()) {
+                out.writeString(componentTemplate.getKey());
+                componentTemplate.getValue().writeTo(out);
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Response that = (Response) o;
+            return Objects.equals(componentTemplates, that.componentTemplates);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(componentTemplates);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.startArray(COMPONENT_TEMPLATES.getPreferredName());
+            for (Map.Entry<String, ComponentTemplate> componentTemplate : this.componentTemplates.entrySet()) {
+                builder.startObject();
+                builder.field(NAME.getPreferredName(), componentTemplate.getKey());
+                builder.field(COMPONENT_TEMPLATE.getPreferredName(), componentTemplate.getValue());
+                builder.endObject();
+            }
+            builder.endArray();
+            builder.endObject();
+            return builder;
+        }
+
+    }
+
+}

+ 94 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/template/get/TransportGetComponentTemplateAction.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.ComponentTemplate;
+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 TransportGetComponentTemplateAction extends
+    TransportMasterNodeReadAction<GetComponentTemplateAction.Request, GetComponentTemplateAction.Response> {
+
+    @Inject
+    public TransportGetComponentTemplateAction(TransportService transportService, ClusterService clusterService,
+                                               ThreadPool threadPool, ActionFilters actionFilters,
+                                               IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(GetComponentTemplateAction.NAME, transportService, clusterService, threadPool, actionFilters,
+            GetComponentTemplateAction.Request::new, indexNameExpressionResolver);
+    }
+
+    @Override
+    protected String executor() {
+        return ThreadPool.Names.SAME;
+    }
+
+    @Override
+    protected GetComponentTemplateAction.Response read(StreamInput in) throws IOException {
+        return new GetComponentTemplateAction.Response(in);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(GetComponentTemplateAction.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
+    }
+
+    @Override
+    protected void masterOperation(Task task, GetComponentTemplateAction.Request request, ClusterState state,
+                                   ActionListener<GetComponentTemplateAction.Response> listener) {
+        Map<String, ComponentTemplate> allTemplates = state.metaData().componentTemplates();
+
+        // If we did not ask for a specific name, then we return all templates
+        if (request.names().length == 0) {
+            listener.onResponse(new GetComponentTemplateAction.Response(allTemplates));
+            return;
+        }
+
+        final Map<String, ComponentTemplate> results = new HashMap<>();
+        for (String name : request.names()) {
+            if (Regex.isSimpleMatchPattern(name)) {
+                for (Map.Entry<String, ComponentTemplate> 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 GetComponentTemplateAction.Response(results));
+    }
+}

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

@@ -0,0 +1,151 @@
+/*
+ * 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.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+import org.elasticsearch.cluster.metadata.ComponentTemplate;
+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 static org.elasticsearch.action.ValidateActions.addValidationError;
+
+/**
+ * An action for putting a single component template into the cluster state
+ */
+public class PutComponentTemplateAction extends ActionType<AcknowledgedResponse> {
+
+    public static final PutComponentTemplateAction INSTANCE = new PutComponentTemplateAction();
+    public static final String NAME = "cluster:admin/component_template/put";
+
+    private PutComponentTemplateAction() {
+        super(NAME, AcknowledgedResponse::new);
+    }
+
+    /**
+     * A request for putting a single component template into the cluster state
+     */
+    public static class Request extends MasterNodeRequest<Request> {
+        private final String name;
+        @Nullable
+        private String cause;
+        private boolean create;
+        private ComponentTemplate componentTemplate;
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+            this.name = in.readString();
+            this.cause = in.readOptionalString();
+            this.create = in.readBoolean();
+            this.componentTemplate = new ComponentTemplate(in);
+        }
+
+        /**
+         * Constructs a new put component 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.componentTemplate.writeTo(out);
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            ActionRequestValidationException validationException = null;
+            if (name == null || Strings.hasText(name) == false) {
+                validationException = addValidationError("name is missing", validationException);
+            }
+            if (componentTemplate == null) {
+                validationException = addValidationError("a component 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 component template that will be inserted into the cluster state
+         */
+        public Request componentTemplate(ComponentTemplate template) {
+            this.componentTemplate = template;
+            return this;
+        }
+
+        public ComponentTemplate componentTemplate() {
+            return this.componentTemplate;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder("PutComponentRequest[");
+            sb.append("name=").append(name);
+            sb.append(", cause=").append(cause);
+            sb.append(", create=").append(create);
+            sb.append(", component_template=").append(componentTemplate);
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
+}

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

@@ -0,0 +1,88 @@
+/*
+ * 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.ComponentTemplate;
+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 TransportPutComponentTemplateAction
+    extends TransportMasterNodeAction<PutComponentTemplateAction.Request, AcknowledgedResponse> {
+
+    private final MetaDataIndexTemplateService indexTemplateService;
+
+    @Inject
+    public TransportPutComponentTemplateAction(TransportService transportService, ClusterService clusterService,
+                                               ThreadPool threadPool, MetaDataIndexTemplateService indexTemplateService,
+                                               ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
+        super(PutComponentTemplateAction.NAME, transportService, clusterService, threadPool, actionFilters,
+            PutComponentTemplateAction.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(PutComponentTemplateAction.Request request, ClusterState state) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
+    }
+
+    @Override
+    protected void masterOperation(Task task, final PutComponentTemplateAction.Request request, final ClusterState state,
+                                   final ActionListener<AcknowledgedResponse> listener) {
+        ComponentTemplate componentTemplate = request.componentTemplate();
+        Template template = componentTemplate.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());
+            componentTemplate = new ComponentTemplate(template, componentTemplate.version(), componentTemplate.metadata());
+        }
+        indexTemplateService.putComponentTemplate(request.cause(), request.create(), request.name(), request.masterNodeTimeout(),
+            componentTemplate, listener);
+    }
+}

+ 1 - 148
server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplate.java

@@ -24,26 +24,19 @@ import org.elasticsearch.cluster.Diff;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.bytes.BytesArray;
-import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.common.xcontent.XContentType;
 
 import java.io.IOException;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 
 /**
- * A component template is a re-usable template as well as metadata about the template. Each
+ * A component template is a re-usable {@link Template} as well as metadata about the template. Each
  * component template is expected to be valid on its own. For example, if a component template
  * contains a field "foo", it's expected to contain all the necessary settings/mappings/etc for the
  * "foo" field. These component templates make up the individual pieces composing an index template.
@@ -157,144 +150,4 @@ public class ComponentTemplate extends AbstractDiffable<ComponentTemplate> imple
         builder.endObject();
         return builder;
     }
-
-    static class Template extends AbstractDiffable<Template> implements ToXContentObject {
-        private static final ParseField SETTINGS = new ParseField("settings");
-        private static final ParseField MAPPINGS = new ParseField("mappings");
-        private static final ParseField ALIASES = new ParseField("aliases");
-
-        @SuppressWarnings("unchecked")
-        private static final ConstructingObjectParser<Template, Void> PARSER = new ConstructingObjectParser<>("template", false,
-            a -> new Template((Settings) a[0], (CompressedXContent) a[1], (Map<String, AliasMetaData>) a[2]));
-
-        static {
-            PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> Settings.fromXContent(p), SETTINGS);
-            PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) ->
-                new CompressedXContent(Strings.toString(XContentFactory.jsonBuilder().map(p.mapOrdered()))), MAPPINGS);
-            PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
-                Map<String, AliasMetaData> aliasMap = new HashMap<>();
-                while ((p.nextToken()) != XContentParser.Token.END_OBJECT) {
-                    AliasMetaData alias = AliasMetaData.Builder.fromXContent(p);
-                    aliasMap.put(alias.alias(), alias);
-                }
-                return aliasMap;
-            }, ALIASES);
-        }
-
-        @Nullable
-        private final Settings settings;
-        @Nullable
-        private final CompressedXContent mappings;
-        @Nullable
-        private final Map<String, AliasMetaData> aliases;
-
-        Template(@Nullable Settings settings, @Nullable CompressedXContent mappings, @Nullable Map<String, AliasMetaData> aliases) {
-            this.settings = settings;
-            this.mappings = mappings;
-            this.aliases = aliases;
-        }
-
-        Template(StreamInput in) throws IOException {
-            if (in.readBoolean()) {
-                this.settings = Settings.readSettingsFromStream(in);
-            } else {
-                this.settings = null;
-            }
-            if (in.readBoolean()) {
-                this.mappings = CompressedXContent.readCompressedString(in);
-            } else {
-                this.mappings = null;
-            }
-            if (in.readBoolean()) {
-                this.aliases = in.readMap(StreamInput::readString, AliasMetaData::new);
-            } else {
-                this.aliases = null;
-            }
-        }
-
-        public Settings settings() {
-            return settings;
-        }
-
-        public CompressedXContent mappings() {
-            return mappings;
-        }
-
-        public Map<String, AliasMetaData> aliases() {
-            return aliases;
-        }
-
-        @Override
-        public void writeTo(StreamOutput out) throws IOException {
-            if (this.settings == null) {
-                out.writeBoolean(false);
-            } else {
-                out.writeBoolean(true);
-                Settings.writeSettingsToStream(this.settings, out);
-            }
-            if (this.mappings == null) {
-                out.writeBoolean(false);
-            } else {
-                out.writeBoolean(true);
-                this.mappings.writeTo(out);
-            }
-            if (this.aliases == null) {
-                out.writeBoolean(false);
-            } else {
-                out.writeBoolean(true);
-                out.writeMap(this.aliases, StreamOutput::writeString, (stream, aliasMetaData) -> aliasMetaData.writeTo(stream));
-            }
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(settings, mappings, aliases);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj == null) {
-                return false;
-            }
-            if (obj.getClass() != getClass()) {
-                return false;
-            }
-            Template other = (Template) obj;
-            return Objects.equals(settings, other.settings) &&
-                Objects.equals(mappings, other.mappings) &&
-                Objects.equals(aliases, other.aliases);
-        }
-
-        @Override
-        public String toString() {
-            return Strings.toString(this);
-        }
-
-        @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            builder.startObject();
-            if (this.settings != null) {
-                builder.startObject(SETTINGS.getPreferredName());
-                this.settings.toXContent(builder, params);
-                builder.endObject();
-            }
-            if (this.mappings != null) {
-                Map<String, Object> uncompressedMapping =
-                    XContentHelper.convertToMap(new BytesArray(this.mappings.uncompressed()), true, XContentType.JSON).v2();
-                if (uncompressedMapping.size() > 0) {
-                    builder.field(MAPPINGS.getPreferredName());
-                    builder.map(uncompressedMapping);
-                }
-            }
-            if (this.aliases != null) {
-                builder.startObject(ALIASES.getPreferredName());
-                for (AliasMetaData alias : this.aliases.values()) {
-                    AliasMetaData.Builder.toXContent(alias, builder, params);
-                }
-                builder.endObject();
-            }
-            builder.endObject();
-            return builder;
-        }
-    }
 }

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

@@ -23,7 +23,9 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.lucene.util.CollectionUtil;
 import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.Alias;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.action.support.master.MasterNodeRequest;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterStateUpdateTask;
@@ -132,6 +134,104 @@ 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 putComponentTemplate(final String cause, final boolean create, final String name, final TimeValue masterTimeout,
+                                     final ComponentTemplate template, final ActionListener<AcknowledgedResponse> listener) {
+        clusterService.submitStateUpdateTask("create-component-template [" + 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 addComponentTemplate(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 addComponentTemplate(final ClusterState currentState, final boolean create,
+                                             final String name, final ComponentTemplate template) {
+        if (create && currentState.metaData().componentTemplates().containsKey(name)) {
+            throw new IllegalArgumentException("component template [" + name + "] already exists");
+        }
+
+        // TODO: validation of component template
+        // validateAndAddTemplate(request, templateBuilder, indicesService, xContentRegistry);
+
+        logger.info("adding component template [{}]", name);
+        return ClusterState.builder(currentState)
+            .metaData(MetaData.builder(currentState.metaData()).put(name, template))
+            .build();
+    }
+
+    /**
+     * Remove the given component template from the cluster state. The component template name
+     * supports simple regex wildcards for removing multiple component templates at a time.
+     */
+    public void removeComponentTemplate(final String name, final TimeValue masterTimeout,
+                                        final ActionListener<AcknowledgedResponse> listener) {
+        clusterService.submitStateUpdateTask("remove-component-template [" + 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) {
+                    Set<String> templateNames = new HashSet<>();
+                    for (String templateName : currentState.metaData().componentTemplates().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;
+                        }
+                        // TODO: perhaps introduce a ComponentTemplateMissingException?
+                        throw new IndexTemplateMissingException(name);
+                    }
+                    MetaData.Builder metaData = MetaData.builder(currentState.metaData());
+                    for (String templateName : templateNames) {
+                        logger.info("removing component template [{}]", templateName);
+                        metaData.removeComponentTemplate(templateName);
+                    }
+                    return ClusterState.builder(currentState).metaData(metaData).build();
+                }
+
+                @Override
+                public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+                    listener.onResponse(new AcknowledgedResponse(true));
+                }
+            });
+    }
+
     public void putTemplate(final PutRequest request, final PutListener listener) {
         Settings.Builder updatedSettingsBuilder = Settings.builder();
         updatedSettingsBuilder.put(request.settings).normalizePrefix(IndexMetaData.INDEX_SETTING_PREFIX);

+ 187 - 0
server/src/main/java/org/elasticsearch/cluster/metadata/Template.java

@@ -0,0 +1,187 @@
+/*
+ * 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.cluster.metadata;
+
+import org.elasticsearch.cluster.AbstractDiffable;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A template consists of optional settings, mappings, or alias configuration for an index, however,
+ * it is entirely independent from an index. It's a building block forming part of a regular index
+ * template and a {@link ComponentTemplate}.
+ */
+public class Template extends AbstractDiffable<Template> implements ToXContentObject {
+    private static final ParseField SETTINGS = new ParseField("settings");
+    private static final ParseField MAPPINGS = new ParseField("mappings");
+    private static final ParseField ALIASES = new ParseField("aliases");
+
+    @SuppressWarnings("unchecked")
+    static final ConstructingObjectParser<Template, Void> PARSER = new ConstructingObjectParser<>("template", false,
+        a -> new Template((Settings) a[0], (CompressedXContent) a[1], (Map<String, AliasMetaData>) a[2]));
+
+    static {
+        PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> Settings.fromXContent(p), SETTINGS);
+        PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) ->
+            new CompressedXContent(Strings.toString(XContentFactory.jsonBuilder().map(p.mapOrdered()))), MAPPINGS);
+        PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
+            Map<String, AliasMetaData> aliasMap = new HashMap<>();
+            while ((p.nextToken()) != XContentParser.Token.END_OBJECT) {
+                AliasMetaData alias = AliasMetaData.Builder.fromXContent(p);
+                aliasMap.put(alias.alias(), alias);
+            }
+            return aliasMap;
+        }, ALIASES);
+    }
+
+    @Nullable
+    private final Settings settings;
+    @Nullable
+    private final CompressedXContent mappings;
+    @Nullable
+    private final Map<String, AliasMetaData> aliases;
+
+    public Template(@Nullable Settings settings, @Nullable CompressedXContent mappings, @Nullable Map<String, AliasMetaData> aliases) {
+        this.settings = settings;
+        this.mappings = mappings;
+        this.aliases = aliases;
+    }
+
+    Template(StreamInput in) throws IOException {
+        if (in.readBoolean()) {
+            this.settings = Settings.readSettingsFromStream(in);
+        } else {
+            this.settings = null;
+        }
+        if (in.readBoolean()) {
+            this.mappings = CompressedXContent.readCompressedString(in);
+        } else {
+            this.mappings = null;
+        }
+        if (in.readBoolean()) {
+            this.aliases = in.readMap(StreamInput::readString, AliasMetaData::new);
+        } else {
+            this.aliases = null;
+        }
+    }
+
+    public Settings settings() {
+        return settings;
+    }
+
+    public CompressedXContent mappings() {
+        return mappings;
+    }
+
+    public Map<String, AliasMetaData> aliases() {
+        return aliases;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        if (this.settings == null) {
+            out.writeBoolean(false);
+        } else {
+            out.writeBoolean(true);
+            Settings.writeSettingsToStream(this.settings, out);
+        }
+        if (this.mappings == null) {
+            out.writeBoolean(false);
+        } else {
+            out.writeBoolean(true);
+            this.mappings.writeTo(out);
+        }
+        if (this.aliases == null) {
+            out.writeBoolean(false);
+        } else {
+            out.writeBoolean(true);
+            out.writeMap(this.aliases, StreamOutput::writeString, (stream, aliasMetaData) -> aliasMetaData.writeTo(stream));
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(settings, mappings, aliases);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        Template other = (Template) obj;
+        return Objects.equals(settings, other.settings) &&
+            Objects.equals(mappings, other.mappings) &&
+            Objects.equals(aliases, other.aliases);
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        if (this.settings != null) {
+            builder.startObject(SETTINGS.getPreferredName());
+            this.settings.toXContent(builder, params);
+            builder.endObject();
+        }
+        if (this.mappings != null) {
+            Map<String, Object> uncompressedMapping =
+                XContentHelper.convertToMap(new BytesArray(this.mappings.uncompressed()), true, XContentType.JSON).v2();
+            if (uncompressedMapping.size() > 0) {
+                builder.field(MAPPINGS.getPreferredName());
+                builder.map(uncompressedMapping);
+            }
+        }
+        if (this.aliases != null) {
+            builder.startObject(ALIASES.getPreferredName());
+            for (AliasMetaData alias : this.aliases.values()) {
+                AliasMetaData.Builder.toXContent(alias, builder, params);
+            }
+            builder.endObject();
+        }
+        builder.endObject();
+        return builder;
+    }
+}

+ 53 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestDeleteComponentTemplateAction.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.DeleteComponentTemplateAction;
+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 RestDeleteComponentTemplateAction extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(new Route(DELETE, "/_component_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "delete_component_template_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+
+        DeleteComponentTemplateAction.Request deleteReq = new DeleteComponentTemplateAction.Request(request.param("name"));
+        deleteReq.masterNodeTimeout(request.paramAsTime("master_timeout", deleteReq.masterNodeTimeout()));
+
+        return channel -> client.execute(DeleteComponentTemplateAction.INSTANCE, deleteReq, new RestToXContentListener<>(channel));
+    }
+}

+ 81 - 0
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetComponentTemplateAction.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.GetComponentTemplateAction;
+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 RestGetComponentTemplateAction extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(
+            new Route(GET, "/_component_template"),
+            new Route(GET, "/_component_template/{name}"),
+            new Route(HEAD, "/_component_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "get_component_template_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+        final String[] names = Strings.splitStringByCommaToArray(request.param("name"));
+
+        final GetComponentTemplateAction.Request getRequest = new GetComponentTemplateAction.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(GetComponentTemplateAction.INSTANCE, getRequest, new RestToXContentListener<>(channel) {
+                    @Override
+                    protected RestStatus getStatus(final GetComponentTemplateAction.Response response) {
+                        final boolean templateExists = response.getComponentTemplates().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/RestPutComponentTemplateAction.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.PutComponentTemplateAction;
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.cluster.metadata.ComponentTemplate;
+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 RestPutComponentTemplateAction extends BaseRestHandler {
+
+    @Override
+    public List<Route> routes() {
+        return List.of(
+            new Route(POST, "/_component_template/{name}"),
+            new Route(PUT, "/_component_template/{name}"));
+    }
+
+    @Override
+    public String getName() {
+        return "put_component_template_action";
+    }
+
+    @Override
+    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
+
+        PutComponentTemplateAction.Request putRequest = new PutComponentTemplateAction.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.componentTemplate(ComponentTemplate.parse(request.contentParser()));
+
+        return channel -> client.execute(PutComponentTemplateAction.INSTANCE, putRequest, new RestToXContentListener<>(channel));
+    }
+}

+ 54 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetComponentTemplateResponseTests.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.ComponentTemplate;
+import org.elasticsearch.cluster.metadata.ComponentTemplateTests;
+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 GetComponentTemplateResponseTests extends AbstractWireSerializingTestCase<GetComponentTemplateAction.Response> {
+    @Override
+    protected Writeable.Reader<GetComponentTemplateAction.Response> instanceReader() {
+        return GetComponentTemplateAction.Response::new;
+    }
+
+    @Override
+    protected GetComponentTemplateAction.Response createTestInstance() {
+        if (randomBoolean()) {
+            return new GetComponentTemplateAction.Response(Collections.emptyMap());
+        }
+        Map<String, ComponentTemplate> templates = new HashMap<>();
+        for (int i = 0; i < randomIntBetween(1, 4); i++) {
+            templates.put(randomAlphaOfLength(4), ComponentTemplateTests.randomInstance());
+        }
+        return new GetComponentTemplateAction.Response(templates);
+    }
+
+    @Override
+    protected GetComponentTemplateAction.Response mutateInstance(GetComponentTemplateAction.Response instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+}

+ 7 - 7
server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java

@@ -76,7 +76,7 @@ public class ComponentTemplateTests extends AbstractDiffableSerializationTestCas
         if (randomBoolean()) {
             aliases = randomAliases();
         }
-        ComponentTemplate.Template template = new ComponentTemplate.Template(settings, mappings, aliases);
+        Template template = new Template(settings, mappings, aliases);
 
         Map<String, Object> meta = null;
         if (randomBoolean()) {
@@ -130,21 +130,21 @@ public class ComponentTemplateTests extends AbstractDiffableSerializationTestCas
             case 0:
                 switch (randomIntBetween(0, 2)) {
                     case 0:
-                        ComponentTemplate.Template ot = orig.template();
+                        Template ot = orig.template();
                         return new ComponentTemplate(
-                            new ComponentTemplate.Template(randomValueOtherThan(ot.settings(), ComponentTemplateTests::randomSettings),
+                            new Template(randomValueOtherThan(ot.settings(), ComponentTemplateTests::randomSettings),
                                 ot.mappings(), ot.aliases()),
                             orig.version(), orig.metadata());
                     case 1:
-                        ComponentTemplate.Template ot2 = orig.template();
+                        Template ot2 = orig.template();
                         return new ComponentTemplate(
-                            new ComponentTemplate.Template(ot2.settings(),
+                            new Template(ot2.settings(),
                                 randomValueOtherThan(ot2.mappings(), ComponentTemplateTests::randomMappings), ot2.aliases()),
                             orig.version(), orig.metadata());
                     case 2:
-                        ComponentTemplate.Template ot3 = orig.template();
+                        Template ot3 = orig.template();
                         return new ComponentTemplate(
-                            new ComponentTemplate.Template(ot3.settings(), ot3.mappings(),
+                            new Template(ot3.settings(), ot3.mappings(),
                                 randomValueOtherThan(ot3.aliases(), ComponentTemplateTests::randomAliases)),
                             orig.version(), orig.metadata());
                     default:

+ 18 - 6
server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java → server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateServiceTests.java

@@ -17,15 +17,10 @@
  * under the License.
  */
 
-package org.elasticsearch.action.admin.indices.template.put;
+package org.elasticsearch.cluster.metadata;
 
 import org.elasticsearch.action.admin.indices.alias.Alias;
 import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.cluster.metadata.AliasValidator;
-import org.elasticsearch.cluster.metadata.IndexMetaData;
-import org.elasticsearch.cluster.metadata.IndexTemplateMetaData;
-import org.elasticsearch.cluster.metadata.MetaDataCreateIndexService;
-import org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService;
 import org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService.PutRequest;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.Strings;
@@ -202,6 +197,23 @@ public class MetaDataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         assertThat(errors.get(0).getMessage(), containsString("global templates may not specify the setting index.hidden"));
     }
 
+    public void testAddComponentTemplate() {
+        ClusterState state = ClusterState.EMPTY_STATE;
+        ComponentTemplate template = ComponentTemplateTests.randomInstance();
+        state = MetaDataIndexTemplateService.addComponentTemplate(state, false, "foo", template);
+
+        assertNotNull(state.metaData().componentTemplates().get("foo"));
+        assertThat(state.metaData().componentTemplates().get("foo"), equalTo(template));
+
+        final ClusterState throwState = ClusterState.builder(state).build();
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
+            () -> MetaDataIndexTemplateService.addComponentTemplate(throwState, true, "foo", template));
+        assertThat(e.getMessage(), containsString("component template [foo] already exists"));
+
+        state = MetaDataIndexTemplateService.addComponentTemplate(state, randomBoolean(), "bar", template);
+        assertNotNull(state.metaData().componentTemplates().get("bar"));
+    }
+
     private static List<Throwable> putTemplate(NamedXContentRegistry xContentRegistry, PutRequest request) {
         MetaDataCreateIndexService createIndexService = new MetaDataCreateIndexService(
                 Settings.EMPTY,

+ 2 - 2
server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java

@@ -118,7 +118,7 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
                         .putAlias(newAliasMetaDataBuilder("alias-bar2").filter("{\"term\":{\"user\":\"kimchy\"}}"))
                         .putAlias(newAliasMetaDataBuilder("alias-bar3").routing("routing-bar")))
                 .put("component_template", new ComponentTemplate(
-                    new ComponentTemplate.Template(Settings.builder().put("setting", "value").build(),
+                    new Template(Settings.builder().put("setting", "value").build(),
                         new CompressedXContent("{\"baz\":\"eggplant\"}"),
                         Collections.singletonMap("alias", AliasMetaData.builder("alias").build())),
                     5L, Collections.singletonMap("my_meta", Collections.singletonMap("foo", "bar"))))
@@ -298,7 +298,7 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
         assertThat(parsedMetaData.componentTemplates().get("component_template").metadata(),
             equalTo(Collections.singletonMap("my_meta", Collections.singletonMap("foo", "bar"))));
         assertThat(parsedMetaData.componentTemplates().get("component_template").template(),
-            equalTo(new ComponentTemplate.Template(Settings.builder().put("setting", "value").build(),
+            equalTo(new Template(Settings.builder().put("setting", "value").build(),
                 new CompressedXContent("{\"baz\":\"eggplant\"}"),
                 Collections.singletonMap("alias", AliasMetaData.builder("alias").build()))));
     }