Browse Source

Migrate legacy/v2/component templates away from custom attributes routing (#82472)

This enhances the migrate to data tiers routing API to also iterate over
the existing legacy, composable, and component templates and look if
they define a custom node attribute routing in their settings for either
`index.routing.allocation.require.{nodeAttrName}` or
`index.routing.allocation.include.{nodeAttrName}`. If any does, we
update them to remove all the routings settings for the provided
`nodeAttrName`.

eg. any template with the following setting configuration:
```
"settings": {
  index.routing.allocation.require.data: "warm",
  index.routing.allocation.include.data: "rack1",
  index.routing.allocation.exclude.data: "rack2,rack3"
}
```
will have its settings updated to:
```
"settings": {}
```
Andrei Dan 3 years ago
parent
commit
3087f164f7

+ 5 - 0
docs/reference/data-management/migrate-index-allocation-filters.asciidoc

@@ -136,6 +136,11 @@ DELETE _template/.cloud-hot-warm-allocation-0
 
 If you're using a custom index template, update it to remove the <<shard-allocation-filtering, attribute-based allocation filters>> you used to assign new indices to the hot tier.
 
+To completely avoid the issues that raise when mixing the tier preference and
+custom attribute routing setting we also recommend updating all the legacy,
+composable, and component templates to remove the <<shard-allocation-filtering, attribute-based allocation filters>>
+from the settings they configure.
+
 [discrete]
 [[set-tier-preference]]
 ==== Set a tier preference for existing indices

+ 50 - 6
docs/reference/ilm/apis/migrate-to-data-tiers.asciidoc

@@ -2,10 +2,10 @@
 [[ilm-migrate-to-data-tiers]]
 === Migrate to data tiers routing API
 ++++
-<titleabbrev>Migrate indices and ILM policies to data tiers routing</titleabbrev>
+<titleabbrev>Migrate indices, ILM policies, and legacy, composable and component templates to data tiers routing</titleabbrev>
 ++++
 
-Switches the indices and ILM policies from using custom node attributes and
+Switches the indices, ILM policies, and legacy, composable and component templates from using custom node attributes and
 <<shard-allocation-filtering, attribute-based allocation filters>> to using <<data-tiers, data tiers>>, and
 optionally deletes one legacy index template.
 Using node roles enables {ilm-init} to <<data-tier-migration, automatically move the indices>> between
@@ -55,9 +55,10 @@ NOTE: When simulating a migration (ie. `dry_run` is `true`) {ilm-init} doesn't n
 [[ilm-migrate-to-data-tiers-example]]
 ==== {api-examples-title}
 
-The following example migrates the indices and ILM policies away from defining
-custom allocation filtering using the `custom_attribute_name` node attribute, and
-deletes legacy template with name `global-template` if it exists in the system.
+The following example migrates the indices, ILM policies, legacy templates,
+composable, and component templates away from defining custom allocation filtering
+using the `custom_attribute_name` node attribute, and deletes the legacy template
+with name `global-template` if it exists in the system.
 
 ////
 [source,console]
@@ -72,6 +73,34 @@ PUT _template/global-template
   }
 }
 
+PUT _template/a-legacy-template
+{
+  "index_patterns": ["legacy-template-migrate-to-tiers-*"],
+  "settings": {
+     "index.routing.allocation.require.custom_attribute_name": "hot"
+  }
+}
+
+PUT _index_template/a-composable-template
+{
+	"index_patterns": [ "composable-template-migrate-to-tiers-*" ],
+	"data_stream": {},
+	"template" : {
+		"settings": {
+			 "index.routing.allocation.require.custom_attribute_name": "hot"
+		}
+	}
+}
+
+PUT _component_template/a-component-template
+{
+	"template" : {
+		"settings": {
+			 "index.routing.allocation.require.custom_attribute_name": "hot"
+		}
+	}
+}
+
 PUT warm-index-to-migrate-000001
 {
   "settings": {
@@ -110,6 +139,12 @@ DELETE warm-index-to-migrate-000001
 
 DELETE _ilm/policy/policy_with_allocate_action
 
+DELETE _template/a-legacy-template
+
+DELETE _index_template/a-composable-template
+
+DELETE _component_template/a-component-template
+
 POST _ilm/start
 ----
 // TEARDOWN
@@ -132,7 +167,10 @@ If the request succeeds, a response like the following will be received:
   "dry_run": false,
   "removed_legacy_template":"global-template", <1>
   "migrated_ilm_policies":["policy_with_allocate_action"], <2>
-  "migrated_indices":["warm-index-to-migrate-000001"] <3>
+  "migrated_indices":["warm-index-to-migrate-000001"], <3>
+  "migrated_legacy_templates":["a-legacy-template"], <4>
+  "migrated_composable_templates":["a-composable-template"], <5>
+  "migrated_component_templates":["a-component-template"] <6>
 }
 ------------------------------------------------------------------------------
 
@@ -140,3 +178,9 @@ If the request succeeds, a response like the following will be received:
 if no legacy index template was deleted.
 <2> The ILM policies that were updated.
 <3> The indices that were migrated to <<tier-preference-allocation-filter,tier preference>> routing.
+<4> The legacy index templates that were updated to not contain custom routing settings for the
+provided data attribute.
+<5> The composable index templates that were updated to not contain custom routing settings for the
+provided data attribute.
+<6> The component templates that were updated to not contain custom routing settings for the
+provided data attribute.

+ 74 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/action/MigrateToDataTiersResponse.java

@@ -7,6 +7,7 @@
 
 package org.elasticsearch.xpack.cluster.action;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
@@ -24,23 +25,35 @@ public class MigrateToDataTiersResponse extends ActionResponse implements ToXCon
     public static final ParseField REMOVED_LEGACY_TEMPLATE = new ParseField("removed_legacy_template");
     public static final ParseField MIGRATED_INDICES = new ParseField("migrated_indices");
     public static final ParseField MIGRATED_ILM_POLICIES = new ParseField("migrated_ilm_policies");
-    private static final ParseField DRY_RUN = new ParseField("dry_run");
+    public static final ParseField MIGRATED_LEGACY_TEMPLATES = new ParseField("migrated_legacy_templates");
+    public static final ParseField MIGRATED_COMPOSABLE_TEMPLATES = new ParseField("migrated_composable_templates");
+    public static final ParseField MIGRATED_COMPONENT_TEMPLATES = new ParseField("migrated_component_templates");
+    public static final ParseField DRY_RUN = new ParseField("dry_run");
 
     @Nullable
     private final String removedIndexTemplateName;
     private final List<String> migratedPolicies;
     private final List<String> migratedIndices;
     private final boolean dryRun;
+    private final List<String> migratedLegacyTemplates;
+    private final List<String> migratedComposableTemplates;
+    private final List<String> migratedComponentTemplates;
 
     public MigrateToDataTiersResponse(
         @Nullable String removedIndexTemplateName,
         List<String> migratedPolicies,
         List<String> migratedIndices,
+        List<String> migratedLegacyTemplates,
+        List<String> migratedComposableTemplates,
+        List<String> migratedComponentTemplates,
         boolean dryRun
     ) {
         this.removedIndexTemplateName = removedIndexTemplateName;
         this.migratedPolicies = migratedPolicies;
         this.migratedIndices = migratedIndices;
+        this.migratedLegacyTemplates = migratedLegacyTemplates;
+        this.migratedComposableTemplates = migratedComposableTemplates;
+        this.migratedComponentTemplates = migratedComponentTemplates;
         this.dryRun = dryRun;
     }
 
@@ -50,6 +63,15 @@ public class MigrateToDataTiersResponse extends ActionResponse implements ToXCon
         migratedPolicies = in.readStringList();
         migratedIndices = in.readStringList();
         dryRun = in.readBoolean();
+        if (in.getVersion().onOrAfter(Version.CURRENT)) {
+            migratedLegacyTemplates = in.readStringList();
+            migratedComposableTemplates = in.readStringList();
+            migratedComponentTemplates = in.readStringList();
+        } else {
+            migratedLegacyTemplates = List.of();
+            migratedComposableTemplates = List.of();
+            migratedComponentTemplates = List.of();
+        }
     }
 
     @Override
@@ -73,6 +95,27 @@ public class MigrateToDataTiersResponse extends ActionResponse implements ToXCon
             }
             builder.endArray();
         }
+        if (migratedLegacyTemplates.size() > 0) {
+            builder.startArray(MIGRATED_LEGACY_TEMPLATES.getPreferredName());
+            for (String legacyTemplate : migratedLegacyTemplates) {
+                builder.value(legacyTemplate);
+            }
+            builder.endArray();
+        }
+        if (migratedComposableTemplates.size() > 0) {
+            builder.startArray(MIGRATED_COMPOSABLE_TEMPLATES.getPreferredName());
+            for (String composableTemplate : migratedComposableTemplates) {
+                builder.value(composableTemplate);
+            }
+            builder.endArray();
+        }
+        if (migratedComponentTemplates.size() > 0) {
+            builder.startArray(MIGRATED_COMPONENT_TEMPLATES.getPreferredName());
+            for (String componentTemplate : migratedComponentTemplates) {
+                builder.value(componentTemplate);
+            }
+            builder.endArray();
+        }
         builder.endObject();
         return builder;
     }
@@ -93,12 +136,29 @@ public class MigrateToDataTiersResponse extends ActionResponse implements ToXCon
         return dryRun;
     }
 
+    public List<String> getMigratedLegacyTemplates() {
+        return migratedLegacyTemplates;
+    }
+
+    public List<String> getMigratedComposableTemplates() {
+        return migratedComposableTemplates;
+    }
+
+    public List<String> getMigratedComponentTemplates() {
+        return migratedComponentTemplates;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeOptionalString(removedIndexTemplateName);
         out.writeStringCollection(migratedPolicies);
         out.writeStringCollection(migratedIndices);
         out.writeBoolean(dryRun);
+        if (out.getVersion().onOrAfter(Version.CURRENT)) {
+            out.writeStringCollection(migratedLegacyTemplates);
+            out.writeStringCollection(migratedComposableTemplates);
+            out.writeStringCollection(migratedComponentTemplates);
+        }
     }
 
     @Override
@@ -113,11 +173,22 @@ public class MigrateToDataTiersResponse extends ActionResponse implements ToXCon
         return dryRun == that.dryRun
             && Objects.equals(removedIndexTemplateName, that.removedIndexTemplateName)
             && Objects.equals(migratedPolicies, that.migratedPolicies)
-            && Objects.equals(migratedIndices, that.migratedIndices);
+            && Objects.equals(migratedIndices, that.migratedIndices)
+            && Objects.equals(migratedLegacyTemplates, that.migratedLegacyTemplates)
+            && Objects.equals(migratedComposableTemplates, that.migratedComposableTemplates)
+            && Objects.equals(migratedComponentTemplates, that.migratedComponentTemplates);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(removedIndexTemplateName, migratedPolicies, migratedIndices, dryRun);
+        return Objects.hash(
+            removedIndexTemplateName,
+            migratedPolicies,
+            migratedIndices,
+            dryRun,
+            migratedLegacyTemplates,
+            migratedComposableTemplates,
+            migratedComponentTemplates
+        );
     }
 }

+ 43 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/action/MigrateToDataTiersResponseTests.java

@@ -26,36 +26,78 @@ public class MigrateToDataTiersResponseTests extends AbstractWireSerializingTest
             randomAlphaOfLength(10),
             randomList(1, 5, () -> randomAlphaOfLengthBetween(5, 50)),
             randomList(1, 5, () -> randomAlphaOfLengthBetween(5, 50)),
+            randomList(1, 5, () -> randomAlphaOfLengthBetween(5, 50)),
+            randomList(1, 5, () -> randomAlphaOfLengthBetween(5, 50)),
+            randomList(1, 5, () -> randomAlphaOfLengthBetween(5, 50)),
             dryRun
         );
     }
 
     @Override
     protected MigrateToDataTiersResponse mutateInstance(MigrateToDataTiersResponse instance) throws IOException {
-        int i = randomIntBetween(0, 3);
+        int i = randomIntBetween(0, 6);
         return switch (i) {
             case 0 -> new MigrateToDataTiersResponse(
                 randomValueOtherThan(instance.getRemovedIndexTemplateName(), () -> randomAlphaOfLengthBetween(5, 15)),
                 instance.getMigratedPolicies(),
                 instance.getMigratedIndices(),
+                instance.getMigratedLegacyTemplates(),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
                 instance.isDryRun()
             );
             case 1 -> new MigrateToDataTiersResponse(
                 instance.getRemovedIndexTemplateName(),
                 randomList(6, 10, () -> randomAlphaOfLengthBetween(5, 50)),
                 instance.getMigratedIndices(),
+                instance.getMigratedLegacyTemplates(),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
                 instance.isDryRun()
             );
             case 2 -> new MigrateToDataTiersResponse(
                 instance.getRemovedIndexTemplateName(),
                 instance.getMigratedPolicies(),
                 randomList(6, 10, () -> randomAlphaOfLengthBetween(5, 50)),
+                instance.getMigratedLegacyTemplates(),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
                 instance.isDryRun()
             );
             case 3 -> new MigrateToDataTiersResponse(
                 instance.getRemovedIndexTemplateName(),
                 instance.getMigratedPolicies(),
                 instance.getMigratedIndices(),
+                randomList(6, 10, () -> randomAlphaOfLengthBetween(5, 50)),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
+                instance.isDryRun()
+            );
+            case 4 -> new MigrateToDataTiersResponse(
+                instance.getRemovedIndexTemplateName(),
+                instance.getMigratedPolicies(),
+                instance.getMigratedIndices(),
+                instance.getMigratedLegacyTemplates(),
+                randomList(6, 10, () -> randomAlphaOfLengthBetween(5, 50)),
+                instance.getMigratedComponentTemplates(),
+                instance.isDryRun()
+            );
+            case 5 -> new MigrateToDataTiersResponse(
+                instance.getRemovedIndexTemplateName(),
+                instance.getMigratedPolicies(),
+                instance.getMigratedIndices(),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
+                randomList(6, 10, () -> randomAlphaOfLengthBetween(5, 50)),
+                instance.isDryRun()
+            );
+            case 6 -> new MigrateToDataTiersResponse(
+                instance.getRemovedIndexTemplateName(),
+                instance.getMigratedPolicies(),
+                instance.getMigratedIndices(),
+                instance.getMigratedLegacyTemplates(),
+                instance.getMigratedComposableTemplates(),
+                instance.getMigratedComponentTemplates(),
                 instance.isDryRun() ? false : true
             );
             default -> throw new UnsupportedOperationException();

+ 120 - 1
x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/MigrateToDataTiersIT.java

@@ -18,6 +18,7 @@ import org.elasticsearch.client.Response;
 import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.routing.allocation.DataTier;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.rest.RestStatus;
@@ -154,7 +155,7 @@ public class MigrateToDataTiersIT extends ESRestTestCase {
                     .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
                     .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
                     .putNull(DataTier.TIER_PREFERENCE) // since we always enforce a tier preference, this will be ignored (i.e.
-                                                       // data_content)
+                    // data_content)
                     .put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, alias + i)
             );
 
@@ -197,6 +198,14 @@ public class MigrateToDataTiersIT extends ESRestTestCase {
         );
         assertThat(migrateResponseAsMap.get(MigrateToDataTiersResponse.REMOVED_LEGACY_TEMPLATE.getPreferredName()), is(templateName));
 
+        // let's verify no index template was migrated
+        assertThat(migrateResponseAsMap.containsKey(MigrateToDataTiersResponse.MIGRATED_LEGACY_TEMPLATES.getPreferredName()), is(false));
+        assertThat(
+            migrateResponseAsMap.containsKey(MigrateToDataTiersResponse.MIGRATED_COMPOSABLE_TEMPLATES.getPreferredName()),
+            is(false)
+        );
+        assertThat(migrateResponseAsMap.containsKey(MigrateToDataTiersResponse.MIGRATED_COMPONENT_TEMPLATES.getPreferredName()), is(false));
+
         // let's verify the legacy template doesn't exist anymore
         Request getTemplateRequest = new Request("HEAD", "_template/" + templateName);
         assertThat(client().performRequest(getTemplateRequest).getStatusLine().getStatusCode(), is(RestStatus.NOT_FOUND.getStatus()));
@@ -245,6 +254,116 @@ public class MigrateToDataTiersIT extends ESRestTestCase {
         assertTrue(json.at("/defaults/cluster/routing/allocation/enforce_default_tier_preference").asBoolean());
     }
 
+    @SuppressWarnings("unchecked")
+    public void testIndexTemplatesMigration() throws Exception {
+        // legacy template to migrate
+        String legacyTemplateToMigrate = "legacy_to_migrate";
+        {
+            Request legacyTemplateToMigrateReq = new Request("PUT", "/_template/" + legacyTemplateToMigrate);
+            Settings indexSettings = Settings.builder()
+                .put("index.number_of_shards", 1)
+                .put("index.routing.allocation.require.data", "hot")
+                .build();
+            legacyTemplateToMigrateReq.setJsonEntity(
+                "{\"index_patterns\":  [\"legacynotreallyimportant-*\"], \"settings\":  " + Strings.toString(indexSettings) + "}"
+            );
+            legacyTemplateToMigrateReq.setOptions(
+                expectWarnings("Legacy index templates are deprecated in favor of composable templates" + ".")
+            );
+            assertOK(client().performRequest(legacyTemplateToMigrateReq));
+        }
+
+        // legacy template that doesn't need migrating
+        String legacyTemplate = "legacy_template";
+        {
+            Request legacyTemplateRequest = new Request("PUT", "/_template/" + legacyTemplate);
+            Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).build();
+            legacyTemplateRequest.setJsonEntity(
+                "{\"index_patterns\":  [\"legacynotreallyimportant-*\"], \"settings\":  " + Strings.toString(indexSettings) + "}"
+            );
+            legacyTemplateRequest.setOptions(expectWarnings("Legacy index templates are deprecated in favor of composable templates."));
+            assertOK(client().performRequest(legacyTemplateRequest));
+        }
+
+        // put a composable template that needs migrating
+        String composableTemplateToMigrate = "to_migrate_composable_template";
+        {
+            Request toMigrateComposableTemplateReq = new Request("PUT", "/_index_template/" + composableTemplateToMigrate);
+            Settings indexSettings = Settings.builder()
+                .put("index.number_of_shards", 1)
+                .put("index.routing.allocation.require.data", "hot")
+                .build();
+            toMigrateComposableTemplateReq.setJsonEntity(
+                "{\"index_patterns\":  [\"0notreallyimportant-*\"], \"template\":{\"settings\":  " + Strings.toString(indexSettings) + "}}"
+            );
+            assertOK(client().performRequest(toMigrateComposableTemplateReq));
+        }
+
+        // put a composable template that doesn't need migrating
+        String composableTemplate = "no_need_to_migrate_composable_template";
+        {
+            Request composableTemplateRequest = new Request("PUT", "/_index_template/" + composableTemplate);
+            Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build();
+            composableTemplateRequest.setJsonEntity(
+                "{\"index_patterns\":  [\"1notreallyimportant-*\"], \"template\":{\"settings\":  " + Strings.toString(indexSettings) + "}}"
+            );
+            assertOK(client().performRequest(composableTemplateRequest));
+        }
+
+        // put a component template that needs migrating
+        String componentTemplateToMigrate = "to_migrate_component_template";
+        {
+            Request componentTemplateRequest = new Request("PUT", "/_component_template/" + componentTemplateToMigrate);
+            Settings indexSettings = Settings.builder()
+                .put("index.number_of_shards", 1)
+                .put("index.routing.allocation.require.data", "hot")
+                .build();
+            componentTemplateRequest.setJsonEntity("{\"template\":{\"settings\":  " + Strings.toString(indexSettings) + "}}");
+            assertOK(client().performRequest(componentTemplateRequest));
+        }
+
+        // put a component template that doesn't need migrating
+        String componentTemplate = "no_need_to_migrate_component_template";
+        {
+            Request componentTemplateRequest = new Request("PUT", "/_component_template/" + componentTemplate);
+            Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).build();
+            componentTemplateRequest.setJsonEntity("{\"template\":{\"settings\":  " + Strings.toString(indexSettings) + "}}");
+            assertOK(client().performRequest(componentTemplateRequest));
+        }
+
+        boolean dryRun = randomBoolean();
+        if (dryRun == false) {
+            client().performRequest(new Request("POST", "_ilm/stop"));
+            assertBusy(() -> {
+                Response response = client().performRequest(new Request("GET", "_ilm/status"));
+                assertThat(EntityUtils.toString(response.getEntity()), containsString(OperationMode.STOPPED.toString()));
+            });
+        }
+
+        Request migrateRequest = new Request("POST", "_ilm/migrate_to_data_tiers");
+        migrateRequest.addParameter("dry_run", String.valueOf(dryRun));
+        migrateRequest.setJsonEntity("""
+            { "node_attribute": "data"}
+            """);
+        Response migrateDeploymentResponse = client().performRequest(migrateRequest);
+        assertOK(migrateDeploymentResponse);
+
+        Map<String, Object> migrateResponseAsMap = responseAsMap(migrateDeploymentResponse);
+        assertThat(
+            (List<String>) migrateResponseAsMap.get(MigrateToDataTiersResponse.MIGRATED_LEGACY_TEMPLATES.getPreferredName()),
+            is(List.of(legacyTemplateToMigrate))
+        );
+        assertThat(
+            (List<String>) migrateResponseAsMap.get(MigrateToDataTiersResponse.MIGRATED_COMPOSABLE_TEMPLATES.getPreferredName()),
+            is(List.of(composableTemplateToMigrate))
+        );
+        assertThat(
+            (List<String>) migrateResponseAsMap.get(MigrateToDataTiersResponse.MIGRATED_COMPONENT_TEMPLATES.getPreferredName()),
+            is(List.of(componentTemplateToMigrate))
+        );
+        assertThat(migrateResponseAsMap.get(MigrateToDataTiersResponse.DRY_RUN.getPreferredName()), is(dryRun));
+    }
+
     @SuppressWarnings("unchecked")
     public void testMigrationDryRun() throws Exception {
         String templateName = randomAlphaOfLengthBetween(10, 15).toLowerCase(Locale.ROOT);

+ 215 - 6
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java

@@ -13,8 +13,12 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ComponentTemplate;
+import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.metadata.Template;
 import org.elasticsearch.cluster.routing.allocation.DataTier;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
@@ -122,6 +126,49 @@ public final class MetadataMigrateToDataTiersRoutingService {
      *        index.routing.allocation.include._tier_preference: "data_cold,data_warm,data_hot"
      *    }
      *
+     *  - loop through the existing legacy, composable, and component templates and remove all the custom attribute routing settings for
+     *  the configured @param nodeAttrName, if any of the index.routing.allocation.require.{nodeAttrName} or index.routing.allocation
+     *  .include.{nodeAttrName} settings are presents in the template (irrespective of what they are configured to, we do not inspect the
+     *  values in this case).
+     *  Eg. this legacy template:
+     *  {
+     *    "order": 0,
+     *    "index_patterns": [
+     *      "*"
+     *    ],
+     *    "settings": {
+     *      "index": {
+     *        "routing": {
+     *          "allocation": {
+     *            "require": {
+     *              "data": "hot"
+     *            },
+     *            "include": {
+     *               "data": "rack1"
+     *            },
+     *            "exclude": {
+     *               "data": "bad_rack"
+     *            }
+     *          }
+     *        }
+     *      }
+     *    },
+     *    "mappings": {},
+     *    "aliases": {}
+     *  }
+     *  will be migrated to
+     *  {
+     *    "order": 0,
+     *    "index_patterns": [
+     *      "*"
+     *    ],
+     *    "settings": {},
+     *    "mappings": {},
+     *    "aliases": {}
+     *  }
+     *
+     * Same pattern applies to composable and component templates.
+     *
      * If no @param nodeAttrName is provided "data" will be used.
      * If no @param indexTemplateToDelete is provided, no index templates will be deleted.
      *
@@ -174,15 +221,16 @@ public final class MetadataMigrateToDataTiersRoutingService {
             attribute = DEFAULT_NODE_ATTRIBUTE_NAME;
         }
         List<String> migratedPolicies = migrateIlmPolicies(mb, currentState, attribute, xContentRegistry, client, licenseState);
-        // Creating an intermediary cluster state view as when migrating policy we also update the cachesd phase definition stored in the
+        // Creating an intermediary cluster state view as when migrating policy we also update the cached phase definition stored in the
         // index metadata so the metadata.builder will probably contain an already updated view over the indices metadata which we don't
         // want to lose when migrating the indices settings
         ClusterState intermediateState = ClusterState.builder(currentState).metadata(mb).build();
         mb = Metadata.builder(intermediateState.metadata());
         List<String> migratedIndices = migrateIndices(mb, intermediateState, attribute);
+        MigratedTemplates migratedTemplates = migrateIndexAndComponentTemplates(mb, intermediateState, attribute);
         return Tuple.tuple(
             ClusterState.builder(currentState).metadata(mb).build(),
-            new MigratedEntities(removedIndexTemplateName, migratedIndices, migratedPolicies)
+            new MigratedEntities(removedIndexTemplateName, migratedIndices, migratedPolicies, migratedTemplates)
         );
     }
 
@@ -584,6 +632,120 @@ public final class MetadataMigrateToDataTiersRoutingService {
         return newSettingsBuilder.build();
     }
 
+    static MigratedTemplates migrateIndexAndComponentTemplates(Metadata.Builder mb, ClusterState clusterState, String nodeAttrName) {
+        List<String> migratedLegacyTemplates = migrateLegacyTemplates(mb, clusterState, nodeAttrName);
+        List<String> migratedComposableTemplates = migrateComposableTemplates(mb, clusterState, nodeAttrName);
+        List<String> migratedComponentTemplates = migrateComponentTemplates(mb, clusterState, nodeAttrName);
+        return new MigratedTemplates(migratedLegacyTemplates, migratedComposableTemplates, migratedComponentTemplates);
+    }
+
+    static List<String> migrateLegacyTemplates(Metadata.Builder mb, ClusterState clusterState, String nodeAttrName) {
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        List<String> migratedLegacyTemplates = new ArrayList<>();
+
+        for (ObjectObjectCursor<String, IndexTemplateMetadata> templateCursor : clusterState.metadata().templates()) {
+            IndexTemplateMetadata templateMetadata = templateCursor.value;
+            if (templateMetadata.settings().keySet().contains(requireRoutingSetting)
+                || templateMetadata.settings().keySet().contains(includeRoutingSetting)) {
+                IndexTemplateMetadata.Builder templateMetadataBuilder = new IndexTemplateMetadata.Builder(templateMetadata);
+                Settings.Builder settingsBuilder = Settings.builder().put(templateMetadata.settings());
+                settingsBuilder.remove(requireRoutingSetting);
+                settingsBuilder.remove(includeRoutingSetting);
+                settingsBuilder.remove(excludeRoutingSetting);
+                templateMetadataBuilder.settings(settingsBuilder);
+
+                mb.put(templateMetadataBuilder);
+                migratedLegacyTemplates.add(templateCursor.key);
+            }
+        }
+        return migratedLegacyTemplates;
+    }
+
+    static List<String> migrateComposableTemplates(Metadata.Builder mb, ClusterState clusterState, String nodeAttrName) {
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        List<String> migratedComposableTemplates = new ArrayList<>();
+
+        for (Map.Entry<String, ComposableIndexTemplate> templateEntry : clusterState.metadata().templatesV2().entrySet()) {
+            ComposableIndexTemplate composableTemplate = templateEntry.getValue();
+            if (composableTemplate.template() != null && composableTemplate.template().settings() != null) {
+                Settings settings = composableTemplate.template().settings();
+
+                if (settings.keySet().contains(requireRoutingSetting) || settings.keySet().contains(includeRoutingSetting)) {
+                    Template currentInnerTemplate = composableTemplate.template();
+                    ComposableIndexTemplate.Builder migratedComposableTemplateBuilder = new ComposableIndexTemplate.Builder();
+                    Settings.Builder settingsBuilder = Settings.builder().put(settings);
+                    settingsBuilder.remove(requireRoutingSetting);
+                    settingsBuilder.remove(includeRoutingSetting);
+                    settingsBuilder.remove(excludeRoutingSetting);
+                    Template migratedInnerTemplate = new Template(
+                        settingsBuilder.build(),
+                        currentInnerTemplate.mappings(),
+                        currentInnerTemplate.aliases()
+                    );
+
+                    migratedComposableTemplateBuilder.indexPatterns(composableTemplate.indexPatterns());
+                    migratedComposableTemplateBuilder.template(migratedInnerTemplate);
+                    migratedComposableTemplateBuilder.componentTemplates(composableTemplate.composedOf());
+                    migratedComposableTemplateBuilder.priority(composableTemplate.priority());
+                    migratedComposableTemplateBuilder.version(composableTemplate.version());
+                    migratedComposableTemplateBuilder.metadata(composableTemplate.metadata());
+                    migratedComposableTemplateBuilder.dataStreamTemplate(composableTemplate.getDataStreamTemplate());
+                    migratedComposableTemplateBuilder.allowAutoCreate(composableTemplate.getAllowAutoCreate());
+
+                    mb.put(templateEntry.getKey(), migratedComposableTemplateBuilder.build());
+                    migratedComposableTemplates.add(templateEntry.getKey());
+                }
+            }
+        }
+
+        return migratedComposableTemplates;
+    }
+
+    static List<String> migrateComponentTemplates(Metadata.Builder mb, ClusterState clusterState, String nodeAttrName) {
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        List<String> migratedComponentTemplates = new ArrayList<>();
+
+        for (Map.Entry<String, ComponentTemplate> componentEntry : clusterState.metadata().componentTemplates().entrySet()) {
+            ComponentTemplate componentTemplate = componentEntry.getValue();
+            if (componentTemplate.template() != null && componentTemplate.template().settings() != null) {
+                Settings settings = componentTemplate.template().settings();
+
+                if (settings.keySet().contains(requireRoutingSetting) || settings.keySet().contains(includeRoutingSetting)) {
+                    Template currentInnerTemplate = componentTemplate.template();
+                    Settings.Builder settingsBuilder = Settings.builder().put(settings);
+                    settingsBuilder.remove(requireRoutingSetting);
+                    settingsBuilder.remove(includeRoutingSetting);
+                    settingsBuilder.remove(excludeRoutingSetting);
+                    Template migratedInnerTemplate = new Template(
+                        settingsBuilder.build(),
+                        currentInnerTemplate.mappings(),
+                        currentInnerTemplate.aliases()
+                    );
+
+                    ComponentTemplate migratedComponentTemplate = new ComponentTemplate(
+                        migratedInnerTemplate,
+                        componentTemplate.version(),
+                        componentTemplate.metadata()
+                    );
+
+                    mb.put(componentEntry.getKey(), migratedComponentTemplate);
+                    migratedComponentTemplates.add(componentEntry.getKey());
+                }
+            }
+        }
+
+        return migratedComponentTemplates;
+    }
+
     private static Settings migrateToDefaultTierPreference(ClusterState currentState, IndexMetadata indexMetadata) {
         Settings currentIndexSettings = indexMetadata.getSettings();
         List<String> tierPreference = DataTier.parseTierList(currentIndexSettings.get(DataTier.TIER_PREFERENCE));
@@ -637,11 +799,18 @@ public final class MetadataMigrateToDataTiersRoutingService {
         public final String removedIndexTemplateName;
         public final List<String> migratedIndices;
         public final List<String> migratedPolicies;
-
-        public MigratedEntities(@Nullable String removedIndexTemplateName, List<String> migratedIndices, List<String> migratedPolicies) {
+        public final MigratedTemplates migratedTemplates;
+
+        public MigratedEntities(
+            @Nullable String removedIndexTemplateName,
+            List<String> migratedIndices,
+            List<String> migratedPolicies,
+            MigratedTemplates migratedTemplates
+        ) {
             this.removedIndexTemplateName = removedIndexTemplateName;
             this.migratedIndices = Collections.unmodifiableList(migratedIndices);
             this.migratedPolicies = Collections.unmodifiableList(migratedPolicies);
+            this.migratedTemplates = migratedTemplates;
         }
 
         @Override
@@ -655,12 +824,52 @@ public final class MetadataMigrateToDataTiersRoutingService {
             MigratedEntities that = (MigratedEntities) o;
             return Objects.equals(removedIndexTemplateName, that.removedIndexTemplateName)
                 && Objects.equals(migratedIndices, that.migratedIndices)
-                && Objects.equals(migratedPolicies, that.migratedPolicies);
+                && Objects.equals(migratedPolicies, that.migratedPolicies)
+                && Objects.equals(migratedTemplates, that.migratedTemplates);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(removedIndexTemplateName, migratedIndices, migratedPolicies, migratedTemplates);
+        }
+    }
+
+    /**
+     * Represents the legacy, composable, and component templates that were migrated away from shard allocation settings based on custom
+     * node attributes.
+     */
+    public static final class MigratedTemplates {
+        public final List<String> migratedLegacyTemplates;
+        public final List<String> migratedComposableTemplates;
+        public final List<String> migratedComponentTemplates;
+
+        public MigratedTemplates(
+            List<String> migratedLegacyTemplates,
+            List<String> migratedComposableTemplates,
+            List<String> migratedComponentTemplates
+        ) {
+            this.migratedLegacyTemplates = Collections.unmodifiableList(migratedLegacyTemplates);
+            this.migratedComposableTemplates = Collections.unmodifiableList(migratedComposableTemplates);
+            this.migratedComponentTemplates = Collections.unmodifiableList(migratedComponentTemplates);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            MigratedTemplates that = (MigratedTemplates) o;
+            return Objects.equals(migratedLegacyTemplates, that.migratedLegacyTemplates)
+                && Objects.equals(migratedComposableTemplates, that.migratedComposableTemplates)
+                && Objects.equals(migratedComponentTemplates, that.migratedComponentTemplates);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(removedIndexTemplateName, migratedIndices, migratedPolicies);
+            return Objects.hash(migratedLegacyTemplates, migratedComposableTemplates, migratedComponentTemplates);
         }
     }
 }

+ 14 - 1
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java

@@ -29,6 +29,7 @@ import org.elasticsearch.xcontent.NamedXContentRegistry;
 import org.elasticsearch.xpack.cluster.action.MigrateToDataTiersAction;
 import org.elasticsearch.xpack.cluster.action.MigrateToDataTiersRequest;
 import org.elasticsearch.xpack.cluster.action.MigrateToDataTiersResponse;
+import org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService;
 import org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.MigratedEntities;
 import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata;
 
@@ -85,8 +86,17 @@ public class TransportMigrateToDataTiersAction extends TransportMasterNodeAction
                 licenseState,
                 request.isDryRun()
             ).v2();
+            MetadataMigrateToDataTiersRoutingService.MigratedTemplates migratedTemplates = entities.migratedTemplates;
             listener.onResponse(
-                new MigrateToDataTiersResponse(entities.removedIndexTemplateName, entities.migratedPolicies, entities.migratedIndices, true)
+                new MigrateToDataTiersResponse(
+                    entities.removedIndexTemplateName,
+                    entities.migratedPolicies,
+                    entities.migratedIndices,
+                    entities.migratedTemplates.migratedLegacyTemplates,
+                    entities.migratedTemplates.migratedComposableTemplates,
+                    entities.migratedTemplates.migratedComponentTemplates,
+                    true
+                )
             );
             return;
         }
@@ -133,6 +143,9 @@ public class TransportMigrateToDataTiersAction extends TransportMasterNodeAction
                         entities.removedIndexTemplateName,
                         entities.migratedPolicies,
                         entities.migratedIndices,
+                        entities.migratedTemplates.migratedLegacyTemplates,
+                        entities.migratedTemplates.migratedComposableTemplates,
+                        entities.migratedTemplates.migratedComponentTemplates,
                         false
                     )
                 );

+ 394 - 1
x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java

@@ -11,12 +11,14 @@ import org.elasticsearch.Version;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.ComponentTemplate;
 import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.Template;
 import org.elasticsearch.cluster.routing.allocation.DataTier;
+import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.core.TimeValue;
@@ -57,6 +59,7 @@ import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTier
 import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateIndices;
 import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateToDataTiersRouting;
 import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.is;
@@ -1104,7 +1107,8 @@ public class MetadataMigrateToDataTiersRoutingServiceTests extends ESTestCase {
             false
         );
         assertThat(migratedEntitiesTuple.v2().removedIndexTemplateName, nullValue());
-        assertThat(migratedEntitiesTuple.v1().metadata().templatesV2().get(composableTemplateName), is(composableIndexTemplate));
+        // the composable template still exists, however it was migrated to not use the custom require.data routing setting
+        assertThat(migratedEntitiesTuple.v1().metadata().templatesV2().get(composableTemplateName), is(notNullValue()));
     }
 
     public void testMigrationSetsEnforceTierPreferenceToTrue() {
@@ -1158,6 +1162,395 @@ public class MetadataMigrateToDataTiersRoutingServiceTests extends ESTestCase {
         return new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong());
     }
 
+    public void testMigrateLegacyIndexTemplates() {
+        String nodeAttrName = "data";
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        IndexTemplateMetadata templateWithRequireRouting = new IndexTemplateMetadata(
+            "template-with-require-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder().put(requireRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        IndexTemplateMetadata templateWithIncludeRouting = new IndexTemplateMetadata(
+            "template-with-include-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder().put(includeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        IndexTemplateMetadata templateWithExcludeRouting = new IndexTemplateMetadata(
+            "template-with-exclude-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder().put(excludeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        IndexTemplateMetadata templateWithRequireAndIncludeRoutings = new IndexTemplateMetadata(
+            "template-with-require-and-include-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder()
+                .put(requireRoutingSetting, "hot")
+                .put(includeRoutingSetting, "rack1")
+                .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                .build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        IndexTemplateMetadata templateWithoutCustomRoutings = new IndexTemplateMetadata(
+            "template-without-custom-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder()
+                .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                .put(LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE, true)
+                .build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT)
+            .metadata(
+                Metadata.builder()
+                    .put(templateWithRequireRouting)
+                    .put(templateWithIncludeRouting)
+                    .put(templateWithRequireAndIncludeRoutings)
+                    .put(templateWithExcludeRouting)
+                    .put(templateWithoutCustomRoutings)
+                    .build()
+            )
+            .build();
+
+        Metadata.Builder mb = Metadata.builder(clusterState.metadata());
+        List<String> migrateLegacyTemplates = MetadataMigrateToDataTiersRoutingService.migrateLegacyTemplates(
+            mb,
+            clusterState,
+            nodeAttrName
+        );
+        assertThat(migrateLegacyTemplates.size(), is(3));
+        assertThat(
+            migrateLegacyTemplates,
+            containsInAnyOrder(
+                "template-with-require-routing",
+                "template-with-include-routing",
+                "template-with-require-and-include-routing"
+            )
+        );
+
+        ImmutableOpenMap<String, IndexTemplateMetadata> migratedTemplates = mb.build().templates();
+        assertThat(migratedTemplates.get("template-with-require-routing").settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-include-routing").settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-require-and-include-routing").settings().size(), is(1));
+
+        // these templates shouldn't have been updated, so the settings size should still be 2
+        assertThat(migratedTemplates.get("template-without-custom-routing").settings().size(), is(2));
+        assertThat(migratedTemplates.get("template-with-exclude-routing").settings().size(), is(2));
+    }
+
+    public void testMigrateComposableIndexTemplates() {
+        String nodeAttrName = "data";
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        ComposableIndexTemplate templateWithRequireRouting = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(
+                Settings.builder().put(requireRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            List.of(),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ComposableIndexTemplate templateWithIncludeRouting = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(
+                Settings.builder().put(includeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            List.of(),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ComposableIndexTemplate templateWithExcludeRouting = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(
+                Settings.builder().put(excludeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            List.of(),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ComposableIndexTemplate templateWithRequireAndIncludeRoutings = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(
+                Settings.builder()
+                    .put(requireRoutingSetting, "hot")
+                    .put(includeRoutingSetting, "rack1")
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .build(),
+                null,
+                null
+            ),
+            List.of(),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ComposableIndexTemplate templateWithoutCustomRoutings = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(
+                Settings.builder()
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .put(LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE, true)
+                    .build(),
+                null,
+                null
+            ),
+            List.of(),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT)
+            .metadata(
+                Metadata.builder()
+                    .put("template-with-require-routing", templateWithRequireRouting)
+                    .put("template-with-include-routing", templateWithIncludeRouting)
+                    .put("template-with-exclude-routing", templateWithExcludeRouting)
+                    .put("template-with-require-and-include-routing", templateWithRequireAndIncludeRoutings)
+                    .put("template-without-custom-routing", templateWithoutCustomRoutings)
+                    .build()
+            )
+            .build();
+
+        Metadata.Builder mb = Metadata.builder(clusterState.metadata());
+        List<String> migratedComposableTemplates = MetadataMigrateToDataTiersRoutingService.migrateComposableTemplates(
+            mb,
+            clusterState,
+            nodeAttrName
+        );
+        assertThat(migratedComposableTemplates.size(), is(3));
+        assertThat(
+            migratedComposableTemplates,
+            containsInAnyOrder(
+                "template-with-require-routing",
+                "template-with-include-routing",
+                "template-with-require-and-include-routing"
+            )
+        );
+
+        Map<String, ComposableIndexTemplate> migratedTemplates = mb.build().templatesV2();
+        assertThat(migratedTemplates.get("template-with-require-routing").template().settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-include-routing").template().settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-require-and-include-routing").template().settings().size(), is(1));
+
+        // these templates shouldn't have been updated, so the settings size should still be 2
+        assertThat(migratedTemplates.get("template-without-custom-routing").template().settings().size(), is(2));
+        assertThat(migratedTemplates.get("template-with-exclude-routing").template().settings().size(), is(2));
+    }
+
+    public void testMigrateComponentTemplates() {
+        String nodeAttrName = "data";
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        ComponentTemplate compTemplateWithRequireRouting = new ComponentTemplate(
+            new Template(
+                Settings.builder().put(requireRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ComponentTemplate compTemplateWithIncludeRouting = new ComponentTemplate(
+            new Template(
+                Settings.builder().put(includeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ComponentTemplate compTemplateWithExcludeRouting = new ComponentTemplate(
+            new Template(
+                Settings.builder().put(excludeRoutingSetting, "hot").put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle").build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ComponentTemplate compTemplateWithRequireAndIncludeRoutings = new ComponentTemplate(
+            new Template(
+                Settings.builder()
+                    .put(requireRoutingSetting, "hot")
+                    .put(includeRoutingSetting, "rack1")
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ComponentTemplate compTemplateWithoutCustomRoutings = new ComponentTemplate(
+            new Template(
+                Settings.builder()
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .put(LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE, true)
+                    .build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT)
+            .metadata(
+                Metadata.builder()
+                    .put("template-with-require-routing", compTemplateWithRequireRouting)
+                    .put("template-with-include-routing", compTemplateWithIncludeRouting)
+                    .put("template-with-exclude-routing", compTemplateWithExcludeRouting)
+                    .put("template-with-require-and-include-routing", compTemplateWithRequireAndIncludeRoutings)
+                    .put("template-without-custom-routing", compTemplateWithoutCustomRoutings)
+                    .build()
+            )
+            .build();
+
+        Metadata.Builder mb = Metadata.builder(clusterState.metadata());
+        List<String> migratedComponentTemplates = MetadataMigrateToDataTiersRoutingService.migrateComponentTemplates(
+            mb,
+            clusterState,
+            nodeAttrName
+        );
+        assertThat(migratedComponentTemplates.size(), is(3));
+        assertThat(
+            migratedComponentTemplates,
+            containsInAnyOrder(
+                "template-with-require-routing",
+                "template-with-include-routing",
+                "template-with-require-and-include-routing"
+            )
+        );
+
+        Map<String, ComponentTemplate> migratedTemplates = mb.build().componentTemplates();
+        assertThat(migratedTemplates.get("template-with-require-routing").template().settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-include-routing").template().settings().size(), is(1));
+        assertThat(migratedTemplates.get("template-with-require-and-include-routing").template().settings().size(), is(1));
+
+        // these templates shouldn't have been updated, so the settings size should still be 2
+        assertThat(migratedTemplates.get("template-without-custom-routing").template().settings().size(), is(2));
+        assertThat(migratedTemplates.get("template-with-exclude-routing").template().settings().size(), is(2));
+    }
+
+    public void testMigrateIndexAndComponentTemplates() {
+        String nodeAttrName = "data";
+        String requireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName;
+        String includeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+        String excludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName;
+
+        IndexTemplateMetadata legacyTemplateWithRequireRouting = new IndexTemplateMetadata(
+            "template-with-require-routing",
+            randomInt(),
+            randomInt(),
+            List.of("test-*"),
+            Settings.builder().put(requireRoutingSetting, "hot").build(),
+            ImmutableOpenMap.of(),
+            ImmutableOpenMap.of()
+        );
+
+        ComponentTemplate compTemplateWithoutCustomRoutings = new ComponentTemplate(
+            new Template(
+                Settings.builder()
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .put(LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE, true)
+                    .build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ComposableIndexTemplate composableTemplateWithRequireRouting = new ComposableIndexTemplate(
+            List.of("test-*"),
+            new Template(Settings.builder().put(requireRoutingSetting, "hot").build(), null, null),
+            List.of("component-template-without-custom-routing"),
+            randomLong(),
+            randomLong(),
+            null
+        );
+
+        ComponentTemplate compTemplateWithRequireAndIncludeRoutings = new ComponentTemplate(
+            new Template(
+                Settings.builder()
+                    .put(requireRoutingSetting, "hot")
+                    .put(includeRoutingSetting, "rack1")
+                    .put(LifecycleSettings.LIFECYCLE_NAME, "testLifecycle")
+                    .build(),
+                null,
+                null
+            ),
+            randomLong(),
+            null
+        );
+
+        ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT)
+            .metadata(
+                Metadata.builder()
+                    .put(legacyTemplateWithRequireRouting)
+                    .put("composable-template-with-require-routing", composableTemplateWithRequireRouting)
+                    .put("component-with-require-and-include-routing", compTemplateWithRequireAndIncludeRoutings)
+                    .put("component-template-without-custom-routing", compTemplateWithoutCustomRoutings)
+                    .build()
+            )
+            .build();
+
+        Metadata.Builder mb = Metadata.builder(clusterState.metadata());
+        MetadataMigrateToDataTiersRoutingService.MigratedTemplates migratedTemplates = MetadataMigrateToDataTiersRoutingService
+            .migrateIndexAndComponentTemplates(mb, clusterState, nodeAttrName);
+        assertThat(migratedTemplates.migratedLegacyTemplates, is(List.of("template-with-require-routing")));
+        assertThat(migratedTemplates.migratedComposableTemplates, is(List.of("composable-template-with-require-routing")));
+        assertThat(migratedTemplates.migratedComponentTemplates, is(List.of("component-with-require-and-include-routing")));
+    }
+
     private String getWarmPhaseDef() {
         return """
             {