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

ITV2: disallow duplicate dynamic templates #56291

Merged
merged 6 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -82,6 +82,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -576,6 +577,18 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
Map<String, Object> innerTemplateNonProperties = new HashMap<>(innerTemplateMapping);
Map<String, Object> maybeProperties = (Map<String, Object>) innerTemplateNonProperties.remove("properties");

List<Map<String, Object>> innerTemplateDynamicTemplates = (List<Map<String, Object>>) innerTemplateNonProperties.get(
"dynamic_templates");
List<Map<String, Object>> previouslySeenDynamicTemplates =
(List<Map<String, Object>>) nonProperties.get("dynamic_templates");
// we want "later" defined dynamic templates to override the previously seen ones with the same name
List<Map<String, Object>> filteredDefaultDynamicTemplates =
removeAll(previouslySeenDynamicTemplates, innerTemplateDynamicTemplates);

if (filteredDefaultDynamicTemplates != previouslySeenDynamicTemplates) {
nonProperties.put("dynamic_templates", filteredDefaultDynamicTemplates);
}
dakrone marked this conversation as resolved.
Show resolved Hide resolved

XContentHelper.mergeDefaults(innerTemplateNonProperties, nonProperties);
nonProperties = innerTemplateNonProperties;

Expand All @@ -590,6 +603,18 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
Map<String, Object> innerRequestNonProperties = new HashMap<>(innerRequestMappings);
Map<String, Object> maybeRequestProperties = (Map<String, Object>) innerRequestNonProperties.remove("properties");

List<Map<String, Object>> innerRequestDynamicTemplates = (List<Map<String, Object>>) innerRequestMappings.get(
"dynamic_templates");
List<Map<String, Object>> previouslySeenDynamicTemplates = (List<Map<String, Object>>) nonProperties.get("dynamic_templates");
// we want the dynamic templates specified in the request to override the ones defined in index/component templates with the
// same name
List<Map<String, Object>> filteredDefaultDynamicTemplates =
removeAll(previouslySeenDynamicTemplates, innerRequestDynamicTemplates);

if (filteredDefaultDynamicTemplates != previouslySeenDynamicTemplates) {
nonProperties.put("dynamic_templates", filteredDefaultDynamicTemplates);
}

XContentHelper.mergeDefaults(innerRequestNonProperties, nonProperties);
nonProperties = innerRequestNonProperties;

Expand All @@ -598,11 +623,78 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
}
}

Map<String, Object> finalMappings = new HashMap<>(nonProperties);
Map<String, Object> finalMappings = dedupDynamicTemplates(nonProperties);
finalMappings.put("properties", properties);
return Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, finalMappings);
}

/**
* Removes all the items from the first list that are already present in the second list
*
* Similar to {@link List#removeAll(Collection)} but the list parameters are not modified.
*
* This expects both list values to be Maps of size one and the "contains" operation that will determine if a value
* from the second list is present in the first list (and be removed from the first list) is based on key name.
*
* eg.
* removeAll([ {"key1" : {}}, {"key2" : {}} ], [ {"key1" : {}}, {"key3" : {}} ])
* Returns:
* [ {"key2" : {}} ]
*/
private static List<Map<String, Object>> removeAll(List<Map<String, Object>> first, List<Map<String, Object>> second) {
dakrone marked this conversation as resolved.
Show resolved Hide resolved
if (first == null) {
return first;
} else {
validateValuesAreMapsOfSizeOne(first);
}

if (second == null) {
return first;
} else {
validateValuesAreMapsOfSizeOne(second);
}

Set<String> keys = second.stream()
.map(value -> value.keySet().iterator().next())
.collect(Collectors.toSet());

return first.stream().filter(value -> keys.contains(value.keySet().iterator().next()) == false).collect(toList());
}

private static void validateValuesAreMapsOfSizeOne(List<Map<String, Object>> second) {
for (Map<String, Object> map : second) {
// all are in the form of [ {"key1" : {}}, {"key2" : {}} ]
if (map.size() != 1) {
throw new IllegalArgumentException("unexpected argument, expected maps with one key, but got " + map);
}
}
}

/**
* Parses the `dynamic_templates` from the provided mappings, if any are configured, and returns a mappings map containing dynamic
* templates with unique names.
*
* The later templates in the provided mapping's `dynamic_templates` array will override the templates with the same name defined
* earlier in the `dynamic_templates` array.
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> dedupDynamicTemplates(Map<String, Object> mappings) {
Objects.requireNonNull(mappings, "deduping the dynamic templates a non-null mapping");
Map<String, Object> results = new HashMap<>(mappings);
List<Map<String, Object>> dynamicTemplates = (List<Map<String, Object>>) mappings.get("dynamic_templates");
if (dynamicTemplates == null) {
return results;
}

LinkedHashMap<String, Map<String, Object>> dedupedDynamicTemplates = new LinkedHashMap<>(dynamicTemplates.size(), 1f);
for (Map<String, Object> dynamicTemplate : dynamicTemplates) {
dedupedDynamicTemplates.put(dynamicTemplate.keySet().iterator().next(), dynamicTemplate);
}

results.put("dynamic_templates", new ArrayList<>(dedupedDynamicTemplates.values()));
return results;
}

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,179 @@ public void testMergeIgnoringDots() throws Exception {
assertThat(results, equalTo(expected));
}

@SuppressWarnings("unchecked")
public void testDedupTemplateDynamicTemplates() throws Exception {
Template template = new Template(null,
new CompressedXContent("{\"_doc\":{\"_source\":{\"enabled\": false}, \"dynamic_templates\": [" +
"{\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"labels.*\"\n" +
" }\n" +
" },\n" +
" {\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"docker.container.labels.*\"\n" +
" }\n" +
"}]}}"), null);

IndexTemplateV2 indexTemplate = new IndexTemplateV2(Collections.singletonList("index"), template, null, null, null, null);

ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE)
.metadata(Metadata.builder(Metadata.EMPTY_METADATA)
.put("index-template", indexTemplate)
.build())
.build();

Map<String, Object> resolved =
MetadataCreateIndexService.resolveV2Mappings("{}", state,
"index-template", new NamedXContentRegistry(Collections.emptyList()));

Map<String, Object> doc = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
List<Map<String, Object>> dynamicTemplates = (List<Map<String, Object>>) doc.get("dynamic_templates");
assertThat(dynamicTemplates.size(), is(1));
Map<String, Object> dynamicMapping = (Map<String, Object>) dynamicTemplates.get(0).get("docker.container.labels");
assertThat(dynamicMapping, is(notNullValue()));
assertThat("last mapping with the same name must override previously defined mappings with the same name",
dynamicMapping.get("path_match"), is("docker.container.labels.*"));
}

public void testDedupRequestDynamicTemplates() throws Exception {
String requestMappingJson = "{\"_doc\":{\"_source\":{\"enabled\": false}, \"dynamic_templates\": [" +
"{\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"labels.*\"\n" +
" }\n" +
" },\n" +
" {\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"source.request.*\"\n" +
" }\n" +
"}]}}";

String templateMappingJson = "{\"_doc\":{\"_source\":{\"enabled\": false}, \"dynamic_templates\": [" +
"{\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"text\",\n" +
" \"copy_to\": \"text_labels\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"source.template.*\"\n" +
" }\n" +
" }\n" +
"]}}";
Template template = new Template(null, new CompressedXContent(templateMappingJson), null);

IndexTemplateV2 indexTemplate = new IndexTemplateV2(Collections.singletonList("index"), template, null, null, null, null);

ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE)
.metadata(Metadata.builder(Metadata.EMPTY_METADATA)
.put("index-template", indexTemplate)
.build())
.build();

Map<String, Object> resolved =
MetadataCreateIndexService.resolveV2Mappings(requestMappingJson, state,
"index-template", new NamedXContentRegistry(Collections.emptyList()));

Map<String, Object> doc = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
List<Map<String, Object>> dynamicTemplates = (List<Map<String, Object>>) doc.get("dynamic_templates");
assertThat(dynamicTemplates.size(), is(1));
Map<String, Object> dynamicMapping = (Map<String, Object>) dynamicTemplates.get(0).get("docker.container.labels");
assertThat(dynamicMapping, is(notNullValue()));
assertThat("last mapping with the same name must override previously defined mappings with the same name",
dynamicMapping.get("path_match"), is("source.request.*"));
Map<String, Object> mapping = (Map<String, Object>) dynamicMapping.get("mapping");
assertThat("the dynamic template defined in the request must not be merged with the dynamic template with the " +
"same name defined in the index template", mapping.size(), is(1));
assertThat(mapping.get("type"), is("keyword"));
}

public void testMultipleComponentTemplatesDefineSameDynamicTemplate() throws Exception {
String ct1Mapping = "{\"_doc\":{\"_source\":{\"enabled\": false}, \"dynamic_templates\": [" +
"{\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"text\",\n" +
" \"copy_to\": \"text_labels\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"source.first.ct.*\"\n" +
" }\n" +
" },\n" +
"{\n" +
" \"other.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"source.first.ct.other.labels*\"\n" +
" }\n" +
" }\n" +
"]}}";
String ct2Mapping = "{\"_doc\":{\"_source\":{\"enabled\": false}, \"dynamic_templates\": [" +
"{\n" +
" \"docker.container.labels\": {\n" +
" \"mapping\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"match_mapping_type\": \"string\",\n" +
" \"path_match\": \"source.second.ct.*\"\n" +
" }\n" +
" }\n" +
"]}}";

Template ctt1 = new Template(null, new CompressedXContent(ct1Mapping), null);
Template ctt2 = new Template(null, new CompressedXContent(ct2Mapping), null);
ComponentTemplate ct1 = new ComponentTemplate(ctt1, null, null);
ComponentTemplate ct2 = new ComponentTemplate(ctt2, null, null);

IndexTemplateV2 template = new IndexTemplateV2(Collections.singletonList("index"), null, Arrays.asList("ct1", "ct2"),
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();

Map<String, Object> resolved =
MetadataCreateIndexService.resolveV2Mappings("{}", state,
"index-template", new NamedXContentRegistry(Collections.emptyList()));

Map<String, Object> doc = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
List<Map<String, Object>> dynamicTemplates = (List<Map<String, Object>>) doc.get("dynamic_templates");
assertThat(dynamicTemplates.size(), is(2));
Map<String, Object> dockerLabelsDynamicTemplate = dynamicTemplates.get(0).get("docker.container.labels") != null ?
dynamicTemplates.get(0) : dynamicTemplates.get(1);
Map<String, Object> dynamicMapping = (Map<String, Object>) dockerLabelsDynamicTemplate.get("docker.container.labels");
assertThat(dynamicMapping, is(notNullValue()));
assertThat("dynamic template defined in the last defined component template must override the previously defined dynamic templates",
dynamicMapping.get("path_match"), is("source.second.ct.*"));
Map<String, Object> mapping = (Map<String, Object>) dynamicMapping.get("mapping");
assertThat("the dynamic template defined in the second component template must not be merged with the dynamic template with the " +
"same name defined in the first component template", mapping.size(), is(1));
assertThat(mapping.get("type"), is("keyword"));
}

private IndexTemplateMetadata addMatchingTemplate(Consumer<IndexTemplateMetadata.Builder> configurator) {
IndexTemplateMetadata.Builder builder = templateMetadataBuilder("template1", "te*");
configurator.accept(builder);
Expand Down