Merge pull request #12897 from hashicorp/jbardin/ignore-changes
ignore_changes causes keys in other flatmapped objects to be lost from diff
This commit is contained in:
commit
b7152c4405
|
@ -3095,3 +3095,54 @@ func TestContext2Plan_listOrder(t *testing.T) {
|
||||||
t.Fatal("aws_instance.a and aws_instance.b diffs should match:\n", plan)
|
t.Fatal("aws_instance.a and aws_instance.b diffs should match:\n", plan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure ignore-changes doesn't interfere with set/list/map diffs.
|
||||||
|
// If a resource was being replaced by a RequiresNew attribute that gets
|
||||||
|
// ignored, we need to filter the diff properly to properly update rather than
|
||||||
|
// replace.
|
||||||
|
func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) {
|
||||||
|
m := testModule(t, "plan-ignore-changes-with-flatmaps")
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
s := &State{
|
||||||
|
Modules: []*ModuleState{
|
||||||
|
&ModuleState{
|
||||||
|
Path: rootModulePath,
|
||||||
|
Resources: map[string]*ResourceState{
|
||||||
|
"aws_instance.foo": &ResourceState{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Primary: &InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
Attributes: map[string]string{
|
||||||
|
"user_data": "x",
|
||||||
|
"require_new": "",
|
||||||
|
"set.#": "1",
|
||||||
|
"set.0.a": "1",
|
||||||
|
"lst.#": "1",
|
||||||
|
"lst.0": "j",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Module: m,
|
||||||
|
Providers: map[string]ResourceProviderFactory{
|
||||||
|
"aws": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
State: s,
|
||||||
|
})
|
||||||
|
|
||||||
|
plan, err := ctx.Plan()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(plan.Diff.String())
|
||||||
|
expected := strings.TrimSpace(testTFPlanDiffIgnoreChangesWithFlatmaps)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -262,6 +262,11 @@ func testDiffFn(
|
||||||
if _, ok := c.Raw["__"+k+"_requires_new"]; ok {
|
if _, ok := c.Raw["__"+k+"_requires_new"]; ok {
|
||||||
attrDiff.RequiresNew = true
|
attrDiff.RequiresNew = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if attr, ok := s.Attributes[k]; ok {
|
||||||
|
attrDiff.Old = attr
|
||||||
|
}
|
||||||
|
|
||||||
diff.Attributes[k] = attrDiff
|
diff.Attributes[k] = attrDiff
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,9 @@ const (
|
||||||
DiffDestroyCreate
|
DiffDestroyCreate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// multiVal matches the index key to a flatmapped set, list or map
|
||||||
|
var multiVal = regexp.MustCompile(`\.(#|%)$`)
|
||||||
|
|
||||||
// Diff trackes the changes that are necessary to apply a configuration
|
// Diff trackes the changes that are necessary to apply a configuration
|
||||||
// to an existing infrastructure.
|
// to an existing infrastructure.
|
||||||
type Diff struct {
|
type Diff struct {
|
||||||
|
@ -808,7 +811,6 @@ func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// search for the suffix of the base of a [computed] map, list or set.
|
// search for the suffix of the base of a [computed] map, list or set.
|
||||||
multiVal := regexp.MustCompile(`\.(#|~#|%)$`)
|
|
||||||
match := multiVal.FindStringSubmatch(k)
|
match := multiVal.FindStringSubmatch(k)
|
||||||
|
|
||||||
if diffOld.NewComputed && len(match) == 2 {
|
if diffOld.NewComputed && len(match) == 2 {
|
||||||
|
|
|
@ -152,6 +152,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter out ignored resources
|
||||||
if err := n.processIgnoreChanges(diff); err != nil {
|
if err := n.processIgnoreChanges(diff); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -190,72 +191,81 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
changeType := diff.ChangeType()
|
|
||||||
|
|
||||||
// If we're just creating the resource, we shouldn't alter the
|
// If we're just creating the resource, we shouldn't alter the
|
||||||
// Diff at all
|
// Diff at all
|
||||||
if changeType == DiffCreate {
|
if diff.ChangeType() == DiffCreate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the resource has been tainted then we don't process ignore changes
|
// If the resource has been tainted then we don't process ignore changes
|
||||||
// since we MUST recreate the entire resource.
|
// since we MUST recreate the entire resource.
|
||||||
if diff.DestroyTainted {
|
if diff.GetDestroyTainted() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attrs := diff.CopyAttributes()
|
||||||
|
|
||||||
|
// get the complete set of keys we want to ignore
|
||||||
ignorableAttrKeys := make(map[string]bool)
|
ignorableAttrKeys := make(map[string]bool)
|
||||||
for _, ignoredKey := range ignoreChanges {
|
for _, ignoredKey := range ignoreChanges {
|
||||||
for k := range diff.CopyAttributes() {
|
for k := range attrs {
|
||||||
if ignoredKey == "*" || strings.HasPrefix(k, ignoredKey) {
|
if ignoredKey == "*" || strings.HasPrefix(k, ignoredKey) {
|
||||||
ignorableAttrKeys[k] = true
|
ignorableAttrKeys[k] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are replacing the resource, then we expect there to be a bunch of
|
// If the resource was being destroyed, check to see if we can ignore the
|
||||||
// extraneous attribute diffs we need to filter out for the other
|
// reason for it being destroyed.
|
||||||
// non-requires-new attributes going from "" -> "configval" or "" ->
|
if diff.GetDestroy() {
|
||||||
// "<computed>". Filtering these out allows us to see if we might be able to
|
for k, v := range attrs {
|
||||||
// skip this diff altogether.
|
if k == "id" {
|
||||||
if changeType == DiffDestroyCreate {
|
// id will always be changed if we intended to replace this instance
|
||||||
for k, v := range diff.CopyAttributes() {
|
continue
|
||||||
|
}
|
||||||
if v.Empty() || v.NewComputed {
|
if v.Empty() || v.NewComputed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any RequiresNew attribute isn't ignored, we need to keep the diff
|
||||||
|
// as-is to be able to replace the resource.
|
||||||
|
if v.RequiresNew && !ignorableAttrKeys[k] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we know that we aren't replacing the instance, we can filter
|
||||||
|
// out all the empty and computed attributes. There may be a bunch of
|
||||||
|
// extraneous attribute diffs for the other non-requires-new attributes
|
||||||
|
// going from "" -> "configval" or "" -> "<computed>".
|
||||||
|
// We must make sure any flatmapped containers are filterred (or not) as a
|
||||||
|
// whole.
|
||||||
|
containers := groupContainers(diff)
|
||||||
|
keep := map[string]bool{}
|
||||||
|
for _, v := range containers {
|
||||||
|
if v.keepDiff() {
|
||||||
|
// At least one key has changes, so list all the sibling keys
|
||||||
|
// to keep in the diff.
|
||||||
|
for k := range v {
|
||||||
|
keep[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range attrs {
|
||||||
|
if (v.Empty() || v.NewComputed) && !keep[k] {
|
||||||
ignorableAttrKeys[k] = true
|
ignorableAttrKeys[k] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we emulate the implementation of diff.RequiresNew() with one small
|
|
||||||
// tweak, we ignore the "id" attribute diff that gets added by EvalDiff,
|
|
||||||
// since that was added in reaction to RequiresNew being true.
|
|
||||||
requiresNewAfterIgnores := false
|
|
||||||
for k, v := range diff.CopyAttributes() {
|
|
||||||
if k == "id" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := ignorableAttrKeys[k]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if v.RequiresNew == true {
|
|
||||||
requiresNewAfterIgnores = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we still require resource replacement after ignores, we
|
|
||||||
// can't touch the diff, as all of the attributes will be
|
|
||||||
// required to process the replacement.
|
|
||||||
if requiresNewAfterIgnores {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we undo the two reactions to RequireNew in EvalDiff - the "id"
|
|
||||||
// attribute diff and the Destroy boolean field
|
|
||||||
log.Printf("[DEBUG] Removing 'id' diff and setting Destroy to false " +
|
|
||||||
"because after ignore_changes, this diff no longer requires replacement")
|
|
||||||
diff.DelAttribute("id")
|
|
||||||
diff.SetDestroy(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here we undo the two reactions to RequireNew in EvalDiff - the "id"
|
||||||
|
// attribute diff and the Destroy boolean field
|
||||||
|
log.Printf("[DEBUG] Removing 'id' diff and setting Destroy to false " +
|
||||||
|
"because after ignore_changes, this diff no longer requires replacement")
|
||||||
|
diff.DelAttribute("id")
|
||||||
|
diff.SetDestroy(false)
|
||||||
|
|
||||||
// If we didn't hit any of our early exit conditions, we can filter the diff.
|
// If we didn't hit any of our early exit conditions, we can filter the diff.
|
||||||
for k := range ignorableAttrKeys {
|
for k := range ignorableAttrKeys {
|
||||||
log.Printf("[DEBUG] [EvalIgnoreChanges] %s - Ignoring diff attribute: %s",
|
log.Printf("[DEBUG] [EvalIgnoreChanges] %s - Ignoring diff attribute: %s",
|
||||||
|
@ -266,6 +276,46 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a group of key-*ResourceAttrDiff pairs from the same flatmapped container
|
||||||
|
type flatAttrDiff map[string]*ResourceAttrDiff
|
||||||
|
|
||||||
|
// we need to keep all keys if any of them have a diff
|
||||||
|
func (f flatAttrDiff) keepDiff() bool {
|
||||||
|
for _, v := range f {
|
||||||
|
if !v.Empty() && !v.NewComputed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets, lists and maps need to be compared for diff inclusion as a whole, so
|
||||||
|
// group the flatmapped keys together for easier comparison.
|
||||||
|
func groupContainers(d *InstanceDiff) map[string]flatAttrDiff {
|
||||||
|
isIndex := multiVal.MatchString
|
||||||
|
containers := map[string]flatAttrDiff{}
|
||||||
|
attrs := d.CopyAttributes()
|
||||||
|
// we need to loop once to find the index key
|
||||||
|
for k := range attrs {
|
||||||
|
if isIndex(k) {
|
||||||
|
// add the key, always including the final dot to fully qualify it
|
||||||
|
containers[k[:len(k)-1]] = flatAttrDiff{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop again to find all the sub keys
|
||||||
|
for prefix, values := range containers {
|
||||||
|
for k, attrDiff := range attrs {
|
||||||
|
// we include the index value as well, since it could be part of the diff
|
||||||
|
if strings.HasPrefix(k, prefix) {
|
||||||
|
values[k] = attrDiff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers
|
||||||
|
}
|
||||||
|
|
||||||
// EvalDiffDestroy is an EvalNode implementation that returns a plain
|
// EvalDiffDestroy is an EvalNode implementation that returns a plain
|
||||||
// destroy diff.
|
// destroy diff.
|
||||||
type EvalDiffDestroy struct {
|
type EvalDiffDestroy struct {
|
||||||
|
|
|
@ -1500,7 +1500,7 @@ DIFF:
|
||||||
|
|
||||||
DESTROY/CREATE: aws_instance.foo
|
DESTROY/CREATE: aws_instance.foo
|
||||||
type: "" => "aws_instance"
|
type: "" => "aws_instance"
|
||||||
vars: "" => "foo"
|
vars: "foo" => "foo"
|
||||||
|
|
||||||
STATE:
|
STATE:
|
||||||
|
|
||||||
|
@ -1570,6 +1570,17 @@ aws_instance.foo:
|
||||||
ami = ami-abcd1234
|
ami = ami-abcd1234
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const testTFPlanDiffIgnoreChangesWithFlatmaps = `
|
||||||
|
UPDATE: aws_instance.foo
|
||||||
|
lst.#: "1" => "2"
|
||||||
|
lst.0: "j" => "j"
|
||||||
|
lst.1: "" => "k"
|
||||||
|
set.#: "1" => "1"
|
||||||
|
set.0.a: "1" => "1"
|
||||||
|
set.0.b: "" => "2"
|
||||||
|
type: "" => "aws_instance"
|
||||||
|
`
|
||||||
|
|
||||||
const testTerraformPlanIgnoreChangesWildcardStr = `
|
const testTerraformPlanIgnoreChangesWildcardStr = `
|
||||||
DIFF:
|
DIFF:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
id = "bar"
|
||||||
|
user_data = "x"
|
||||||
|
require_new = "yes"
|
||||||
|
|
||||||
|
set = {
|
||||||
|
a = "1"
|
||||||
|
b = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
lst = ["j", "k"]
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = ["require_new"]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue