package command import ( "bytes" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "path/filepath" "reflect" "strings" "sync" "testing" "time" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) func TestApply(t *testing.T) { statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } // high water mark counter type hwm struct { sync.Mutex val int max int } func (t *hwm) Inc() { t.Lock() defer t.Unlock() t.val++ if t.val > t.max { t.max = t.val } } func (t *hwm) Dec() { t.Lock() defer t.Unlock() t.val-- } func (t *hwm) Max() int { t.Lock() defer t.Unlock() return t.max } func TestApply_parallelism(t *testing.T) { provider := testProvider() statePath := testTempFile(t) par := 4 // This blocks all the appy functions. We close it when we exit so // they end quickly after this test finishes. block := make(chan struct{}) // signal how many goroutines have started started := make(chan int, 100) runCount := &hwm{} provider.ApplyFn = func( i *terraform.InstanceInfo, s *terraform.InstanceState, d *terraform.InstanceDiff) (*terraform.InstanceState, error) { // Increment so we're counting parallelism started <- 1 runCount.Inc() defer runCount.Dec() // Block here to stage up our max number of parallel instances <-block return nil, nil } ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(provider), Ui: ui, }, } args := []string{ "-state", statePath, fmt.Sprintf("-parallelism=%d", par), testFixturePath("parallelism"), } // Run in a goroutine. We can get any errors from the ui.OutputWriter doneCh := make(chan int, 1) go func() { doneCh <- c.Run(args) }() timeout := time.After(5 * time.Second) // ensure things are running for i := 0; i < par; i++ { select { case <-timeout: t.Fatal("timeout waiting for all goroutines to start") case <-started: } } // a little extra sleep, since we can't ensure all goroutines from the walk have // really started time.Sleep(100 * time.Millisecond) close(block) select { case res := <-doneCh: if res != 0 { t.Fatal(ui.OutputWriter.String()) } case <-timeout: t.Fatal("timeout waiting from Run()") } // The total in flight should equal the parallelism if runCount.Max() != par { t.Fatalf("Expected parallelism: %d, got: %d", par, runCount.Max()) } } func TestApply_configInvalid(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", testTempFile(t), testFixturePath("apply-config-invalid"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } } func TestApply_defaultState(t *testing.T) { td, err := ioutil.TempDir("", "tf") if err != nil { t.Fatalf("err: %s", err) } statePath := filepath.Join(td, DefaultStateFilename) // Change to the temporary 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 := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } func TestApply_error(t *testing.T) { statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } var lock sync.Mutex errored := false p.ApplyFn = func( info *terraform.InstanceInfo, s *terraform.InstanceState, d *terraform.InstanceDiff) (*terraform.InstanceState, error) { lock.Lock() defer lock.Unlock() if !errored { errored = true return nil, fmt.Errorf("error") } return &terraform.InstanceState{ID: "foo"}, nil } p.DiffFn = func( *terraform.InstanceInfo, *terraform.InstanceState, *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { return &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ami": &terraform.ResourceAttrDiff{ New: "bar", }, }, }, nil } args := []string{ "-state", statePath, testFixturePath("apply-error"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } if len(state.RootModule().Resources) == 0 { t.Fatal("no resources in state") } } func TestApply_init(t *testing.T) { // Change to the temporary directory cwd, err := os.Getwd() if err != nil { t.Fatalf("err: %s", err) } dir := tempDir(t) if err := os.MkdirAll(dir, 0755); err != nil { t.Fatalf("err: %s", err) } if err := os.Chdir(dir); err != nil { t.Fatalf("err: %s", err) } defer os.Chdir(cwd) // Create the test fixtures statePath := testTempFile(t) ln := testHttpServer(t) defer ln.Close() // Initialize the command p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } // Build the URL to the init var u url.URL u.Scheme = "http" u.Host = ln.Addr().String() u.Path = "/header" args := []string{ "-state", statePath, u.String(), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat("hello.tf"); err != nil { t.Fatalf("err: %s", err) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } func TestApply_input(t *testing.T) { // Disable test mode so input would be asked test = false defer func() { test = true }() // Set some default reader/writers for the inputs defaultInputReader = bytes.NewBufferString("foo\n") defaultInputWriter = new(bytes.Buffer) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, testFixturePath("apply-input"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if !p.InputCalled { t.Fatal("input should be called") } } // When only a partial set of the variables are set, Terraform // should still ask for the unset ones by default (with -input=true) func TestApply_inputPartial(t *testing.T) { // Disable test mode so input would be asked test = false defer func() { test = true }() // Set some default reader/writers for the inputs defaultInputReader = bytes.NewBufferString("one\ntwo\n") defaultInputWriter = new(bytes.Buffer) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, "-var", "foo=foovalue", testFixturePath("apply-input-partial"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } expected := strings.TrimSpace(` Outputs: bar = one foo = foovalue `) testStateOutput(t, statePath, expected) } func TestApply_noArgs(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) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } func TestApply_plan(t *testing.T) { // Disable test mode so input would be asked test = false defer func() { test = true }() // Set some default reader/writers for the inputs defaultInputReader = new(bytes.Buffer) defaultInputWriter = new(bytes.Buffer) planPath := testPlanFile(t, &terraform.Plan{ Module: testModule(t, "apply"), }) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if p.InputCalled { t.Fatalf("input should not be called for plans") } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } func TestApply_plan_backup(t *testing.T) { planPath := testPlanFile(t, testPlan(t)) statePath := testTempFile(t) backupPath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, "-backup", backupPath, planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } { // Should have a backup file f, err := os.Open(backupPath) if err != nil { t.Fatalf("err: %s", err) } _, err = terraform.ReadState(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } } } func TestApply_plan_noBackup(t *testing.T) { planPath := testPlanFile(t, testPlan(t)) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, "-backup", "-", planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } // Ensure there is no backup _, err := os.Stat(statePath + DefaultBackupExtension) if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } // Ensure there is no literal "-" _, err = os.Stat("-") if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } } func TestApply_plan_remoteState(t *testing.T) { // Disable test mode so input would be asked test = false defer func() { test = true }() tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil { t.Fatalf("err: %s", err) } // Set some default reader/writers for the inputs defaultInputReader = new(bytes.Buffer) defaultInputWriter = new(bytes.Buffer) // Create a remote state state := testState() conf, srv := testRemoteState(t, state, 200) defer srv.Close() state.Remote = conf planPath := testPlanFile(t, &terraform.Plan{ Module: testModule(t, "apply"), State: state, }) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if p.InputCalled { t.Fatalf("input should not be called for plans") } // State file should be not be installed if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil { t.Fatalf("State path should not exist") } // Check for remote state if _, err := os.Stat(remoteStatePath); err != nil { t.Fatalf("missing remote state: %s", err) } } func TestApply_planWithVarFile(t *testing.T) { varFileDir := testTempDir(t) varFilePath := filepath.Join(varFileDir, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } planPath := testPlanFile(t, &terraform.Plan{ Module: testModule(t, "apply"), }) statePath := testTempFile(t) 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 := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } } func TestApply_planVars(t *testing.T) { planPath := testPlanFile(t, &terraform.Plan{ Module: testModule(t, "apply"), }) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, "-var", "foo=bar", planPath, } if code := c.Run(args); code == 0 { t.Fatal("should've failed") } } func TestApply_refresh(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) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if !p.RefreshCalled { t.Fatal("should call refresh") } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } // Should have a backup file f, err = os.Open(statePath + DefaultBackupExtension) if err != nil { t.Fatalf("err: %s", err) } backupState, err := terraform.ReadState(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } actualStr := strings.TrimSpace(backupState.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } } func TestApply_shutdown(t *testing.T) { stopped := false stopCh := make(chan struct{}) stopReplyCh := make(chan struct{}) statePath := testTempFile(t) p := testProvider() shutdownCh := make(chan struct{}) ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, ShutdownCh: shutdownCh, } p.DiffFn = func( *terraform.InstanceInfo, *terraform.InstanceState, *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { return &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ami": &terraform.ResourceAttrDiff{ New: "bar", }, }, }, nil } p.ApplyFn = func( *terraform.InstanceInfo, *terraform.InstanceState, *terraform.InstanceDiff) (*terraform.InstanceState, error) { if !stopped { stopped = true close(stopCh) <-stopReplyCh } return &terraform.InstanceState{ ID: "foo", Attributes: map[string]string{ "ami": "2", }, }, nil } go func() { <-stopCh shutdownCh <- struct{}{} // This is really dirty, but we have no other way to assure that // tf.Stop() has been called. This doesn't assure it either, but // it makes it much more certain. time.Sleep(50 * time.Millisecond) close(stopReplyCh) }() args := []string{ "-state", statePath, testFixturePath("apply-shutdown"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } if len(state.RootModule().Resources) != 1 { t.Fatalf("bad: %d", len(state.RootModule().Resources)) } } func TestApply_state(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) p := testProvider() p.DiffReturn = &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ami": &terraform.ResourceAttrDiff{ New: "bar", }, }, } ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } // Run the apply command pointing to our existing state args := []string{ "-state", statePath, testFixturePath("apply"), } 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(testApplyStateDiffStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } actual = strings.TrimSpace(p.ApplyState.String()) expected = strings.TrimSpace(testApplyStateStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } // Should have a backup file f, err = os.Open(statePath + DefaultBackupExtension) if err != nil { t.Fatalf("err: %s", err) } backupState, err := terraform.ReadState(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } // nil out the ConnInfo since that should not be restored originalState.RootModule().Resources["test_instance.foo"].Primary.Ephemeral.ConnInfo = nil actualStr := strings.TrimSpace(backupState.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } } func TestApply_stateNoExist(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "idontexist.tfstate", testFixturePath("apply"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } } func TestApply_sensitiveOutput(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } statePath := testTempFile(t) args := []string{ "-state", statePath, testFixturePath("apply-sensitive-output"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } output := ui.OutputWriter.String() if !strings.Contains(output, "notsensitive = Hello world") { t.Fatalf("bad: output should contain 'notsensitive' output\n%s", output) } if !strings.Contains(output, "sensitive = ") { t.Fatalf("bad: output should contain 'sensitive' output\n%s", output) } } func TestApply_stateFuture(t *testing.T) { originalState := testState() originalState.TFVersion = "99.99.99" statePath := testStateFile(t, originalState) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, testFixturePath("apply"), } 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 TestApply_statePast(t *testing.T) { originalState := testState() originalState.TFVersion = "0.1.0" statePath := testStateFile(t, originalState) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } args := []string{ "-state", statePath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } func TestApply_vars(t *testing.T) { statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ 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 &terraform.InstanceDiff{}, nil } args := []string{ "-var", "foo=bar", "-state", statePath, testFixturePath("apply-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 TestApply_varFile(t *testing.T) { varFilePath := testTempFile(t) if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ 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 &terraform.InstanceDiff{}, nil } args := []string{ "-var-file", varFilePath, "-state", statePath, testFixturePath("apply-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 TestApply_varFileDefault(t *testing.T) { varFileDir := testTempDir(t) varFilePath := filepath.Join(varFileDir, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } statePath := testTempFile(t) 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 := &ApplyCommand{ 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 &terraform.InstanceDiff{}, nil } args := []string{ "-state", statePath, testFixturePath("apply-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 TestApply_varFileDefaultJSON(t *testing.T) { varFileDir := testTempDir(t) varFilePath := filepath.Join(varFileDir, "terraform.tfvars.json") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFileJSON), 0644); err != nil { t.Fatalf("err: %s", err) } statePath := testTempFile(t) 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 := &ApplyCommand{ 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 &terraform.InstanceDiff{}, nil } args := []string{ "-state", statePath, testFixturePath("apply-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 TestApply_backup(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", }, }, }, }, }, } originalState.Init() statePath := testStateFile(t, originalState) backupPath := testTempFile(t) p := testProvider() p.DiffReturn = &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ami": &terraform.ResourceAttrDiff{ New: "bar", }, }, } ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } // Run the apply command pointing to our existing state args := []string{ "-state", statePath, "-backup", backupPath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } // Should have a backup file f, err = os.Open(backupPath) if err != nil { t.Fatalf("err: %s", err) } backupState, err := terraform.ReadState(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } actual := backupState.RootModule().Resources["test_instance.foo"] expected := originalState.RootModule().Resources["test_instance.foo"] if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: %#v %#v", actual, expected) } } func TestApply_disableBackup(t *testing.T) { originalState := testState() statePath := testStateFile(t, originalState) p := testProvider() p.DiffReturn = &terraform.InstanceDiff{ Attributes: map[string]*terraform.ResourceAttrDiff{ "ami": &terraform.ResourceAttrDiff{ New: "bar", }, }, } ui := new(cli.MockUi) c := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } // Run the apply command pointing to our existing state args := []string{ "-state", statePath, "-backup", "-", testFixturePath("apply"), } 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(testApplyDisableBackupStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } actual = strings.TrimSpace(p.ApplyState.String()) expected = strings.TrimSpace(testApplyDisableBackupStateStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() state, err := terraform.ReadState(f) if err != nil { t.Fatalf("err: %s", err) } if state == nil { t.Fatal("state should not be nil") } // Ensure there is no backup _, err = os.Stat(statePath + DefaultBackupExtension) if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } // Ensure there is no literal "-" _, err = os.Stat("-") if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } } // -state-out wasn't taking effect when a plan is supplied. GH-7264 func TestApply_stateOutWithPlan(t *testing.T) { p := testProvider() ui := new(cli.MockUi) tmpDir := testTempDir(t) defer os.RemoveAll(tmpDir) statePath := filepath.Join(tmpDir, "state.tfstate") planPath := filepath.Join(tmpDir, "terraform.tfplan") args := []string{ "-state", statePath, "-out", planPath, testFixturePath("plan"), } // Run plan first to get a current plan file pc := &PlanCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } if code := pc.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } // now run apply with the generated plan stateOutPath := filepath.Join(tmpDir, "state-new.tfstate") args = []string{ "-state", statePath, "-state-out", stateOutPath, planPath, } ac := &ApplyCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } if code := ac.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } // now make sure we wrote out our new state if _, err := os.Stat(stateOutPath); err != nil { t.Fatalf("missing new state file: %s", err) } } func testHttpServer(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("err: %s", err) } mux := http.NewServeMux() mux.HandleFunc("/header", testHttpHandlerHeader) var server http.Server server.Handler = mux go server.Serve(ln) return ln } func testHttpHandlerHeader(w http.ResponseWriter, r *http.Request) { var url url.URL url.Scheme = "file" url.Path = filepath.ToSlash(testFixturePath("init")) w.Header().Add("X-Terraform-Get", url.String()) w.WriteHeader(200) } const applyVarFile = ` foo = "bar" ` const applyVarFileJSON = ` { "foo": "bar" } ` const testApplyDisableBackupStr = ` ID = bar Tainted = false ` const testApplyDisableBackupStateStr = ` ID = bar Tainted = false ` const testApplyStateStr = ` ID = bar Tainted = false ` const testApplyStateDiffStr = ` ID = bar Tainted = false `