Merge pull request #26421 from hashicorp/jbardin/ignore-changes-map

allow ignore_changes to reference any map key
This commit is contained in:
James Bardin 2020-10-05 12:06:05 -04:00 committed by GitHub
commit ee564a5ceb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 286 additions and 62 deletions

View File

@ -608,38 +608,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{

View File

@ -638,16 +638,22 @@ meta-arguments are supported:
any difference in the current settings of a real infrastructure object any difference in the current settings of a real infrastructure object
and plans to update the remote object to match configuration. and plans to update the remote object to match configuration.
In some rare cases, settings of a remote object are modified by processes The `ignore_chages` feature is intended to be used when a resource is
outside of Terraform, which Terraform would then attempt to "fix" on the created with references to data that may change in the future, but should
next run. In order to make Terraform share management responsibilities not effect said resource after its creation. In some rare cases, settings
of a single object with a separate process, the `ignore_changes` of a remote object are modified by processes outside of Terraform, which
meta-argument specifies resource attributes that Terraform should ignore Terraform would then attempt to "fix" on the next run. In order to make
when planning updates to the associated remote object. Terraform share management responsibilities of a single object with a
separate process, the `ignore_changes` meta-argument specifies resource
attributes that Terraform should ignore when planning updates to the
associated remote object.
The arguments corresponding to the given attribute names are considered The arguments corresponding to the given attribute names are considered
when planning a _create_ operation, but are ignored when planning an when planning a _create_ operation, but are ignored when planning an
_update_. _update_. The arugments are the relative address of the attributes in the
resource. Map and list elements can be referenced using index notation,
like `tags["Name"]` and `list[0]` respectively.
```hcl ```hcl
resource "aws_instance" "example" { resource "aws_instance" "example" {
@ -663,35 +669,6 @@ meta-arguments are supported:
} }
``` ```
You can also ignore specific map elements by writing references like
`tags["Name"]` in the `ignore_changes` list, though with an important
caveat: the ignoring applies only to in-place updates to an existing
key. Adding or removing a key is treated by Terraform as a change to the
containing map itself rather than to the individual key, and so if you
wish to ignore changes to a particular tag made by an external system
you must ensure that the Terraform configuration creates a placeholder
element for that tag name so that the external system changes will be
understood as an in-place edit of that key:
```hcl
resource "aws_instance" "example" {
# ...
tags = {
# Initial value for Name is overridden by our automatic scheduled
# re-tagging process; changes to this are ignored by ignore_changes
# below.
Name = "placeholder"
}
lifecycle {
ignore_changes = [
tags["Name"],
]
}
}
```
Instead of a list, the special keyword `all` may be used to instruct Instead of a list, the special keyword `all` may be used to instruct
Terraform to ignore _all_ attributes, which means that Terraform can Terraform to ignore _all_ attributes, which means that Terraform can
create and destroy the remote object but will never propose updates to it. create and destroy the remote object but will never propose updates to it.