mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-01-10 02:01:50 -05:00
In 3ea1592 the plan rendering was refactored to add an extra indirection
of producing a display-oriented plan object first and then rendering from
that object.
There was a logic error while adapting the existing plan rendering code
to use the new display-oriented object: the core InstanceDiff object sets
the "Destroy" flag (a boolean) for both DiffDestroy and DiffDestroyCreate,
and so this code previously checked r.Destroy to recognize the
"destroy-create" case. This was incorrectly adapted to a check for the
display action being DiffDestroy, when it should actually have been
DiffDestroyCreate.
The effect of this bug was to cause the "(forces new resource)"
annotations to not be displayed on attributes, though the resource-level
information still correctly reflected that a new resource was required.
This fix restores the attribute-level annotations.
620 lines
14 KiB
Go
620 lines
14 KiB
Go
package format
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/colorstring"
|
|
)
|
|
|
|
var disabledColorize = &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
}
|
|
|
|
func TestNewPlan(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input *terraform.Plan
|
|
Want *Plan
|
|
}{
|
|
"nil input": {
|
|
Input: nil,
|
|
Want: &Plan{
|
|
Resources: nil,
|
|
},
|
|
},
|
|
"nil diff": {
|
|
Input: &terraform.Plan{},
|
|
Want: &Plan{
|
|
Resources: nil,
|
|
},
|
|
},
|
|
"empty diff": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: nil,
|
|
},
|
|
},
|
|
"create managed resource": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo": {
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"id": {
|
|
NewComputed: true,
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffCreate,
|
|
Attributes: []*AttributeDiff{
|
|
{
|
|
Path: "id",
|
|
Action: terraform.DiffCreate,
|
|
NewComputed: true,
|
|
ForcesNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"create managed resource in child module": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo": {
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"id": {
|
|
NewComputed: true,
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Path: []string{"root", "foo"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo": {
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"id": {
|
|
NewComputed: true,
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffCreate,
|
|
Attributes: []*AttributeDiff{
|
|
{
|
|
Path: "id",
|
|
Action: terraform.DiffCreate,
|
|
NewComputed: true,
|
|
ForcesNew: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("module.foo.test_resource.foo"),
|
|
Action: terraform.DiffCreate,
|
|
Attributes: []*AttributeDiff{
|
|
{
|
|
Path: "id",
|
|
Action: terraform.DiffCreate,
|
|
NewComputed: true,
|
|
ForcesNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"create data resource": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"data.test_data_source.foo": {
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"id": {
|
|
NewComputed: true,
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("data.test_data_source.foo"),
|
|
Action: terraform.DiffRefresh,
|
|
Attributes: []*AttributeDiff{
|
|
{
|
|
Path: "id",
|
|
Action: terraform.DiffUpdate,
|
|
NewComputed: true,
|
|
ForcesNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"destroy managed resource": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo": {
|
|
Destroy: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"destroy data resource": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"data.test_data_source.foo": {
|
|
Destroy: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
// Data source destroys are not shown
|
|
Resources: nil,
|
|
},
|
|
},
|
|
"destroy many instances of a resource": {
|
|
Input: &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo.0": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.1": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.10": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.2": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.3": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.4": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.5": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.6": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.7": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.8": {
|
|
Destroy: true,
|
|
},
|
|
"test_resource.foo.9": {
|
|
Destroy: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Want: &Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[0]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[1]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[2]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[3]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[4]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[5]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[6]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[7]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[8]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[9]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo[10]"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := NewPlan(test.Input)
|
|
if !reflect.DeepEqual(got, test.Want) {
|
|
t.Errorf(
|
|
"wrong result\ninput: %sgot: %swant:%s",
|
|
spew.Sdump(test.Input),
|
|
spew.Sdump(got),
|
|
spew.Sdump(test.Want),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanStats(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input *Plan
|
|
Want PlanStats
|
|
}{
|
|
"empty": {
|
|
&Plan{},
|
|
PlanStats{},
|
|
},
|
|
"destroy": {
|
|
&Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.bar"),
|
|
Action: terraform.DiffDestroy,
|
|
},
|
|
},
|
|
},
|
|
PlanStats{
|
|
ToDestroy: 2,
|
|
},
|
|
},
|
|
"create": {
|
|
&Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffCreate,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.bar"),
|
|
Action: terraform.DiffCreate,
|
|
},
|
|
},
|
|
},
|
|
PlanStats{
|
|
ToAdd: 2,
|
|
},
|
|
},
|
|
"update": {
|
|
&Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffUpdate,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.bar"),
|
|
Action: terraform.DiffUpdate,
|
|
},
|
|
},
|
|
},
|
|
PlanStats{
|
|
ToChange: 2,
|
|
},
|
|
},
|
|
"data source refresh": {
|
|
&Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("data.test.foo"),
|
|
Action: terraform.DiffRefresh,
|
|
},
|
|
},
|
|
},
|
|
PlanStats{
|
|
// data resource refreshes are not counted in our stats
|
|
},
|
|
},
|
|
"replace": {
|
|
&Plan{
|
|
Resources: []*InstanceDiff{
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.foo"),
|
|
Action: terraform.DiffDestroyCreate,
|
|
},
|
|
{
|
|
Addr: mustParseResourceAddress("test_resource.bar"),
|
|
Action: terraform.DiffDestroyCreate,
|
|
},
|
|
},
|
|
},
|
|
PlanStats{
|
|
ToDestroy: 2,
|
|
ToAdd: 2,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := test.Input.Stats()
|
|
if !reflect.DeepEqual(got, test.Want) {
|
|
t.Errorf(
|
|
"wrong result\ninput: %sgot: %swant:%s",
|
|
spew.Sdump(test.Input),
|
|
spew.Sdump(got),
|
|
spew.Sdump(test.Want),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test that deposed instances are marked as such
|
|
func TestPlan_destroyDeposed(t *testing.T) {
|
|
plan := &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
&terraform.ModuleDiff{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"aws_instance.foo": &terraform.InstanceDiff{
|
|
DestroyDeposed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
dispPlan := NewPlan(plan)
|
|
actual := dispPlan.Format(disabledColorize)
|
|
|
|
expected := strings.TrimSpace(`
|
|
- aws_instance.foo (deposed)
|
|
`)
|
|
if actual != expected {
|
|
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
}
|
|
}
|
|
|
|
// Test that computed fields with an interpolation string get displayed
|
|
func TestPlan_displayInterpolations(t *testing.T) {
|
|
plan := &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
&terraform.ModuleDiff{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"aws_instance.foo": &terraform.InstanceDiff{
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"computed_field": &terraform.ResourceAttrDiff{
|
|
New: "${aws_instance.other.id}",
|
|
NewComputed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
dispPlan := NewPlan(plan)
|
|
out := dispPlan.Format(disabledColorize)
|
|
lines := strings.Split(out, "\n")
|
|
if len(lines) != 2 {
|
|
t.Fatal("expected 2 lines of output, got:\n", out)
|
|
}
|
|
|
|
actual := strings.TrimSpace(lines[1])
|
|
expected := `computed_field: "" => "${aws_instance.other.id}"`
|
|
|
|
if actual != expected {
|
|
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
}
|
|
}
|
|
|
|
// Ensure that (forces new resource) text is included
|
|
// https://github.com/hashicorp/terraform/issues/16035
|
|
func TestPlan_forcesNewResource(t *testing.T) {
|
|
plan := &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
&terraform.ModuleDiff{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"test_resource.foo": &terraform.InstanceDiff{
|
|
Destroy: true,
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"A": &terraform.ResourceAttrDiff{
|
|
New: "B",
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
dispPlan := NewPlan(plan)
|
|
actual := dispPlan.Format(disabledColorize)
|
|
|
|
expected := strings.TrimSpace(`
|
|
-/+ test_resource.foo (new resource required)
|
|
A: "" => "B" (forces new resource)
|
|
`)
|
|
if actual != expected {
|
|
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
}
|
|
}
|
|
|
|
// Test that a root level data source gets a special plan output on create
|
|
func TestPlan_rootDataSource(t *testing.T) {
|
|
plan := &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
&terraform.ModuleDiff{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"data.type.name": &terraform.InstanceDiff{
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"A": &terraform.ResourceAttrDiff{
|
|
New: "B",
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
dispPlan := NewPlan(plan)
|
|
actual := dispPlan.Format(disabledColorize)
|
|
|
|
expected := strings.TrimSpace(`
|
|
<= data.type.name
|
|
A: "B"
|
|
`)
|
|
if actual != expected {
|
|
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
}
|
|
}
|
|
|
|
// Test that data sources nested in modules get the same plan output
|
|
func TestPlan_nestedDataSource(t *testing.T) {
|
|
plan := &terraform.Plan{
|
|
Diff: &terraform.Diff{
|
|
Modules: []*terraform.ModuleDiff{
|
|
&terraform.ModuleDiff{
|
|
Path: []string{"root", "nested"},
|
|
Resources: map[string]*terraform.InstanceDiff{
|
|
"data.type.name": &terraform.InstanceDiff{
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
"A": &terraform.ResourceAttrDiff{
|
|
New: "B",
|
|
RequiresNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
dispPlan := NewPlan(plan)
|
|
actual := dispPlan.Format(disabledColorize)
|
|
|
|
expected := strings.TrimSpace(`
|
|
<= module.nested.data.type.name
|
|
A: "B"
|
|
`)
|
|
if actual != expected {
|
|
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
}
|
|
}
|
|
|
|
func mustParseResourceAddress(s string) *terraform.ResourceAddress {
|
|
addr, err := terraform.ParseResourceAddress(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return addr
|
|
}
|