diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/15_composition.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/15_composition.yml index 0099ec96b2e9d..9bad6437dcf6a 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/15_composition.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_index_template/15_composition.yml @@ -14,7 +14,7 @@ number_of_replicas: 1 mappings: properties: - field2: + field1: type: text aliases: aliasname: @@ -47,9 +47,8 @@ index.number_of_shards: 2 mappings: properties: - field: - type: keyword - ignore_above: 255 + field3: + type: integer aliases: my_alias: {} aliasname: @@ -78,8 +77,9 @@ - match: {bar-baz.settings.index.number_of_shards: "2"} - match: {bar-baz.settings.index.number_of_replicas: "0"} - match: {bar-baz.settings.index.priority: "17"} - - match: {bar-baz.mappings.properties.field: {type: keyword, ignore_above: 255}} + - match: {bar-baz.mappings.properties.field1: {type: text}} - match: {bar-baz.mappings.properties.field2: {type: keyword}} + - match: {bar-baz.mappings.properties.field3: {type: integer}} - match: {bar-baz.mappings.properties.foo: {type: keyword}} - match: {bar-baz.aliases.aliasname: {filter: {match_all: {}}}} - match: {bar-baz.aliases.my_alias: {}} diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java index 19377d25e9755..d9693258f41d7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java @@ -590,7 +590,7 @@ static Map> parseV2Mappings(String mappingsJson, Lis nonProperties = innerTemplateNonProperties; if (maybeProperties != null) { - properties = mergeIgnoringDots(properties, maybeProperties); + properties = mergeFailingOnReplacement(properties, maybeProperties); } } } @@ -605,7 +605,7 @@ static Map> parseV2Mappings(String mappingsJson, Lis nonProperties = innerRequestNonProperties; if (maybeRequestProperties != null) { - properties = mergeIgnoringDots(properties, maybeRequestProperties); + properties = mergeFailingOnReplacement(properties, maybeRequestProperties); } } @@ -705,18 +705,18 @@ private static Map dedupDynamicTemplates(Map map } /** - * Add the objects in the second map to the first, where the keys in the {@code second} map have - * higher predecence and overwrite the keys in the {@code first} map. In the event of a key with - * a dot in it (ie, "foo.bar"), the keys are treated as only the prefix counting towards - * equality. If the {@code second} map has a key such as "foo", all keys starting from "foo." in - * the {@code first} map are discarded. + * Add the objects in the second map to the first, A duplicated field is treated as illegal and + * an exception is thrown. */ - static Map mergeIgnoringDots(Map first, Map second) { + static Map mergeFailingOnReplacement(Map first, Map second) { Objects.requireNonNull(first, "merging requires two non-null maps but the first map was null"); Objects.requireNonNull(second, "merging requires two non-null maps but the second map was null"); Map results = new HashMap<>(first); Set prefixes = second.keySet().stream().map(MetadataCreateIndexService::prefix).collect(Collectors.toSet()); - results.keySet().removeIf(k -> prefixes.contains(prefix(k))); + List matchedPrefixes = results.keySet().stream().filter(k -> prefixes.contains(prefix(k))).collect(Collectors.toList()); + if (matchedPrefixes.size() > 0) { + throw new IllegalArgumentException("mapping fields " + matchedPrefixes + " cannot be replaced during template composition"); + } results.putAll(second); return results; } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java index 7ae302f0d319f..3c2dd9e55910b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java @@ -196,15 +196,21 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean validateTemplate(finalSettings, stringMappings, indicesService, xContentRegistry); + // Collect all the composable (index) templates that use this component template, we'll use + // this for validating that they're still going to be valid after this component template + // has been updated + final Map templatesUsingComponent = currentState.metadata().templatesV2().entrySet().stream() + .filter(e -> e.getValue().composedOf().contains(name)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + // 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 if (create == false && finalSettings != null) { // if the CT is specifying the `index.hidden` setting it cannot be part of any global template if (IndexMetadata.INDEX_HIDDEN_SETTING.exists(finalSettings)) { - Map existingTemplates = currentState.metadata().templatesV2(); List globalTemplatesThatUseThisComponent = new ArrayList<>(); - for (Map.Entry entry : existingTemplates.entrySet()) { + for (Map.Entry entry : templatesUsingComponent.entrySet()) { ComposableIndexTemplate templateV2 = entry.getValue(); - if (templateV2.composedOf().contains(name) && templateV2.indexPatterns().stream().anyMatch(Regex::isMatchAllPattern)) { + if (templateV2.indexPatterns().stream().anyMatch(Regex::isMatchAllPattern)) { // global templates don't support configuring the `index.hidden` setting so we don't need to resolve the settings as // no other component template can remove this setting from the resolved settings, so just invalidate this update globalTemplatesThatUseThisComponent.add(entry.getKey()); @@ -234,6 +240,32 @@ ClusterState addComponentTemplate(final ClusterState currentState, final boolean stringMappings == null ? null : new CompressedXContent(stringMappings), template.template().aliases()); final ComponentTemplate finalComponentTemplate = new ComponentTemplate(finalTemplate, template.version(), template.metadata()); validate(name, finalComponentTemplate); + + if (templatesUsingComponent.size() > 0) { + ClusterState tempStateWithComponentTemplateAdded = ClusterState.builder(currentState) + .metadata(Metadata.builder(currentState.metadata()).put(name, finalComponentTemplate)) + .build(); + Exception validationFailure = null; + for (Map.Entry entry : templatesUsingComponent.entrySet()) { + final String composableTemplateName = entry.getKey(); + final ComposableIndexTemplate composableTemplate = entry.getValue(); + try { + validateCompositeTemplate(tempStateWithComponentTemplateAdded, composableTemplateName, + composableTemplate, indicesService, xContentRegistry); + } catch (Exception e) { + if (validationFailure == null) { + validationFailure = new IllegalArgumentException("updating component template [" + name + + "] results in invalid composable template [" + composableTemplateName + "] after templates are merged", e); + } else { + validationFailure.addSuppressed(e); + } + } + } + if (validationFailure != null) { + throw validationFailure; + } + } + logger.info("adding component template [{}]", name); return ClusterState.builder(currentState) .metadata(Metadata.builder(currentState.metadata()).put(name, finalComponentTemplate)) @@ -385,7 +417,6 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo // adjusted (to add _doc) and it should be validated CompressedXContent mappings = innerTemplate.mappings(); String stringMappings = mappings == null ? null : mappings.string(); - validateTemplate(finalSettings, stringMappings, indicesService, xContentRegistry); // Mappings in index templates don't include _doc, so update the mappings to include this single type if (stringMappings != null) { @@ -404,6 +435,17 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo } validate(name, finalIndexTemplate); + + // Finally, right before adding the template, we need to ensure that the composite settings, + // mappings, and aliases are valid after it's been composed with the component templates + try { + validateCompositeTemplate(currentState, name, finalIndexTemplate, indicesService, xContentRegistry); + } catch (Exception e) { + throw new IllegalArgumentException("composable template [" + name + + "] template after composition " + + (finalIndexTemplate.composedOf().size() > 0 ? "with component templates " + finalIndexTemplate.composedOf() + " " : "") + + "is invalid", e); + } logger.info("adding index template [{}]", name); return ClusterState.builder(currentState) .metadata(Metadata.builder(currentState.metadata()).put(name, finalIndexTemplate)) @@ -748,7 +790,6 @@ public static List resolveMappings(final ClusterState state, return Collections.emptyList(); } final Map componentTemplates = state.metadata().componentTemplates(); - // TODO: more fine-grained merging of component template mappings, ie, merge fields as distint entities List mappings = template.composedOf().stream() .map(componentTemplates::get) .filter(Objects::nonNull) @@ -855,6 +896,72 @@ public static List> resolveAliases(final Metadata met return Collections.unmodifiableList(aliases); } + /** + * Given a state and a composable template, validate that the final composite template + * generated by the composable template and all of its component templates contains valid + * settings, mappings, and aliases. + */ + private static void validateCompositeTemplate(final ClusterState state, + final String templateName, + final ComposableIndexTemplate template, + final IndicesService indicesService, + final NamedXContentRegistry xContentRegistry) throws Exception { + final ClusterState stateWithTemplate = ClusterState.builder(state) + .metadata(Metadata.builder(state.metadata()).put(templateName, template)) + .build(); + + final String temporaryIndexName = "validate-template-" + UUIDs.randomBase64UUID().toLowerCase(Locale.ROOT); + Settings resolvedSettings = resolveSettings(stateWithTemplate.metadata(), templateName); + + // use the provided values, otherwise just pick valid dummy values + int dummyPartitionSize = IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING.get(resolvedSettings); + int dummyShards = resolvedSettings.getAsInt(IndexMetadata.SETTING_NUMBER_OF_SHARDS, + dummyPartitionSize == 1 ? 1 : dummyPartitionSize + 1); + int shardReplicas = resolvedSettings.getAsInt(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0); + + + // Create the final aggregate settings, which will be used to create the temporary index metadata to validate everything + Settings finalResolvedSettings = Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(resolvedSettings) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, dummyShards) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, shardReplicas) + .put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()) + .build(); + + // Validate index metadata (settings) + final ClusterState stateWithIndex = ClusterState.builder(stateWithTemplate) + .metadata(Metadata.builder(stateWithTemplate.metadata()) + .put(IndexMetadata.builder(temporaryIndexName).settings(finalResolvedSettings)) + .build()) + .build(); + final IndexMetadata tmpIndexMetadata = stateWithIndex.metadata().index(temporaryIndexName); + indicesService.withTempIndexService(tmpIndexMetadata, + tempIndexService -> { + // Validate aliases + MetadataCreateIndexService.resolveAndValidateAliases(temporaryIndexName, Collections.emptySet(), + MetadataIndexTemplateService.resolveAliases(stateWithIndex.metadata(), templateName), stateWithIndex.metadata(), + new AliasValidator(), + // the context is only used for validation so it's fine to pass fake values for the + // shard id and the current timestamp + xContentRegistry, tempIndexService.newQueryShardContext(0, null, () -> 0L, null)); + + // Parse mappings to ensure they are valid after being composed + List mappings = resolveMappings(stateWithIndex, templateName); + try { + MapperService dummyMapperService = tempIndexService.mapperService(); + for (CompressedXContent mapping : mappings) { + // TODO: Eventually change this to: + // dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MergeReason.INDEX_TEMPLATE); + dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MergeReason.MAPPING_UPDATE); + } + } catch (Exception e) { + throw new IllegalArgumentException("invalid composite mappings for [" + templateName + "]", e); + } + return null; + }); + } + private static void validateTemplate(Settings validateSettings, String mappings, IndicesService indicesService, NamedXContentRegistry xContentRegistry) throws Exception { validateTemplate(validateSettings, Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, mappings), diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java index 2709c59863c77..844dcb221113f 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java @@ -88,7 +88,7 @@ public static ComponentTemplate randomInstance() { public static Map randomAliases() { String aliasName = randomAlphaOfLength(5); AliasMetadata aliasMeta = AliasMetadata.builder(aliasName) - .filter(Collections.singletonMap(randomAlphaOfLength(2), randomAlphaOfLength(2))) + .filter("{\"term\":{\"year\":" + randomIntBetween(1, 3000) + "}}") .routing(randomBoolean() ? null : randomAlphaOfLength(3)) .isHidden(randomBoolean() ? null : randomBoolean()) .writeIndex(randomBoolean() ? null : randomBoolean()) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java index 9ffcc0d5f5661..54ea6b30e0142 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java @@ -100,7 +100,7 @@ public static ComposableIndexTemplate randomInstance() { private static Map randomAliases() { String aliasName = randomAlphaOfLength(5); AliasMetadata aliasMeta = AliasMetadata.builder(aliasName) - .filter(Collections.singletonMap(randomAlphaOfLength(2), randomAlphaOfLength(2))) + .filter("{\"term\":{\"year\":" + randomIntBetween(1, 3000) + "}}") .routing(randomBoolean() ? null : randomAlphaOfLength(3)) .isHidden(randomBoolean() ? null : randomBoolean()) .writeIndex(randomBoolean() ? null : randomBoolean()) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java index e852da473f5bd..132eca91a6761 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java @@ -1002,6 +1002,7 @@ public void testValidateTranslogRetentionSettings() { } @SuppressWarnings("unchecked") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/57393") public void testMappingsMergingIsSmart() throws Exception { Template ctt1 = new Template(null, new CompressedXContent("{\"_doc\":{\"_source\":{\"enabled\": false},\"_meta\":{\"ct1\":{\"ver\": \"text\"}}," + @@ -1066,6 +1067,7 @@ public void testMappingsMergingIsSmart() throws Exception { } @SuppressWarnings("unchecked") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/57393") public void testMappingsMergingHandlesDots() throws Exception { Template ctt1 = new Template(null, new CompressedXContent("{\"_doc\":{\"properties\":{\"foo\":{\"properties\":{\"bar\":{\"type\": \"long\"}}}}}}"), null); @@ -1100,33 +1102,31 @@ public void testMappingsMergingHandlesDots() throws Exception { equalTo(Collections.singletonMap("properties", Collections.singletonMap("bar", Collections.singletonMap("type", "long"))))); } - public void testMergeIgnoringDots() throws Exception { - Map first = new HashMap<>(); - first.put("foo", Collections.singletonMap("type", "long")); - Map second = new HashMap<>(); - second.put("foo.bar", Collections.singletonMap("type", "long")); - Map results = MetadataCreateIndexService.mergeIgnoringDots(first, second); - assertThat(results, equalTo(second)); - - results = MetadataCreateIndexService.mergeIgnoringDots(second, first); - assertThat(results, equalTo(first)); - - second.clear(); - Map inner = new HashMap<>(); - inner.put("type", "text"); - inner.put("analyzer", "english"); - second.put("foo", inner); - - results = MetadataCreateIndexService.mergeIgnoringDots(first, second); - assertThat(results, equalTo(second)); - - first.put("baz", 3); - second.put("egg", 7); - - results = MetadataCreateIndexService.mergeIgnoringDots(first, second); - Map expected = new HashMap<>(second); - expected.put("baz", 3); - assertThat(results, equalTo(expected)); + public void testMappingsMergingThrowsOnConflictDots() throws Exception { + Template ctt1 = new Template(null, + new CompressedXContent("{\"_doc\":{\"properties\":{\"foo\":{\"properties\":{\"bar\":{\"type\": \"long\"}}}}}}"), null); + Template ctt2 = new Template(null, + new CompressedXContent("{\"_doc\":{\"properties\":{\"foo.bar\":{\"type\": \"text\",\"analyzer\":\"english\"}}}}"), null); + + ComponentTemplate ct1 = new ComponentTemplate(ctt1, null, null); + ComponentTemplate ct2 = new ComponentTemplate(ctt2, null, null); + + ComposableIndexTemplate template = new ComposableIndexTemplate(Collections.singletonList("index"), + null, Arrays.asList("ct2", "ct1"), null, null, null); + + ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(Metadata.builder(Metadata.EMPTY_METADATA) + .put("ct1", ct1) + .put("ct2", ct2) + .put("index-template", template) + .build()) + .build(); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> MetadataCreateIndexService.resolveV2Mappings("{}", state, + "index-template", new NamedXContentRegistry(Collections.emptyList()))); + + assertThat(e.getMessage(), containsString("mapping fields [foo.bar] cannot be replaced during template composition")); } @SuppressWarnings("unchecked") diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java index 3d9e6affc4db4..a1a978e26e108 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java @@ -64,10 +64,12 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase { public void testIndexTemplateInvalidNumberOfShards() { @@ -670,7 +672,8 @@ public void testFindV2InvalidGlobalTemplate() { } } - public void testResolveMappings() throws Exception { + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/57393") + public void testResolveConflictingMappings() throws Exception { final MetadataIndexTemplateService service = getMetadataIndexTemplateService(); ClusterState state = ClusterState.EMPTY_STATE; @@ -736,6 +739,67 @@ public void testResolveMappings() throws Exception { Collections.singletonMap("field", Collections.singletonMap("type", "keyword")))))); } + public void testResolveMappings() throws Exception { + final MetadataIndexTemplateService service = getMetadataIndexTemplateService(); + ClusterState state = ClusterState.EMPTY_STATE; + + ComponentTemplate ct1 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field1\": {\n" + + " \"type\": \"keyword\"\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + ComponentTemplate ct2 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field2\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + state = service.addComponentTemplate(state, true, "ct_high", ct1); + state = service.addComponentTemplate(state, true, "ct_low", ct2); + ComposableIndexTemplate it = new ComposableIndexTemplate(Arrays.asList("i*"), + new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field3\": {\n" + + " \"type\": \"integer\"\n" + + " }\n" + + " }\n" + + " }"), null), + Arrays.asList("ct_low", "ct_high"), 0L, 1L, null); + state = service.addIndexTemplateV2(state, true, "my-template", it); + + List mappings = MetadataIndexTemplateService.resolveMappings(state, "my-template"); + + assertNotNull(mappings); + assertThat(mappings.size(), equalTo(3)); + List> parsedMappings = mappings.stream() + .map(m -> { + try { + return MapperService.parseMapping(new NamedXContentRegistry(Collections.emptyList()), m.string()); + } catch (Exception e) { + logger.error(e); + fail("failed to parse mappings: " + m.string()); + return null; + } + }) + .collect(Collectors.toList()); + + assertThat(parsedMappings.get(0), + equalTo(Collections.singletonMap("_doc", + Collections.singletonMap("properties", Collections.singletonMap("field2", Collections.singletonMap("type", "text")))))); + assertThat(parsedMappings.get(1), + equalTo(Collections.singletonMap("_doc", + Collections.singletonMap("properties", Collections.singletonMap("field1", Collections.singletonMap("type", "keyword")))))); + assertThat(parsedMappings.get(2), + equalTo(Collections.singletonMap("_doc", + Collections.singletonMap("properties", Collections.singletonMap("field3", Collections.singletonMap("type", "integer")))))); + } + public void testResolveSettings() throws Exception { final MetadataIndexTemplateService service = getMetadataIndexTemplateService(); ClusterState state = ClusterState.EMPTY_STATE; @@ -794,6 +858,129 @@ public void testResolveAliases() throws Exception { assertThat(resolvedAliases, equalTo(Arrays.asList(a3, a1, a2))); } + /** + * Tests that we check that settings/mappings/etc are valid even after template composition, + * when adding/updating a composable index template + */ + public void testIndexTemplateFailsToOverrideComponentTemplateMappingField() throws Exception { + final MetadataIndexTemplateService service = getMetadataIndexTemplateService(); + ClusterState state = ClusterState.EMPTY_STATE; + + ComponentTemplate ct1 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field2\": {\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"foo\": {\n" + + " \"type\": \"integer\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + ComponentTemplate ct2 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field1\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + state = service.addComponentTemplate(state, true, "c1", ct1); + state = service.addComponentTemplate(state, true, "c2", ct2); + ComposableIndexTemplate it = new ComposableIndexTemplate(Arrays.asList("i*"), + new Template(null, new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field2\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }"), null), + randomBoolean() ? Arrays.asList("c1", "c2") : Arrays.asList("c2", "c1"), 0L, 1L, null); + + final ClusterState finalState = state; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> service.addIndexTemplateV2(finalState, randomBoolean(), "my-template", it)); + + assertThat(e.getMessage(), + matchesRegex("composable template \\[my-template\\] template after composition with component templates .+ is invalid")); + + assertNotNull(e.getCause()); + assertThat(e.getCause().getMessage(), + containsString("invalid composite mappings for [my-template]")); + + assertNotNull(e.getCause().getCause()); + assertThat(e.getCause().getCause().getMessage(), + anyOf(containsString("mapping fields [field2] cannot be replaced during template composition"), + containsString("Can't merge a non object mapping [field2] with an object mapping [field2]"))); + } + + /** + * Tests that we check that settings/mappings/etc are valid even after template composition, + * when updating a component template + */ + public void testUpdateComponentTemplateFailsIfResolvedIndexTemplatesWouldBeInvalid() throws Exception { + final MetadataIndexTemplateService service = getMetadataIndexTemplateService(); + ClusterState state = ClusterState.EMPTY_STATE; + + ComponentTemplate ct1 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field2\": {\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"foo\": {\n" + + " \"type\": \"integer\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + ComponentTemplate ct2 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field1\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + state = service.addComponentTemplate(state, true, "c1", ct1); + state = service.addComponentTemplate(state, true, "c2", ct2); + ComposableIndexTemplate it = new ComposableIndexTemplate(Arrays.asList("i*"), + new Template(null, null, null), + randomBoolean() ? Arrays.asList("c1", "c2") : Arrays.asList("c2", "c1"), 0L, 1L, null); + + // Great, the templates aren't invalid + state = service.addIndexTemplateV2(state, randomBoolean(), "my-template", it); + + ComponentTemplate changedCt2 = new ComponentTemplate(new Template(null, + new CompressedXContent("{\n" + + " \"properties\": {\n" + + " \"field2\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }"), null), null, null); + + final ClusterState finalState = state; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> service.addComponentTemplate(finalState, false, "c2", changedCt2)); + + assertThat(e.getMessage(), + containsString("updating component template [c2] results in invalid " + + "composable template [my-template] after templates are merged")); + + assertNotNull(e.getCause()); + assertThat(e.getCause().getMessage(), + containsString("invalid composite mappings for [my-template]")); + + assertNotNull(e.getCause().getCause()); + assertThat(e.getCause().getCause().getMessage(), + anyOf(containsString("mapping fields [field2] cannot be replaced during template composition"), + containsString("mapper [field2] of different type, current_type [text], merged_type [ObjectMapper]"))); + } + private static List putTemplate(NamedXContentRegistry xContentRegistry, PutRequest request) { MetadataCreateIndexService createIndexService = new MetadataCreateIndexService( Settings.EMPTY,