@@ -196,15 +196,21 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean
196
196
197
197
validateTemplate (finalSettings , stringMappings , indicesService , xContentRegistry );
198
198
199
+ // Collect all the composable (index) templates that use this component template, we'll use
200
+ // this for validating that they're still going to be valid after this component template
201
+ // has been updated
202
+ final Map <String , ComposableIndexTemplate > templatesUsingComponent = currentState .metadata ().templatesV2 ().entrySet ().stream ()
203
+ .filter (e -> e .getValue ().composedOf ().contains (name ))
204
+ .collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
205
+
199
206
// 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
200
207
if (create == false && finalSettings != null ) {
201
208
// if the CT is specifying the `index.hidden` setting it cannot be part of any global template
202
209
if (IndexMetadata .INDEX_HIDDEN_SETTING .exists (finalSettings )) {
203
- Map <String , ComposableIndexTemplate > existingTemplates = currentState .metadata ().templatesV2 ();
204
210
List <String > globalTemplatesThatUseThisComponent = new ArrayList <>();
205
- for (Map .Entry <String , ComposableIndexTemplate > entry : existingTemplates .entrySet ()) {
211
+ for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
206
212
ComposableIndexTemplate templateV2 = entry .getValue ();
207
- if (templateV2 .composedOf (). contains ( name ) && templateV2 . indexPatterns ().stream ().anyMatch (Regex ::isMatchAllPattern )) {
213
+ if (templateV2 .indexPatterns ().stream ().anyMatch (Regex ::isMatchAllPattern )) {
208
214
// global templates don't support configuring the `index.hidden` setting so we don't need to resolve the settings as
209
215
// no other component template can remove this setting from the resolved settings, so just invalidate this update
210
216
globalTemplatesThatUseThisComponent .add (entry .getKey ());
@@ -234,6 +240,32 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean
234
240
stringMappings == null ? null : new CompressedXContent (stringMappings ), template .template ().aliases ());
235
241
final ComponentTemplate finalComponentTemplate = new ComponentTemplate (finalTemplate , template .version (), template .metadata ());
236
242
validate (name , finalComponentTemplate );
243
+
244
+ if (templatesUsingComponent .size () > 0 ) {
245
+ ClusterState tempStateWithComponentTemplateAdded = ClusterState .builder (currentState )
246
+ .metadata (Metadata .builder (currentState .metadata ()).put (name , finalComponentTemplate ))
247
+ .build ();
248
+ Exception validationFailure = null ;
249
+ for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
250
+ final String composableTemplateName = entry .getKey ();
251
+ final ComposableIndexTemplate composableTemplate = entry .getValue ();
252
+ try {
253
+ validateCompositeTemplate (tempStateWithComponentTemplateAdded , composableTemplateName ,
254
+ composableTemplate , indicesService , xContentRegistry );
255
+ } catch (Exception e ) {
256
+ if (validationFailure == null ) {
257
+ validationFailure = new IllegalArgumentException ("updating component template [" + name +
258
+ "] results in invalid composable template [" + composableTemplateName + "] after templates are merged" , e );
259
+ } else {
260
+ validationFailure .addSuppressed (e );
261
+ }
262
+ }
263
+ }
264
+ if (validationFailure != null ) {
265
+ throw validationFailure ;
266
+ }
267
+ }
268
+
237
269
logger .info ("adding component template [{}]" , name );
238
270
return ClusterState .builder (currentState )
239
271
.metadata (Metadata .builder (currentState .metadata ()).put (name , finalComponentTemplate ))
@@ -385,7 +417,6 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo
385
417
// adjusted (to add _doc) and it should be validated
386
418
CompressedXContent mappings = innerTemplate .mappings ();
387
419
String stringMappings = mappings == null ? null : mappings .string ();
388
- validateTemplate (finalSettings , stringMappings , indicesService , xContentRegistry );
389
420
390
421
// Mappings in index templates don't include _doc, so update the mappings to include this single type
391
422
if (stringMappings != null ) {
@@ -404,6 +435,17 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo
404
435
}
405
436
406
437
validate (name , finalIndexTemplate );
438
+
439
+ // Finally, right before adding the template, we need to ensure that the composite settings,
440
+ // mappings, and aliases are valid after it's been composed with the component templates
441
+ try {
442
+ validateCompositeTemplate (currentState , name , finalIndexTemplate , indicesService , xContentRegistry );
443
+ } catch (Exception e ) {
444
+ throw new IllegalArgumentException ("composable template [" + name +
445
+ "] template after composition " +
446
+ (finalIndexTemplate .composedOf ().size () > 0 ? "with component templates " + finalIndexTemplate .composedOf () + " " : "" ) +
447
+ "is invalid" , e );
448
+ }
407
449
logger .info ("adding index template [{}]" , name );
408
450
return ClusterState .builder (currentState )
409
451
.metadata (Metadata .builder (currentState .metadata ()).put (name , finalIndexTemplate ))
@@ -748,7 +790,6 @@ public static List<CompressedXContent> resolveMappings(final ClusterState state,
748
790
return Collections .emptyList ();
749
791
}
750
792
final Map <String , ComponentTemplate > componentTemplates = state .metadata ().componentTemplates ();
751
- // TODO: more fine-grained merging of component template mappings, ie, merge fields as distint entities
752
793
List <CompressedXContent > mappings = template .composedOf ().stream ()
753
794
.map (componentTemplates ::get )
754
795
.filter (Objects ::nonNull )
@@ -855,6 +896,72 @@ public static List<Map<String, AliasMetadata>> resolveAliases(final Metadata met
855
896
return Collections .unmodifiableList (aliases );
856
897
}
857
898
899
+ /**
900
+ * Given a state and a composable template, validate that the final composite template
901
+ * generated by the composable template and all of its component templates contains valid
902
+ * settings, mappings, and aliases.
903
+ */
904
+ private static void validateCompositeTemplate (final ClusterState state ,
905
+ final String templateName ,
906
+ final ComposableIndexTemplate template ,
907
+ final IndicesService indicesService ,
908
+ final NamedXContentRegistry xContentRegistry ) throws Exception {
909
+ final ClusterState stateWithTemplate = ClusterState .builder (state )
910
+ .metadata (Metadata .builder (state .metadata ()).put (templateName , template ))
911
+ .build ();
912
+
913
+ final String temporaryIndexName = "validate-template-" + UUIDs .randomBase64UUID ().toLowerCase (Locale .ROOT );
914
+ Settings resolvedSettings = resolveSettings (stateWithTemplate .metadata (), templateName );
915
+
916
+ // use the provided values, otherwise just pick valid dummy values
917
+ int dummyPartitionSize = IndexMetadata .INDEX_ROUTING_PARTITION_SIZE_SETTING .get (resolvedSettings );
918
+ int dummyShards = resolvedSettings .getAsInt (IndexMetadata .SETTING_NUMBER_OF_SHARDS ,
919
+ dummyPartitionSize == 1 ? 1 : dummyPartitionSize + 1 );
920
+ int shardReplicas = resolvedSettings .getAsInt (IndexMetadata .SETTING_NUMBER_OF_REPLICAS , 0 );
921
+
922
+
923
+ // Create the final aggregate settings, which will be used to create the temporary index metadata to validate everything
924
+ Settings finalResolvedSettings = Settings .builder ()
925
+ .put (IndexMetadata .SETTING_VERSION_CREATED , Version .CURRENT )
926
+ .put (resolvedSettings )
927
+ .put (IndexMetadata .SETTING_NUMBER_OF_SHARDS , dummyShards )
928
+ .put (IndexMetadata .SETTING_NUMBER_OF_REPLICAS , shardReplicas )
929
+ .put (IndexMetadata .SETTING_INDEX_UUID , UUIDs .randomBase64UUID ())
930
+ .build ();
931
+
932
+ // Validate index metadata (settings)
933
+ final ClusterState stateWithIndex = ClusterState .builder (stateWithTemplate )
934
+ .metadata (Metadata .builder (stateWithTemplate .metadata ())
935
+ .put (IndexMetadata .builder (temporaryIndexName ).settings (finalResolvedSettings ))
936
+ .build ())
937
+ .build ();
938
+ final IndexMetadata tmpIndexMetadata = stateWithIndex .metadata ().index (temporaryIndexName );
939
+ indicesService .withTempIndexService (tmpIndexMetadata ,
940
+ tempIndexService -> {
941
+ // Validate aliases
942
+ MetadataCreateIndexService .resolveAndValidateAliases (temporaryIndexName , Collections .emptySet (),
943
+ MetadataIndexTemplateService .resolveAliases (stateWithIndex .metadata (), templateName ), stateWithIndex .metadata (),
944
+ new AliasValidator (),
945
+ // the context is only used for validation so it's fine to pass fake values for the
946
+ // shard id and the current timestamp
947
+ xContentRegistry , tempIndexService .newQueryShardContext (0 , null , () -> 0L , null ));
948
+
949
+ // Parse mappings to ensure they are valid after being composed
950
+ List <CompressedXContent > mappings = resolveMappings (stateWithIndex , templateName );
951
+ try {
952
+ MapperService dummyMapperService = tempIndexService .mapperService ();
953
+ for (CompressedXContent mapping : mappings ) {
954
+ // TODO: Eventually change this to:
955
+ // dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MergeReason.INDEX_TEMPLATE);
956
+ dummyMapperService .merge (MapperService .SINGLE_MAPPING_NAME , mapping , MergeReason .MAPPING_UPDATE );
957
+ }
958
+ } catch (Exception e ) {
959
+ throw new IllegalArgumentException ("invalid composite mappings for [" + templateName + "]" , e );
960
+ }
961
+ return null ;
962
+ });
963
+ }
964
+
858
965
private static void validateTemplate (Settings validateSettings , String mappings ,
859
966
IndicesService indicesService , NamedXContentRegistry xContentRegistry ) throws Exception {
860
967
validateTemplate (validateSettings , Collections .singletonMap (MapperService .SINGLE_MAPPING_NAME , mappings ),
0 commit comments