mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-27 09:21:14 -06:00
ae73aa2fb4
This makes the behavior of plans much more predictable, as they no longer potentially have side effects on shared remote state.
611 lines
12 KiB
Go
611 lines
12 KiB
Go
package command
|
|
|
|
import (
|
|
"bytes"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/cli"
|
|
)
|
|
|
|
func TestPlan(t *testing.T) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(testFixturePath("plan")); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
}
|
|
|
|
func TestPlan_destroy(t *testing.T) {
|
|
originalState := &terraform.State{
|
|
Modules: []*terraform.ModuleState{
|
|
&terraform.ModuleState{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.ResourceState{
|
|
"test_instance.foo": &terraform.ResourceState{
|
|
Type: "test_instance",
|
|
Primary: &terraform.InstanceState{
|
|
ID: "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
outPath := testTempFile(t)
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-destroy",
|
|
"-out", outPath,
|
|
"-state", statePath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
if !p.RefreshCalled {
|
|
t.Fatal("refresh should be called")
|
|
}
|
|
|
|
plan := testReadPlan(t, outPath)
|
|
for _, m := range plan.Diff.Modules {
|
|
for _, r := range m.Resources {
|
|
if !r.Destroy {
|
|
t.Fatalf("bad: %#v", r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlan_noState(t *testing.T) {
|
|
tmp, cwd := testCwd(t)
|
|
defer testFixCwd(t, tmp, cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
// Verify that refresh was called
|
|
if p.RefreshCalled {
|
|
t.Fatal("refresh should not be called")
|
|
}
|
|
|
|
// Verify that the provider was called with the existing state
|
|
actual := strings.TrimSpace(p.DiffState.String())
|
|
expected := strings.TrimSpace(testPlanNoStateStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad:\n\n%s", actual)
|
|
}
|
|
}
|
|
|
|
func TestPlan_outPath(t *testing.T) {
|
|
tf, err := ioutil.TempFile("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
outPath := tf.Name()
|
|
os.Remove(tf.Name())
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
p.DiffReturn = &terraform.InstanceDiff{
|
|
Destroy: true,
|
|
}
|
|
|
|
args := []string{
|
|
"-out", outPath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
f, err := os.Open(outPath)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if _, err := terraform.ReadPlan(f); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestPlan_outPathNoChange(t *testing.T) {
|
|
originalState := &terraform.State{
|
|
Modules: []*terraform.ModuleState{
|
|
&terraform.ModuleState{
|
|
Path: []string{"root"},
|
|
Resources: map[string]*terraform.ResourceState{
|
|
"test_instance.foo": &terraform.ResourceState{
|
|
Type: "test_instance",
|
|
Primary: &terraform.InstanceState{
|
|
ID: "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
tf, err := ioutil.TempFile("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
outPath := tf.Name()
|
|
os.Remove(tf.Name())
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-out", outPath,
|
|
"-state", statePath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
plan := testReadPlan(t, outPath)
|
|
if !plan.Diff.Empty() {
|
|
t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan)
|
|
}
|
|
}
|
|
|
|
func TestPlan_refresh(t *testing.T) {
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-refresh=false",
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
if p.RefreshCalled {
|
|
t.Fatal("refresh should not be called")
|
|
}
|
|
}
|
|
|
|
func TestPlan_state(t *testing.T) {
|
|
// Write out some prior state
|
|
tf, err := ioutil.TempFile("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
statePath := tf.Name()
|
|
defer os.Remove(tf.Name())
|
|
|
|
originalState := testState()
|
|
err = terraform.WriteState(originalState, tf)
|
|
tf.Close()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
// Verify that the provider was called with the existing state
|
|
actual := strings.TrimSpace(p.DiffState.String())
|
|
expected := strings.TrimSpace(testPlanStateStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad:\n\n%s", actual)
|
|
}
|
|
}
|
|
|
|
func TestPlan_stateDefault(t *testing.T) {
|
|
originalState := testState()
|
|
|
|
// Write the state file in a temporary directory with the
|
|
// default filename.
|
|
td, err := ioutil.TempDir("", "tf")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
statePath := filepath.Join(td, DefaultStateFilename)
|
|
|
|
f, err := os.Create(statePath)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
err = terraform.WriteState(originalState, f)
|
|
f.Close()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Change to that directory
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
// Verify that the provider was called with the existing state
|
|
actual := strings.TrimSpace(p.DiffState.String())
|
|
expected := strings.TrimSpace(testPlanStateDefaultStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad:\n\n%s", actual)
|
|
}
|
|
}
|
|
|
|
func TestPlan_stateFuture(t *testing.T) {
|
|
originalState := testState()
|
|
originalState.TFVersion = "99.99.99"
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code == 0 {
|
|
t.Fatal("should fail")
|
|
}
|
|
|
|
f, err := os.Open(statePath)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
newState, err := terraform.ReadState(f)
|
|
f.Close()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !newState.Equal(originalState) {
|
|
t.Fatalf("bad: %#v", newState)
|
|
}
|
|
if newState.TFVersion != originalState.TFVersion {
|
|
t.Fatalf("bad: %#v", newState)
|
|
}
|
|
}
|
|
|
|
func TestPlan_statePast(t *testing.T) {
|
|
originalState := testState()
|
|
originalState.TFVersion = "0.1.0"
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
testFixturePath("plan"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
}
|
|
|
|
func TestPlan_vars(t *testing.T) {
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.DiffFn = func(
|
|
info *terraform.InstanceInfo,
|
|
s *terraform.InstanceState,
|
|
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
|
|
if v, ok := c.Config["value"]; ok {
|
|
actual = v.(string)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
args := []string{
|
|
"-var", "foo=bar",
|
|
testFixturePath("plan-vars"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestPlan_varsUnset(t *testing.T) {
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
|
|
defaultInputReader = bytes.NewBufferString("bar\n")
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
testFixturePath("plan-vars"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
}
|
|
|
|
func TestPlan_varFile(t *testing.T) {
|
|
varFilePath := testTempFile(t)
|
|
if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.DiffFn = func(
|
|
info *terraform.InstanceInfo,
|
|
s *terraform.InstanceState,
|
|
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
|
|
if v, ok := c.Config["value"]; ok {
|
|
actual = v.(string)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
args := []string{
|
|
"-var-file", varFilePath,
|
|
testFixturePath("plan-vars"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestPlan_varFileDefault(t *testing.T) {
|
|
varFileDir := testTempDir(t)
|
|
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
|
|
if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(varFileDir); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.DiffFn = func(
|
|
info *terraform.InstanceInfo,
|
|
s *terraform.InstanceState,
|
|
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
|
|
if v, ok := c.Config["value"]; ok {
|
|
actual = v.(string)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
args := []string{
|
|
testFixturePath("plan-vars"),
|
|
}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestPlan_detailedExitcode(t *testing.T) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(testFixturePath("plan")); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{"-detailed-exitcode"}
|
|
if code := c.Run(args); code != 2 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
}
|
|
|
|
func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(testFixturePath("plan-emptydiff")); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := testProvider()
|
|
ui := new(cli.MockUi)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
ContextOpts: testCtxConfig(p),
|
|
Ui: ui,
|
|
},
|
|
}
|
|
|
|
args := []string{"-detailed-exitcode"}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
}
|
|
|
|
const planVarFile = `
|
|
foo = "bar"
|
|
`
|
|
|
|
const testPlanNoStateStr = `
|
|
<not created>
|
|
`
|
|
|
|
const testPlanStateStr = `
|
|
ID = bar
|
|
`
|
|
|
|
const testPlanStateDefaultStr = `
|
|
ID = bar
|
|
`
|