@@ -195,15 +195,21 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean
195195
196196 validateTemplate (finalSettings , stringMappings , indicesService , xContentRegistry );
197197
198+ // Collect all the composable (index) templates that use this component template, we'll use
199+ // this for validating that they're still going to be valid after this component template
200+ // has been updated
201+ final Map <String , ComposableIndexTemplate > templatesUsingComponent = currentState .metadata ().templatesV2 ().entrySet ().stream ()
202+ .filter (e -> e .getValue ().composedOf ().contains (name ))
203+ .collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
204+
198205 // if we're updating a component template, let's check if it's part of any V2 template that will yield the CT update invalid
199206 if (create == false && finalSettings != null ) {
200207 // if the CT is specifying the `index.hidden` setting it cannot be part of any global template
201208 if (IndexMetadata .INDEX_HIDDEN_SETTING .exists (finalSettings )) {
202- Map <String , ComposableIndexTemplate > existingTemplates = currentState .metadata ().templatesV2 ();
203209 List <String > globalTemplatesThatUseThisComponent = new ArrayList <>();
204- for (Map .Entry <String , ComposableIndexTemplate > entry : existingTemplates .entrySet ()) {
210+ for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
205211 ComposableIndexTemplate templateV2 = entry .getValue ();
206- if (templateV2 .composedOf (). contains ( name ) && templateV2 . indexPatterns ().stream ().anyMatch (Regex ::isMatchAllPattern )) {
212+ if (templateV2 .indexPatterns ().stream ().anyMatch (Regex ::isMatchAllPattern )) {
207213 // global templates don't support configuring the `index.hidden` setting so we don't need to resolve the settings as
208214 // no other component template can remove this setting from the resolved settings, so just invalidate this update
209215 globalTemplatesThatUseThisComponent .add (entry .getKey ());
@@ -233,6 +239,32 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean
233239 stringMappings == null ? null : new CompressedXContent (stringMappings ), template .template ().aliases ());
234240 final ComponentTemplate finalComponentTemplate = new ComponentTemplate (finalTemplate , template .version (), template .metadata ());
235241 validate (name , finalComponentTemplate );
242+
243+ if (templatesUsingComponent .size () > 0 ) {
244+ ClusterState tempStateWithComponentTemplateAdded = ClusterState .builder (currentState )
245+ .metadata (Metadata .builder (currentState .metadata ()).put (name , finalComponentTemplate ))
246+ .build ();
247+ Exception validationFailure = null ;
248+ for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
249+ final String composableTemplateName = entry .getKey ();
250+ final ComposableIndexTemplate composableTemplate = entry .getValue ();
251+ try {
252+ validateCompositeTemplate (tempStateWithComponentTemplateAdded , composableTemplateName ,
253+ composableTemplate , indicesService , xContentRegistry );
254+ } catch (Exception e ) {
255+ if (validationFailure == null ) {
256+ validationFailure = new IllegalArgumentException ("updating component template [" + name +
257+ "] results in invalid composable template [" + composableTemplateName + "] after templates are merged" , e );
258+ } else {
259+ validationFailure .addSuppressed (e );
260+ }
261+ }
262+ }
263+ if (validationFailure != null ) {
264+ throw validationFailure ;
265+ }
266+ }
267+
236268 logger .info ("adding component template [{}]" , name );
237269 return ClusterState .builder (currentState )
238270 .metadata (Metadata .builder (currentState .metadata ()).put (name , finalComponentTemplate ))
@@ -422,7 +454,6 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo
422454 // adjusted (to add _doc) and it should be validated
423455 CompressedXContent mappings = innerTemplate .mappings ();
424456 String stringMappings = mappings == null ? null : mappings .string ();
425- validateTemplate (finalSettings , stringMappings , indicesService , xContentRegistry );
426457
427458 // Mappings in index templates don't include _doc, so update the mappings to include this single type
428459 if (stringMappings != null ) {
@@ -441,6 +472,17 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo
441472 }
442473
443474 validate (name , finalIndexTemplate );
475+
476+ // Finally, right before adding the template, we need to ensure that the composite settings,
477+ // mappings, and aliases are valid after it's been composed with the component templates
478+ try {
479+ validateCompositeTemplate (currentState , name , finalIndexTemplate , indicesService , xContentRegistry );
480+ } catch (Exception e ) {
481+ throw new IllegalArgumentException ("composable template [" + name +
482+ "] template after composition " +
483+ (finalIndexTemplate .composedOf ().size () > 0 ? "with component templates " + finalIndexTemplate .composedOf () + " " : "" ) +
484+ "is invalid" , e );
485+ }
444486 logger .info ("adding index template [{}]" , name );
445487 return ClusterState .builder (currentState )
446488 .metadata (Metadata .builder (currentState .metadata ()).put (name , finalIndexTemplate ))
@@ -803,7 +845,6 @@ public static List<CompressedXContent> resolveMappings(final ClusterState state,
803845 return List .of ();
804846 }
805847 final Map <String , ComponentTemplate > componentTemplates = state .metadata ().componentTemplates ();
806- // TODO: more fine-grained merging of component template mappings, ie, merge fields as distint entities
807848 List <CompressedXContent > mappings = template .composedOf ().stream ()
808849 .map (componentTemplates ::get )
809850 .filter (Objects ::nonNull )
@@ -910,6 +951,77 @@ public static List<Map<String, AliasMetadata>> resolveAliases(final Metadata met
910951 return Collections .unmodifiableList (aliases );
911952 }
912953
954+ /**
955+ * Given a state and a composable template, validate that the final composite template
956+ * generated by the composable template and all of its component templates contains valid
957+ * settings, mappings, and aliases.
958+ */
959+ private static void validateCompositeTemplate (final ClusterState state ,
960+ final String templateName ,
961+ final ComposableIndexTemplate template ,
962+ final IndicesService indicesService ,
963+ final NamedXContentRegistry xContentRegistry ) throws Exception {
964+ final ClusterState stateWithTemplate = ClusterState .builder (state )
965+ .metadata (Metadata .builder (state .metadata ()).put (templateName , template ))
966+ .build ();
967+
968+ Index createdIndex = null ;
969+ final String temporaryIndexName = "validate-template-" + UUIDs .randomBase64UUID ().toLowerCase (Locale .ROOT );
970+ try {
971+ Settings resolvedSettings = resolveSettings (stateWithTemplate .metadata (), templateName );
972+
973+ // use the provided values, otherwise just pick valid dummy values
974+ int dummyPartitionSize = IndexMetadata .INDEX_ROUTING_PARTITION_SIZE_SETTING .get (resolvedSettings );
975+ int dummyShards = resolvedSettings .getAsInt (IndexMetadata .SETTING_NUMBER_OF_SHARDS ,
976+ dummyPartitionSize == 1 ? 1 : dummyPartitionSize + 1 );
977+ int shardReplicas = resolvedSettings .getAsInt (IndexMetadata .SETTING_NUMBER_OF_REPLICAS , 0 );
978+
979+
980+ //create index service for parsing and validating "mappings"
981+ Settings dummySettings = Settings .builder ()
982+ .put (IndexMetadata .SETTING_VERSION_CREATED , Version .CURRENT )
983+ .put (resolvedSettings )
984+ .put (IndexMetadata .SETTING_NUMBER_OF_SHARDS , dummyShards )
985+ .put (IndexMetadata .SETTING_NUMBER_OF_REPLICAS , shardReplicas )
986+ .put (IndexMetadata .SETTING_INDEX_UUID , UUIDs .randomBase64UUID ())
987+ .build ();
988+
989+ // Validate index metadata (settings)
990+ final ClusterState stateWithIndex = ClusterState .builder (stateWithTemplate )
991+ .metadata (Metadata .builder (stateWithTemplate .metadata ())
992+ .put (IndexMetadata .builder (temporaryIndexName ).settings (dummySettings ))
993+ .build ())
994+ .build ();
995+ final IndexMetadata tmpIndexMetadata = stateWithIndex .metadata ().index (temporaryIndexName );
996+ IndexService dummyIndexService = indicesService .createIndex (tmpIndexMetadata , Collections .emptyList (), false );
997+ createdIndex = dummyIndexService .index ();
998+
999+ // Validate aliases
1000+ MetadataCreateIndexService .resolveAndValidateAliases (temporaryIndexName , Collections .emptySet (),
1001+ MetadataIndexTemplateService .resolveAliases (stateWithIndex .metadata (), templateName ), stateWithIndex .metadata (),
1002+ new AliasValidator (),
1003+ // the context is only used for validation so it's fine to pass fake values for the
1004+ // shard id and the current timestamp
1005+ xContentRegistry , dummyIndexService .newQueryShardContext (0 , null , () -> 0L , null ));
1006+
1007+ // Parse mappings to ensure they are valid after being composed
1008+ List <CompressedXContent > mappings = resolveMappings (stateWithIndex , templateName );
1009+ Map <String , Object > finalMappings = MetadataCreateIndexService .parseV2Mappings ("{}" , mappings , xContentRegistry );
1010+ MapperService dummyMapperService = dummyIndexService .mapperService ();
1011+ if (finalMappings .isEmpty () == false ) {
1012+ assert finalMappings .size () == 1 : finalMappings ;
1013+ // TODO: Eventually change this to:
1014+ // dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MergeReason.INDEX_TEMPLATE);
1015+ dummyMapperService .merge (MapperService .SINGLE_MAPPING_NAME , finalMappings , MergeReason .MAPPING_UPDATE );
1016+ }
1017+
1018+ } finally {
1019+ if (createdIndex != null ) {
1020+ indicesService .removeIndex (createdIndex , NO_LONGER_ASSIGNED , "created for validating template addition" );
1021+ }
1022+ }
1023+ }
1024+
9131025 private static void validateTemplate (Settings validateSettings , String mappings ,
9141026 IndicesService indicesService , NamedXContentRegistry xContentRegistry ) throws Exception {
9151027 // First check to see if mappings are valid XContent
0 commit comments