opentofu/terraform/diff_test.go
Mitchell Hashimoto fe32f7b189
terraform: ignore RequiresNew for collection removal in diff.Same
Fixes #11349

I tracked this bug back to the early 0.7 days so this has been around a
really long time. I wanted to confirm that this wasn't introduced by any
new graph changes and it appears to predate all of that. I couldn't find
a single 0.7.x release where this worked, and I didn't want to go back
to 0.6.x since it was pre-vendoring.

The test case shows the logic the best, but the basic idea is: for
collections that go to zero elements, the "RequiresNew" sameness check
should be ignored, since the new diff can choose to not have that at all
in the diff.
2017-02-06 17:46:48 -08:00

1158 lines
21 KiB
Go

package terraform
import (
"fmt"
"reflect"
"strings"
"testing"
)
func TestDiffEmpty(t *testing.T) {
var diff *Diff
if !diff.Empty() {
t.Fatal("should be empty")
}
diff = new(Diff)
if !diff.Empty() {
t.Fatal("should be empty")
}
mod := diff.AddModule(rootModulePath)
mod.Resources["nodeA"] = &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "foo",
New: "bar",
},
},
}
if diff.Empty() {
t.Fatal("should not be empty")
}
}
func TestDiffEmpty_taintedIsNotEmpty(t *testing.T) {
diff := new(Diff)
mod := diff.AddModule(rootModulePath)
mod.Resources["nodeA"] = &InstanceDiff{
DestroyTainted: true,
}
if diff.Empty() {
t.Fatal("should not be empty, since DestroyTainted was set")
}
}
func TestDiffEqual(t *testing.T) {
cases := map[string]struct {
D1, D2 *Diff
Equal bool
}{
"nil": {
nil,
new(Diff),
false,
},
"empty": {
new(Diff),
new(Diff),
true,
},
"different module order": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}},
&ModuleDiff{Path: []string{"root", "bar"}},
},
},
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "bar"}},
&ModuleDiff{Path: []string{"root", "foo"}},
},
},
true,
},
"different module diff destroys": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: false},
},
},
true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
actual := tc.D1.Equal(tc.D2)
if actual != tc.Equal {
t.Fatalf("expected: %v\n\n%#v\n\n%#v", tc.Equal, tc.D1, tc.D2)
}
})
}
}
func TestDiffPrune(t *testing.T) {
cases := map[string]struct {
D1, D2 *Diff
}{
"nil": {
nil,
nil,
},
"empty": {
new(Diff),
new(Diff),
},
"empty module": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}},
},
},
&Diff{},
},
"destroy module": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tc.D1.Prune()
if !tc.D1.Equal(tc.D2) {
t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2)
}
})
}
}
func TestModuleDiff_ChangeType(t *testing.T) {
cases := []struct {
Diff *ModuleDiff
Result DiffChangeType
}{
{
&ModuleDiff{},
DiffNone,
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": &InstanceDiff{Destroy: true},
},
},
DiffDestroy,
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
},
},
DiffUpdate,
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
},
},
DiffCreate,
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": &InstanceDiff{
Destroy: true,
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
},
},
DiffUpdate,
},
}
for i, tc := range cases {
actual := tc.Diff.ChangeType()
if actual != tc.Result {
t.Fatalf("%d: %#v", i, actual)
}
}
}
func TestDiff_DeepCopy(t *testing.T) {
cases := map[string]*Diff{
"empty": &Diff{},
"basic diff": &Diff{
Modules: []*ModuleDiff{
&ModuleDiff{
Path: []string{"root"},
Resources: map[string]*InstanceDiff{
"aws_instance.foo": &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"num": &ResourceAttrDiff{
Old: "0",
New: "2",
},
},
},
},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
dup := tc.DeepCopy()
if !reflect.DeepEqual(dup, tc) {
t.Fatalf("\n%#v\n\n%#v", dup, tc)
}
})
}
}
func TestModuleDiff_Empty(t *testing.T) {
diff := new(ModuleDiff)
if !diff.Empty() {
t.Fatal("should be empty")
}
diff.Resources = map[string]*InstanceDiff{
"nodeA": &InstanceDiff{},
}
if !diff.Empty() {
t.Fatal("should be empty")
}
diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "foo",
New: "bar",
},
}
if diff.Empty() {
t.Fatal("should not be empty")
}
diff.Resources["nodeA"].Attributes = nil
diff.Resources["nodeA"].Destroy = true
if diff.Empty() {
t.Fatal("should not be empty")
}
}
func TestModuleDiff_String(t *testing.T) {
diff := &ModuleDiff{
Resources: map[string]*InstanceDiff{
"nodeA": &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "foo",
New: "bar",
},
"bar": &ResourceAttrDiff{
Old: "foo",
NewComputed: true,
},
"longfoo": &ResourceAttrDiff{
Old: "foo",
New: "bar",
RequiresNew: true,
},
"secretfoo": &ResourceAttrDiff{
Old: "foo",
New: "bar",
Sensitive: true,
},
},
},
},
}
actual := strings.TrimSpace(diff.String())
expected := strings.TrimSpace(moduleDiffStrBasic)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
func TestInstanceDiff_ChangeType(t *testing.T) {
cases := []struct {
Diff *InstanceDiff
Result DiffChangeType
}{
{
&InstanceDiff{},
DiffNone,
},
{
&InstanceDiff{Destroy: true},
DiffDestroy,
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
DiffUpdate,
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
DiffCreate,
},
{
&InstanceDiff{
Destroy: true,
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
DiffDestroyCreate,
},
{
&InstanceDiff{
DestroyTainted: true,
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
DiffDestroyCreate,
},
}
for i, tc := range cases {
actual := tc.Diff.ChangeType()
if actual != tc.Result {
t.Fatalf("%d: %#v", i, actual)
}
}
}
func TestInstanceDiff_Empty(t *testing.T) {
var rd *InstanceDiff
if !rd.Empty() {
t.Fatal("should be empty")
}
rd = new(InstanceDiff)
if !rd.Empty() {
t.Fatal("should be empty")
}
rd = &InstanceDiff{Destroy: true}
if rd.Empty() {
t.Fatal("should not be empty")
}
rd = &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
New: "bar",
},
},
}
if rd.Empty() {
t.Fatal("should not be empty")
}
}
func TestModuleDiff_Instances(t *testing.T) {
yesDiff := &InstanceDiff{Destroy: true}
noDiff := &InstanceDiff{Destroy: true, DestroyTainted: true}
cases := []struct {
Diff *ModuleDiff
Id string
Result []*InstanceDiff
}{
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": yesDiff,
"bar": noDiff,
},
},
"foo",
[]*InstanceDiff{
yesDiff,
},
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": yesDiff,
"foo.0": yesDiff,
"bar": noDiff,
},
},
"foo",
[]*InstanceDiff{
yesDiff,
yesDiff,
},
},
{
&ModuleDiff{
Resources: map[string]*InstanceDiff{
"foo": yesDiff,
"foo.0": yesDiff,
"foo_bar": noDiff,
"bar": noDiff,
},
},
"foo",
[]*InstanceDiff{
yesDiff,
yesDiff,
},
},
}
for i, tc := range cases {
actual := tc.Diff.Instances(tc.Id)
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: %#v", i, actual)
}
}
}
func TestInstanceDiff_RequiresNew(t *testing.T) {
rd := &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
},
}
if rd.RequiresNew() {
t.Fatal("should not require new")
}
rd.Attributes["foo"].RequiresNew = true
if !rd.RequiresNew() {
t.Fatal("should require new")
}
}
func TestInstanceDiff_RequiresNew_nil(t *testing.T) {
var rd *InstanceDiff
if rd.RequiresNew() {
t.Fatal("should not require new")
}
}
func TestInstanceDiffSame(t *testing.T) {
cases := []struct {
One, Two *InstanceDiff
Same bool
Reason string
}{
{
&InstanceDiff{},
&InstanceDiff{},
true,
"",
},
{
nil,
nil,
true,
"",
},
{
&InstanceDiff{Destroy: false},
&InstanceDiff{Destroy: true},
false,
"diff: Destroy; old: false, new: true",
},
{
&InstanceDiff{Destroy: true},
&InstanceDiff{Destroy: true},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
},
},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"bar": &ResourceAttrDiff{},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
},
},
false,
"attribute mismatch: bar",
},
// Extra attributes
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{},
"bar": &ResourceAttrDiff{},
},
},
false,
"extra attributes: bar",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{RequiresNew: true},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{RequiresNew: false},
},
},
false,
"diff RequiresNew; old: true, new: false",
},
// NewComputed on primitive
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "${var.foo}",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "0",
New: "1",
},
},
},
true,
"",
},
// NewComputed on primitive, removed
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "${var.foo}",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{},
},
true,
"",
},
// NewComputed on set, removed
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.1": &ResourceAttrDiff{
Old: "foo",
New: "",
NewRemoved: true,
},
"foo.2": &ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{NewComputed: true},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.0": &ResourceAttrDiff{
Old: "",
New: "12",
},
},
},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~35964334.bar": &ResourceAttrDiff{
Old: "",
New: "${var.foo}",
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.87654323.bar": &ResourceAttrDiff{
Old: "",
New: "12",
},
},
},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{},
},
true,
"",
},
// Computed can change RequiresNew by removal, and that's okay
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
NewComputed: true,
RequiresNew: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{},
},
true,
"",
},
// Computed can change Destroy by removal, and that's okay
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
NewComputed: true,
RequiresNew: true,
},
},
Destroy: true,
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{},
},
true,
"",
},
// Computed can change Destroy by elements
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
NewComputed: true,
RequiresNew: true,
},
},
Destroy: true,
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "1",
New: "1",
},
"foo.12": &ResourceAttrDiff{
Old: "4",
New: "12",
RequiresNew: true,
},
},
Destroy: true,
},
true,
"",
},
// Computed sets may not contain all fields in the original diff, and
// because multiple entries for the same set can compute to the same
// hash before the values are computed or interpolated, the overall
// count can change as well.
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~35964334.bar": &ResourceAttrDiff{
Old: "",
New: "${var.foo}",
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "2",
},
"foo.87654323.bar": &ResourceAttrDiff{
Old: "",
New: "12",
},
"foo.87654325.bar": &ResourceAttrDiff{
Old: "",
New: "12",
},
"foo.87654325.baz": &ResourceAttrDiff{
Old: "",
New: "12",
},
},
},
true,
"",
},
// Computed values in maps will fail the "Same" check as well
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.%": &ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.%": &ResourceAttrDiff{
Old: "0",
New: "1",
NewComputed: false,
},
"foo.val": &ResourceAttrDiff{
Old: "",
New: "something",
},
},
},
true,
"",
},
// In a DESTROY/CREATE scenario, the plan diff will be run against the
// state of the old instance, while the apply diff will be run against an
// empty state (because the state is cleared when the destroy runs.)
// For complex attributes, this can result in keys that seem to disappear
// between the two diffs, when in reality everything is working just fine.
//
// Same() needs to take into account this scenario by analyzing NewRemoved
// and treating as "Same" a diff that does indeed have that key removed.
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"somemap.oldkey": &ResourceAttrDiff{
Old: "long ago",
New: "",
NewRemoved: true,
},
"somemap.newkey": &ResourceAttrDiff{
Old: "",
New: "brave new world",
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"somemap.newkey": &ResourceAttrDiff{
Old: "",
New: "brave new world",
},
},
},
true,
"",
},
// Another thing that can occur in DESTROY/CREATE scenarios is that list
// values that are going to zero have diffs that show up at plan time but
// are gone at apply time. The NewRemoved handling catches the fields and
// treats them as OK, but it also needs to treat the .# field itself as
// okay to be present in the old diff but not in the new one.
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"reqnew": &ResourceAttrDiff{
Old: "old",
New: "new",
RequiresNew: true,
},
"somemap.#": &ResourceAttrDiff{
Old: "1",
New: "0",
},
"somemap.oldkey": &ResourceAttrDiff{
Old: "long ago",
New: "",
NewRemoved: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"reqnew": &ResourceAttrDiff{
Old: "",
New: "new",
RequiresNew: true,
},
},
},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"reqnew": &ResourceAttrDiff{
Old: "old",
New: "new",
RequiresNew: true,
},
"somemap.%": &ResourceAttrDiff{
Old: "1",
New: "0",
},
"somemap.oldkey": &ResourceAttrDiff{
Old: "long ago",
New: "",
NewRemoved: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"reqnew": &ResourceAttrDiff{
Old: "",
New: "new",
RequiresNew: true,
},
},
},
true,
"",
},
// Innner computed set should allow outer change in key
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~1.outer_val": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"foo.~1.inner.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~1.inner.~2.value": &ResourceAttrDiff{
Old: "",
New: "${var.bar}",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.12.outer_val": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"foo.12.inner.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.12.inner.42.value": &ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
true,
"",
},
// Innner computed list should allow outer change in key
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~1.outer_val": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"foo.~1.inner.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.~1.inner.0.value": &ResourceAttrDiff{
Old: "",
New: "${var.bar}",
NewComputed: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.12.outer_val": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"foo.12.inner.#": &ResourceAttrDiff{
Old: "0",
New: "1",
},
"foo.12.inner.0.value": &ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
true,
"",
},
// When removing all collection items, the diff is allowed to contain
// nothing when re-creating the resource. This should be the "Same"
// since we said we were going from 1 to 0.
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.%": &ResourceAttrDiff{
Old: "1",
New: "0",
RequiresNew: true,
},
"foo.bar": &ResourceAttrDiff{
Old: "baz",
New: "",
NewRemoved: true,
RequiresNew: true,
},
},
},
&InstanceDiff{},
true,
"",
},
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo.#": &ResourceAttrDiff{
Old: "1",
New: "0",
RequiresNew: true,
},
"foo.0": &ResourceAttrDiff{
Old: "baz",
New: "",
NewRemoved: true,
RequiresNew: true,
},
},
},
&InstanceDiff{},
true,
"",
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
same, reason := tc.One.Same(tc.Two)
if same != tc.Same {
t.Fatalf("%d: expected same: %t, got %t (%s)\n\n one: %#v\n\ntwo: %#v",
i, tc.Same, same, reason, tc.One, tc.Two)
}
if reason != tc.Reason {
t.Fatalf(
"%d: bad reason\n\nexpected: %#v\n\ngot: %#v", i, tc.Reason, reason)
}
})
}
}
const moduleDiffStrBasic = `
CREATE: nodeA
bar: "foo" => "<computed>"
foo: "foo" => "bar"
longfoo: "foo" => "bar" (forces new resource)
secretfoo: "<sensitive>" => "<sensitive>" (attribute changed)
`