func TestAtlasClient_LegitimateConflict(t *testing.T) { fakeAtlas := newFakeAtlas(t, testStateSimple) srv := fakeAtlas.Server() defer srv.Close() client, err := atlasFactory(map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, }) if err != nil { t.Fatalf("err: %s", err) } state, err := terraform.ReadState(bytes.NewReader(testStateSimple)) if err != nil { t.Fatalf("err: %s", err) } // Changing the state but not the serial. Should generate a conflict. state.RootModule().Outputs["drift"] = "happens" var stateJson bytes.Buffer if err := terraform.WriteState(state, &stateJson); err != nil { t.Fatalf("err: %s", err) } if err := client.Put(stateJson.Bytes()); err == nil { t.Fatal("Expected error from state conflict, got none.") } }
// testClient is a generic function to test any client. func testClient(t *testing.T, c Client) { var buf bytes.Buffer s := state.TestStateInitial() if err := terraform.WriteState(s, &buf); err != nil { t.Fatalf("err: %s", err) } data := buf.Bytes() if err := c.Put(data); err != nil { t.Fatalf("put: %s", err) } p, err := c.Get() if err != nil { t.Fatalf("get: %s", err) } if !bytes.Equal(p.Data, data) { t.Fatalf("bad: %#v", p) } if err := c.Delete(); err != nil { t.Fatalf("delete: %s", err) } p, err = c.Get() if err != nil { t.Fatalf("get: %s", err) } if p != nil { t.Fatalf("bad: %#v", p) } }
func TestAtlasClient_NoConflict(t *testing.T) { fakeAtlas := newFakeAtlas(t, testStateSimple) srv := fakeAtlas.Server() defer srv.Close() client, err := atlasFactory(map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, }) if err != nil { t.Fatalf("err: %s", err) } state, err := terraform.ReadState(bytes.NewReader(testStateSimple)) if err != nil { t.Fatalf("err: %s", err) } fakeAtlas.NoConflictAllowed(true) var stateJson bytes.Buffer if err := terraform.WriteState(state, &stateJson); err != nil { t.Fatalf("err: %s", err) } if err := client.Put(stateJson.Bytes()); err != nil { t.Fatalf("err: %s", err) } }
func TestInit_remoteStateWithLocal(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) statePath := filepath.Join(tmp, DefaultStateFilename) // Write some state f, err := os.Create(statePath) if err != nil { t.Fatalf("err: %s", err) } err = terraform.WriteState(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } ui := new(cli.MockUi) c := &InitCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{ "-backend", "http", "-backend-config", "address=http://google.com", testFixturePath("init"), } if code := c.Run(args); code == 0 { t.Fatalf("should have failed: \n%s", ui.OutputWriter.String()) } }
func TestOutput_stateDefault(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, Outputs: map[string]string{ "foo": "bar", }, }, }, } // 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) ui := new(cli.MockUi) c := &OutputCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{ "foo", } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } actual := strings.TrimSpace(ui.OutputWriter.String()) if actual != "bar" { t.Fatalf("bad: %#v", actual) } }
// Test enabling remote state func TestRemoteConfig_enableRemote(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) // Create a non-remote enabled state s := terraform.NewState() s.Serial = 5 // Add the state at the default path fh, err := os.Create(DefaultStateFilename) if err != nil { t.Fatalf("err: %v", err) } err = terraform.WriteState(s, fh) fh.Close() if err != nil { t.Fatalf("err: %v", err) } ui := new(cli.MockUi) c := &RemoteConfigCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{ "-backend=http", "-backend-config", "address=http://example.com", "-backend-config", "access_token=test", "-pull=false", } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename) ls := &state.LocalState{Path: remotePath} if err := ls.RefreshState(); err != nil { t.Fatalf("err: %s", err) } local := ls.State() if local.Remote.Type != "http" { t.Fatalf("Bad: %#v", local.Remote) } if local.Remote.Config["address"] != "http://example.com" { t.Fatalf("Bad: %#v", local.Remote) } if local.Remote.Config["access_token"] != "test" { t.Fatalf("Bad: %#v", local.Remote) } // Backup file should exist, state file should not testRemoteLocal(t, false) testRemoteLocalBackup(t, true) }
// StatePersister impl. func (s *State) PersistState() error { s.state.IncrementSerialMaybe(s.readState) var buf bytes.Buffer if err := terraform.WriteState(s.state, &buf); err != nil { return err } return s.Client.Put(buf.Bytes()) }
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) } }
// testStateFileDefault writes the state out to the default statefile // in the cwd. Use `testCwd` to change into a temp cwd. func testStateFileDefault(t *testing.T, s *terraform.State) string { f, err := os.Create(DefaultStateFilename) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() if err := terraform.WriteState(s, f); err != nil { t.Fatalf("err: %s", err) } return DefaultStateFilename }
func testStateFile(t *testing.T, s *terraform.State) string { path := testTempFile(t) f, err := os.Create(path) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() if err := terraform.WriteState(s, f); err != nil { t.Fatalf("err: %s", err) } return path }
// TestStateInitial is the initial state that a State should have // for TestState. func TestStateInitial() *terraform.State { initial := &terraform.State{ Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root", "child"}, Outputs: map[string]string{ "foo": "bar", }, }, }, } var scratch bytes.Buffer terraform.WriteState(initial, &scratch) return initial }
// testStateFileRemote writes the state out to the remote statefile // in the cwd. Use `testCwd` to change into a temp cwd. func testStateFileRemote(t *testing.T, s *terraform.State) string { path := filepath.Join(DefaultDataDir, DefaultStateFilename) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatalf("err: %s", err) } f, err := os.Create(path) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() if err := terraform.WriteState(s, f); err != nil { t.Fatalf("err: %s", err) } return path }
func testLocalState(t *testing.T) *LocalState { f, err := ioutil.TempFile("", "tf") if err != nil { t.Fatalf("err: %s", err) } err = terraform.WriteState(TestStateInitial(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } ls := &LocalState{Path: f.Name()} if err := ls.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } return ls }
// Test the case where both managed and non managed state present func TestRemoteConfig_managedAndNonManaged(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) // Persist local remote state s := terraform.NewState() s.Serial = 5 // Write the state statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) state := &state.LocalState{Path: statePath} if err := state.WriteState(s); err != nil { t.Fatalf("err: %s", err) } if err := state.PersistState(); err != nil { t.Fatalf("err: %s", err) } // Also put a file at the default path fh, err := os.Create(DefaultStateFilename) if err != nil { t.Fatalf("err: %v", err) } err = terraform.WriteState(s, fh) fh.Close() if err != nil { t.Fatalf("err: %v", err) } ui := new(cli.MockUi) c := &RemoteConfigCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{} if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } }
func TestShow_noArgs(t *testing.T) { // Create the default state 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(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } // 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) ui := new(cli.MockUi) c := &ShowCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } }
func TestAtlasClient_UnresolvableConflict(t *testing.T) { fakeAtlas := newFakeAtlas(t, testStateSimple) // Something unexpected causes Atlas to conflict in a way that we can't fix. fakeAtlas.AlwaysConflict(true) srv := fakeAtlas.Server() defer srv.Close() client, err := atlasFactory(map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, }) if err != nil { t.Fatalf("err: %s", err) } state, err := terraform.ReadState(bytes.NewReader(testStateSimple)) if err != nil { t.Fatalf("err: %s", err) } var stateJson bytes.Buffer if err := terraform.WriteState(state, &stateJson); err != nil { t.Fatalf("err: %s", err) } doneCh := make(chan struct{}) go func() { defer close(doneCh) if err := client.Put(stateJson.Bytes()); err == nil { t.Fatal("Expected error from state conflict, got none.") } }() select { case <-doneCh: // OK case <-time.After(50 * time.Millisecond): t.Fatalf("Timed out after 50ms, probably because retrying infinitely.") } }
// Atlas returns an HTTP 409 - Conflict if the pushed state reports the same // Serial number but the checksum of the raw content differs. This can // sometimes happen when Terraform changes state representation internally // between versions in a way that's semantically neutral but affects the JSON // output and therefore the checksum. // // Here we detect and handle this situation by ticking the serial and retrying // iff for the previous state and the proposed state: // // * the serials match // * the parsed states are Equal (semantically equivalent) // // In other words, in this situation Terraform can override Atlas's detected // conflict by asserting that the state it is pushing is indeed correct. func (c *AtlasClient) handleConflict(msg string, state []byte) error { log.Printf("[DEBUG] Handling Atlas conflict response: %s", msg) if c.conflictHandlingAttempted { log.Printf("[DEBUG] Already attempted conflict resolution; returning conflict.") } else { c.conflictHandlingAttempted = true log.Printf("[DEBUG] Atlas reported conflict, checking for equivalent states.") payload, err := c.Get() if err != nil { return conflictHandlingError(err) } currentState, err := terraform.ReadState(bytes.NewReader(payload.Data)) if err != nil { return conflictHandlingError(err) } proposedState, err := terraform.ReadState(bytes.NewReader(state)) if err != nil { return conflictHandlingError(err) } if statesAreEquivalent(currentState, proposedState) { log.Printf("[DEBUG] States are equivalent, incrementing serial and retrying.") proposedState.Serial++ var buf bytes.Buffer if err := terraform.WriteState(proposedState, &buf); err != nil { return conflictHandlingError(err) } return c.Put(buf.Bytes()) } else { log.Printf("[DEBUG] States are not equivalent, returning conflict.") } } return fmt.Errorf( "Atlas detected a remote state conflict.\n\nMessage: %s", msg) }
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 TestRemotePull_local(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) s := terraform.NewState() s.Serial = 10 conf, srv := testRemoteState(t, s, 200) s = terraform.NewState() s.Serial = 5 s.Remote = conf defer srv.Close() // Store the local state statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil { t.Fatalf("err: %s", err) } f, err := os.Create(statePath) if err != nil { t.Fatalf("err: %s", err) } err = terraform.WriteState(s, f) f.Close() if err != nil { t.Fatalf("err: %s", err) } ui := new(cli.MockUi) c := &RemotePullCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } }
func TestShow_noArgsRemoteState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) // Pretend like we have a local cache of remote state remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil { t.Fatalf("err: %s", err) } f, err := os.Create(remoteStatePath) if err != nil { t.Fatalf("err: %s", err) } err = terraform.WriteState(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } ui := new(cli.MockUi) c := &ShowCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } expected := "test_instance.foo" actual := ui.OutputWriter.String() if !strings.Contains(actual, expected) { t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) } }
func TestRefresh_defaultState(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 := &RefreshCommand{ Meta: Meta{ ContextOpts: testCtxConfig(p), Ui: ui, }, } p.RefreshFn = nil p.RefreshReturn = &terraform.InstanceState{ID: "yes"} args := []string{ testFixturePath("refresh"), } 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") } 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) } actual := newState.RootModule().Resources["test_instance.foo"].Primary expected := p.RefreshReturn if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: %#v", actual) } 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) } actual = backupState.RootModule().Resources["test_instance.foo"].Primary expected = originalState.RootModule().Resources["test_instance.foo"].Primary if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: %#v", actual) } }
func TestPlan_disableBackup(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 := &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", }, }, }, }, }, } 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, "-backup", "-", 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(testPlanDisableBackupStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } // Ensure there is no backup _, err = os.Stat(statePath + DefaultBackupExtension) if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } }
func TestPlan_backup(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()) // Write out some prior state backupf, err := ioutil.TempFile("", "tf") if err != nil { t.Fatalf("err: %s", err) } backupPath := backupf.Name() backupf.Close() os.Remove(backupPath) defer os.Remove(backupPath) 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", }, }, }, }, }, } 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, "-backup", backupPath, 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(testPlanBackupStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } // Verify the backup exist 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) } actualStr := strings.TrimSpace(backupState.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } }