@@ -294,25 +294,26 @@ public ProjectMetadata execute(ProjectMetadata currentProject) throws Exception
294294 );
295295 }
296296
297- // Public visible for testing
297+ /**
298+ * Add the given component template to the project. If {@code create} is true, we will fail if there exists a component template with
299+ * the same name. If a component template with the same name exists, but the content is identical, no change will be made.
300+ * This method will perform all necessary validation but assumes that the component template has already been normalized (see
301+ * {@link #normalizeComponentTemplate(ComponentTemplate)}.
302+ */
298303 public ProjectMetadata addComponentTemplate (
299304 final ProjectMetadata project ,
300305 final boolean create ,
301306 final String name ,
302307 final ComponentTemplate template
303- ) throws Exception {
304- final ComponentTemplate existing = project .componentTemplates ().get (name );
305- if (create && existing != null ) {
306- throw new IllegalArgumentException ("component template [" + name + "] already exists" );
307- }
308-
309- CompressedXContent mappings = template .template ().mappings ();
310- CompressedXContent wrappedMappings = wrapMappingsIfNecessary (mappings , xContentRegistry );
311-
312- // We may need to normalize index settings, so do that also
313- Settings finalSettings = template .template ().settings ();
314- if (finalSettings != null ) {
315- finalSettings = Settings .builder ().put (finalSettings ).normalizePrefix (IndexMetadata .INDEX_SETTING_PREFIX ).build ();
308+ ) throws IOException {
309+ final ComponentTemplate existingTemplate = project .componentTemplates ().get (name );
310+ if (existingTemplate != null ) {
311+ if (create ) {
312+ throw new IllegalArgumentException ("component template [" + name + "] already exists" );
313+ }
314+ if (template .contentEquals (existingTemplate )) {
315+ return project ;
316+ }
316317 }
317318
318319 // Collect all the composable (index) templates that use this component template, we'll use
@@ -325,9 +326,9 @@ public ProjectMetadata addComponentTemplate(
325326 .collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
326327
327328 // 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
328- if (create == false && finalSettings != null ) {
329+ if (create == false && template . template (). settings () != null ) {
329330 // if the CT is specifying the `index.hidden` setting it cannot be part of any global template
330- if (IndexMetadata .INDEX_HIDDEN_SETTING .exists (finalSettings )) {
331+ if (IndexMetadata .INDEX_HIDDEN_SETTING .exists (template . template (). settings () )) {
331332 List <String > globalTemplatesThatUseThisComponent = new ArrayList <>();
332333 for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
333334 ComposableIndexTemplate templateV2 = entry .getValue ();
@@ -351,47 +352,27 @@ public ProjectMetadata addComponentTemplate(
351352 }
352353 }
353354
354- final Template finalTemplate = Template .builder (template .template ()).settings (finalSettings ).mappings (wrappedMappings ).build ();
355- final long now = instantSource .instant ().toEpochMilli ();
356- final ComponentTemplate finalComponentTemplate ;
357- if (existing == null ) {
358- finalComponentTemplate = new ComponentTemplate (
359- finalTemplate ,
360- template .version (),
361- template .metadata (),
362- template .deprecated (),
363- now ,
364- now
365- );
366- } else {
367- final ComponentTemplate templateToCompareToExisting = new ComponentTemplate (
368- finalTemplate ,
369- template .version (),
370- template .metadata (),
371- template .deprecated (),
372- existing .createdDateMillis ().orElse (null ),
373- existing .modifiedDateMillis ().orElse (null )
374- );
375- if (templateToCompareToExisting .equals (existing )) {
376- return project ;
377- }
378- finalComponentTemplate = new ComponentTemplate (
379- finalTemplate ,
380- template .version (),
381- template .metadata (),
382- template .deprecated (),
383- existing .createdDateMillis ().orElse (null ),
384- now
385- );
386- }
355+ final Long now = instantSource .instant ().toEpochMilli ();
356+ final Long createdDateMillis = existingTemplate == null ? now : existingTemplate .createdDateMillis ().orElse (null );
357+ final ComponentTemplate finalComponentTemplate = new ComponentTemplate (
358+ template .template (),
359+ template .version (),
360+ template .metadata (),
361+ template .deprecated (),
362+ createdDateMillis ,
363+ now
364+ );
387365
388- validateTemplate (finalSettings , wrappedMappings , indicesService );
366+ // These two validation checks are only scoped to the component template itself (and don't depend on any other entities in the
367+ // cluster state) and could thus be done in the transport action. However, since we're parsing mappings here, we shouldn't be doing
368+ // it directly on the transport thread. Instead, we should fork to a different threadpool (management/generic).
369+ validateTemplate (finalComponentTemplate .template ().settings (), finalComponentTemplate .template ().mappings (), indicesService );
389370 validate (name , finalComponentTemplate .template (), List .of (), null );
390371
391372 ProjectMetadata projectWithComponentTemplateAdded = ProjectMetadata .builder (project ).put (name , finalComponentTemplate ).build ();
392373 // Validate all composable index templates that use this component template
393374 if (templatesUsingComponent .isEmpty () == false ) {
394- Exception validationFailure = null ;
375+ IllegalArgumentException validationFailure = null ;
395376 for (Map .Entry <String , ComposableIndexTemplate > entry : templatesUsingComponent .entrySet ()) {
396377 final String composableTemplateName = entry .getKey ();
397378 final ComposableIndexTemplate composableTemplate = entry .getValue ();
@@ -425,10 +406,42 @@ public ProjectMetadata addComponentTemplate(
425406 .addWarningHeaderIfDataRetentionNotEffective (globalRetentionSettings .get (false ), false );
426407 }
427408
428- logger .info ("{} component template [{}]" , existing == null ? "adding" : "updating" , name );
409+ logger .info ("{} component template [{}]" , existingTemplate == null ? "adding" : "updating" , name );
429410 return projectWithComponentTemplateAdded ;
430411 }
431412
413+ /**
414+ * Normalize the given component template by trying to normalize settings and wrapping mappings if necessary. Returns the same instance
415+ * if nothing needs to be done.
416+ */
417+ public ComponentTemplate normalizeComponentTemplate (final ComponentTemplate componentTemplate ) throws IOException {
418+ Template template = componentTemplate .template ();
419+ // Normalize the index settings if necessary
420+ Settings prefixedSettings = null ;
421+ if (template .settings () != null ) {
422+ prefixedSettings = template .settings ().maybeNormalizePrefix (IndexMetadata .INDEX_SETTING_PREFIX );
423+ }
424+ // TODO: theoretically, we could avoid parsing the mappings once by combining this wrapping with the mapping validation later on,
425+ // but that refactoring will be non-trivial as we currently don't seem to have methods available to merge already-parsed mappings;
426+ // we only allow merging mappings from CompressedXContent.
427+ CompressedXContent wrappedMappings = MetadataIndexTemplateService .wrapMappingsIfNecessary (template .mappings (), xContentRegistry );
428+
429+ // No need to build a new component template if we didn't change anything.
430+ // We can check for reference equality since `maybeNormalizePrefix` and `wrapMappingsIfNecessary` return the same instance if
431+ // nothing needs to be done.
432+ if (prefixedSettings == template .settings () && wrappedMappings == template .mappings ()) {
433+ return componentTemplate ;
434+ }
435+ return new ComponentTemplate (
436+ Template .builder (template ).settings (prefixedSettings ).mappings (wrappedMappings ).build (),
437+ componentTemplate .version (),
438+ componentTemplate .metadata (),
439+ componentTemplate .deprecated (),
440+ componentTemplate .createdDateMillis ().orElse (null ),
441+ componentTemplate .modifiedDateMillis ().orElse (null )
442+ );
443+ }
444+
432445 /**
433446 * Mappings in templates don't have to include <code>_doc</code>, so update the mappings to include this single type if necessary
434447 *
@@ -2048,7 +2061,7 @@ private static void validateCompositeTemplate(
20482061 }
20492062
20502063 public static void validateTemplate (Settings validateSettings , CompressedXContent mappings , IndicesService indicesService )
2051- throws Exception {
2064+ throws IOException {
20522065 // Hard to validate settings if they're non-existent, so used empty ones if none were provided
20532066 Settings settings = validateSettings ;
20542067 if (settings == null ) {
0 commit comments