Summary
elasticstack_elasticsearch_index_mappings's schema documents: "Only the keys and fields declared here are tracked; dynamic extras added by Elasticsearch are ignored." This holds for properties (via intersectProperties), but not for dynamic_templates: after a Read, state ends up containing the full API-returned dynamic_templates array — including entries contributed by an index template — instead of just the templates the user declared in config.
Root cause
internal/elasticsearch/index/indexmappings/intersect.go's intersectMappings special-cases properties (name-aware intersection), but falls back to index.FieldSemanticallyEqual(stateVal, apiVal) for every other top-level key:
if index.FieldSemanticallyEqual(stateVal, apiVal) {
result[key] = stateVal
continue
}
result[key] = apiVal
FieldSemanticallyEqual/fieldSemanticallyEqual type-asserts both sides to map[string]any. dynamic_templates is a []any, so the assertion always fails, FieldSemanticallyEqual always returns false, and result[key] = apiVal unconditionally stores the entire API array (user templates + template-injected extras) into state.
Repro
Given:
stateMap: {"dynamic_templates": [{"text_ja_example": {...}}]} (user-declared)
apiMap: {"dynamic_templates": [{"template_default": {...}}, {"text_ja_example": {...}}]} (API, with an index-template-injected extra)
intersectMappings(apiMap, stateMap) returns dynamic_templates containing both template_default and text_ja_example, not just the user-owned text_ja_example.
Confirmed with a standalone probe test against intersectMappings:
result: {"dynamic_templates":[{"template_default":{"mapping":{"type":"keyword"},"match_mapping_type":"string"}},{"text_ja_example":{"mapping":{"type":"text"},"path_match":"hoge.example_field.freetext"}}]}
Impact
terraform state show / terraform show -json for this resource can contain dynamic_templates entries the user never declared and doesn't own, contradicting the documented "user-declared subset" contract.
- The existing acceptance test coverage doesn't catch this:
checkStateMappingsDynamicTemplates(minCount) in acc_test.go asserts len(templates) >= minCount, which can't distinguish "state has just the user's template" from "state has the user's template plus injected extras."
Suggested fix
Reuse the name-keyed comparison introduced in #4581 (dynamicTemplatesByName in internal/elasticsearch/index/mappings_value.go) inside intersectMappings/intersectProperties so dynamic_templates is intersected by template name the same way properties is intersected by field name — keeping only the user-declared template names in state. Also tighten checkStateMappingsDynamicTemplates to assert an exact set of template names rather than a minimum count, so a regression here is actually caught.
Related
Summary
elasticstack_elasticsearch_index_mappings's schema documents: "Only the keys and fields declared here are tracked; dynamic extras added by Elasticsearch are ignored." This holds forproperties(viaintersectProperties), but not fordynamic_templates: after aRead, state ends up containing the full API-returneddynamic_templatesarray — including entries contributed by an index template — instead of just the templates the user declared in config.Root cause
internal/elasticsearch/index/indexmappings/intersect.go'sintersectMappingsspecial-casesproperties(name-aware intersection), but falls back toindex.FieldSemanticallyEqual(stateVal, apiVal)for every other top-level key:FieldSemanticallyEqual/fieldSemanticallyEqualtype-asserts both sides tomap[string]any.dynamic_templatesis a[]any, so the assertion always fails,FieldSemanticallyEqualalways returnsfalse, andresult[key] = apiValunconditionally stores the entire API array (user templates + template-injected extras) into state.Repro
Given:
stateMap:{"dynamic_templates": [{"text_ja_example": {...}}]}(user-declared)apiMap:{"dynamic_templates": [{"template_default": {...}}, {"text_ja_example": {...}}]}(API, with an index-template-injected extra)intersectMappings(apiMap, stateMap)returnsdynamic_templatescontaining bothtemplate_defaultandtext_ja_example, not just the user-ownedtext_ja_example.Confirmed with a standalone probe test against
intersectMappings:Impact
terraform state show/terraform show -jsonfor this resource can containdynamic_templatesentries the user never declared and doesn't own, contradicting the documented "user-declared subset" contract.checkStateMappingsDynamicTemplates(minCount)inacc_test.goassertslen(templates) >= minCount, which can't distinguish "state has just the user's template" from "state has the user's template plus injected extras."Suggested fix
Reuse the name-keyed comparison introduced in #4581 (
dynamicTemplatesByNameininternal/elasticsearch/index/mappings_value.go) insideintersectMappings/intersectPropertiessodynamic_templatesis intersected by template name the same waypropertiesis intersected by field name — keeping only the user-declared template names in state. Also tightencheckStateMappingsDynamicTemplatesto assert an exact set of template names rather than a minimum count, so a regression here is actually caught.Related
dynamic_templatescausing false "Provider produced inconsistent result after apply" errors during the plan/apply consistency check. That fix is inmappings_value.go'sMappingsSemanticallyEqual/StringSemanticEqualspath and does not touchintersect.go, so this state-persistence issue is pre-existing and unaffected by fix(elasticsearch-index): compare dynamic templates semantically #4581.