terraform/command/format/plan_test.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
}