allow ignore_changes to reference any map key

There are situations when a user may want to keep or exclude a map key
using `ignore_changes` which may not be listed directly in the
configuration. This didn't work previously because the transformation
always started off with the configuration, and would never encounter a
key if it was only present in the prior value.
This commit is contained in:
James Bardin 2020-09-29 15:36:49 -04:00
parent a78d75ccfb
commit ad0b81de81
2 changed files with 273 additions and 26 deletions

View File

@ -607,38 +607,127 @@ func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChanges []hcl
ignoreChangesPath[i] = path ignoreChangesPath[i] = path
} }
var diags tfdiags.Diagnostics type ignoreChange struct {
ret, _ := cty.Transform(config, func(path cty.Path, v cty.Value) (cty.Value, error) { // Path is the full path, minus any trailing map index
// First we must see if this is a path that's being ignored at all. path cty.Path
// We're looking for an exact match here because this walk will visit // Value is the value we are to retain at the above path. If there is a
// leaf values first and then their containers, and we want to do // key value, this must be a map and the desired value will be at the
// the "ignore" transform once we reach the point indicated, throwing // key index.
// away any deeper values we already produced at that point. value cty.Value
var ignoreTraversal hcl.Traversal // Key is the index key if the ignored path ends in a map index.
for i, candidate := range ignoreChangesPath { key cty.Value
if path.Equals(candidate) { }
ignoreTraversal = ignoreChanges[i] var ignoredValues []ignoreChange
// Find the actual changes first and store them in the ignoreChange struct.
// If the change was to a map value, and the key doesn't exist in the
// config, it would never be visited in the transform walk.
for _, icPath := range ignoreChangesPath {
key := cty.NullVal(cty.String)
// check for a map index, since maps are the only structure where we
// could have invalid path steps.
last, ok := icPath[len(icPath)-1].(cty.IndexStep)
if ok {
if last.Key.Type() == cty.String {
icPath = icPath[:len(icPath)-1]
key = last.Key
} }
} }
if ignoreTraversal == nil {
return v, nil // The structure should have been validated already, and we already
// trimmed the trailing map index. Any other intermediate index error
// means we wouldn't be able to apply the value below, so no need to
// record this.
p, err := icPath.Apply(prior)
if err != nil {
continue
}
c, err := icPath.Apply(config)
if err != nil {
continue
} }
// If we're able to follow the same path through the prior value, // If this is a map, it is checking the entire map value for equality
// we'll take the value there instead, effectively undoing the // rather than the individual key. This means that the change is stored
// change that was planned. // here even if our ignored key doesn't change. That is OK since it
priorV, diags := hcl.ApplyPath(prior, path, nil) // won't cause any changes in the transformation, but allows us to skip
if diags.HasErrors() { // breaking up the maps and checking for key existence here too.
// We just ignore the errors and move on here, since we assume it's eq := p.Equals(c)
// just because the prior value was a slightly-different shape. if eq.IsKnown() && eq.False() {
// It could potentially also be that the traversal doesn't match // there a change to ignore at this path, store the prior value
// the schema, but we should've caught that during the validate ignoredValues = append(ignoredValues, ignoreChange{icPath, p, key})
// walk if so.
return v, nil
} }
return priorV, nil }
if len(ignoredValues) == 0 {
return config, nil
}
ret, _ := cty.Transform(config, func(path cty.Path, v cty.Value) (cty.Value, error) {
for _, ignored := range ignoredValues {
if !path.Equals(ignored.path) {
return v, nil
}
// no index, so we can return the entire value
if ignored.key.IsNull() {
return ignored.value, nil
}
// we have an index key, so make sure we have a map
if !v.Type().IsMapType() {
// we'll let other validation catch any type mismatch
return v, nil
}
// Now we know we are ignoring a specific index of this map, so get
// the config map and modify, add, or remove the desired key.
var configMap map[string]cty.Value
var priorMap map[string]cty.Value
if !v.IsNull() {
if !v.IsKnown() {
// if the entire map is not known, we can't ignore any
// specific keys yet.
continue
}
configMap = v.AsValueMap()
}
if configMap == nil {
configMap = map[string]cty.Value{}
}
// We also need to create a prior map, so we can check for
// existence while getting the value. Value.Index will always
// return null.
if !ignored.value.IsNull() {
priorMap = ignored.value.AsValueMap()
}
if priorMap == nil {
priorMap = map[string]cty.Value{}
}
key := ignored.key.AsString()
priorElem, keep := priorMap[key]
switch {
case !keep:
// this didn't exist in the old map value, so we're keeping the
// "absence" of the key by removing it from the config
delete(configMap, key)
default:
configMap[key] = priorElem
}
if len(configMap) == 0 {
return cty.MapValEmpty(v.Type().ElementType()), nil
}
return cty.MapVal(configMap), nil
}
return v, nil
}) })
return ret, diags return ret, nil
} }
// a group of key-*ResourceAttrDiff pairs from the same flatmapped container // a group of key-*ResourceAttrDiff pairs from the same flatmapped container

View File

@ -68,6 +68,164 @@ func TestProcessIgnoreChangesIndividual(t *testing.T) {
"b": cty.StringVal("new b value"), "b": cty.StringVal("new b value"),
}), }),
}, },
"list_index": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("a0 value"),
cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("new a0 value"),
cty.StringVal("new a1 value"),
}),
"b": cty.StringVal("new b value"),
}),
[]string{"a[1]"},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("new a0 value"),
cty.StringVal("a1 value"),
}),
"b": cty.StringVal("new b value"),
}),
},
"map_index": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("new a0 value"),
"a1": cty.StringVal("new a1 value"),
}),
"b": cty.StringVal("b value"),
}),
[]string{`a["a1"]`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("new a0 value"),
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
},
"map_index_no_config": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.Map(cty.String)),
"b": cty.StringVal("b value"),
}),
[]string{`a["a1"]`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
},
"missing_map_index": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(cty.String),
"b": cty.StringVal("b value"),
}),
[]string{`a["a1"]`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a1": cty.StringVal("a1 value"),
}),
"b": cty.StringVal("b value"),
}),
},
"missing_map_index_empty": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a0 value"),
}),
}),
[]string{`a["a"]`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(cty.String),
}),
},
"missing_map_index_to_object": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("aa0"),
"b": cty.StringVal("ab0"),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("ba0"),
"b": cty.StringVal("bb0"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(
cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.String,
}),
),
}),
// we expect the config to be used here, as the ignore changes was
// `a["a"].b`, but the change was larger than that removing
// `a["a"]` entirely.
[]string{`a["a"].b`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(
cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.String,
}),
),
}),
},
"missing_prior_map_index": {
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
}),
"b": cty.StringVal("b value"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
"a1": cty.StringVal("new a1 value"),
}),
"b": cty.StringVal("b value"),
}),
[]string{`a["a1"]`},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"a0": cty.StringVal("a0 value"),
}),
"b": cty.StringVal("b value"),
}),
},
"object attribute": { "object attribute": {
cty.ObjectVal(map[string]cty.Value{ cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{