Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow merging existing mapping field definitions in templates #57701

Merged
merged 6 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
number_of_replicas: 1
mappings:
properties:
field2:
field1:
type: text
aliases:
aliasname:
Expand Down Expand Up @@ -47,9 +47,8 @@
index.number_of_shards: 2
mappings:
properties:
field:
type: keyword
ignore_above: 255
field3:
type: integer
aliases:
my_alias: {}
aliasname:
Expand Down Expand Up @@ -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: {}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
nonProperties = innerTemplateNonProperties;

if (maybeProperties != null) {
properties = mergeIgnoringDots(properties, maybeProperties);
properties = mergeFailingOnReplacement(properties, maybeProperties);
}
}
}
Expand All @@ -587,7 +587,7 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
nonProperties = innerRequestNonProperties;

if (maybeRequestProperties != null) {
properties = mergeIgnoringDots(properties, maybeRequestProperties);
properties = mergeFailingOnReplacement(properties, maybeRequestProperties);
}
}

Expand Down Expand Up @@ -687,18 +687,18 @@ private static Map<String, Object> dedupDynamicTemplates(Map<String, Object> 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<String, Object> mergeIgnoringDots(Map<String, Object> first, Map<String, Object> second) {
static Map<String, Object> mergeFailingOnReplacement(Map<String, Object> first, Map<String, Object> 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<String, Object> results = new HashMap<>(first);
Set<String> prefixes = second.keySet().stream().map(MetadataCreateIndexService::prefix).collect(Collectors.toSet());
results.keySet().removeIf(k -> prefixes.contains(prefix(k)));
List<String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,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<String, ComposableIndexTemplate> 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<String, ComposableIndexTemplate> existingTemplates = currentState.metadata().templatesV2();
List<String> globalTemplatesThatUseThisComponent = new ArrayList<>();
for (Map.Entry<String, ComposableIndexTemplate> entry : existingTemplates.entrySet()) {
for (Map.Entry<String, ComposableIndexTemplate> entry : templatesUsingComponent.entrySet()) {
dakrone marked this conversation as resolved.
Show resolved Hide resolved
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());
Expand Down Expand Up @@ -233,6 +239,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<String, ComposableIndexTemplate> 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))
Expand Down Expand Up @@ -422,7 +454,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) {
Expand All @@ -441,6 +472,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))
Expand Down Expand Up @@ -803,7 +845,6 @@ public static List<CompressedXContent> resolveMappings(final ClusterState state,
return List.of();
}
final Map<String, ComponentTemplate> componentTemplates = state.metadata().componentTemplates();
// TODO: more fine-grained merging of component template mappings, ie, merge fields as distint entities
List<CompressedXContent> mappings = template.composedOf().stream()
.map(componentTemplates::get)
.filter(Objects::nonNull)
Expand Down Expand Up @@ -910,6 +951,76 @@ public static List<Map<String, AliasMetadata>> 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,
Comment on lines +954 to +959
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the composite term is a bit confusing (at least for me) - we have composable templates so composite template made me think of a possible v3 template of sorts that might combine other templates. Would it make sense to reuse resolve(d) for the final configuration of a composable template? ie. validateResolvedComposableTemplate ? resolveAndValidateComposableTemplate ?

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, I didn't think they were too different, I wanted to treat "resolved" as pertaining to a particular index, so I think the current name isn't too bad.

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<CompressedXContent> mappings = resolveMappings(stateWithIndex, templateName);
final Map<String, Object> finalMappings;
try {
finalMappings = MetadataCreateIndexService.parseV2Mappings("{}", mappings, xContentRegistry);

MapperService dummyMapperService = tempIndexService.mapperService();
if (finalMappings.isEmpty() == false) {
assert finalMappings.size() == 1 : finalMappings;
// TODO: Eventually change this to:
// dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MergeReason.INDEX_TEMPLATE);
dummyMapperService.merge(MapperService.SINGLE_MAPPING_NAME, finalMappings, 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 {
// First check to see if mappings are valid XContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static ComponentTemplate randomInstance() {
public static Map<String, AliasMetadata> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static ComposableIndexTemplate randomInstance() {
private static Map<String, AliasMetadata> 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,7 @@ public void testDeprecateTranslogRetentionSettings() {
}

@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\"}}," +
Expand Down Expand Up @@ -1010,6 +1011,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);
Expand Down Expand Up @@ -1044,33 +1046,31 @@ public void testMappingsMergingHandlesDots() throws Exception {
equalTo(Collections.singletonMap("properties", Collections.singletonMap("bar", Collections.singletonMap("type", "long")))));
}

public void testMergeIgnoringDots() throws Exception {
Map<String, Object> first = new HashMap<>();
first.put("foo", Collections.singletonMap("type", "long"));
Map<String, Object> second = new HashMap<>();
second.put("foo.bar", Collections.singletonMap("type", "long"));
Map<String, Object> results = MetadataCreateIndexService.mergeIgnoringDots(first, second);
assertThat(results, equalTo(second));

results = MetadataCreateIndexService.mergeIgnoringDots(second, first);
assertThat(results, equalTo(first));

second.clear();
Map<String, Object> 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<String, Object> 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, 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")
Expand Down
Loading