// blankState is used to return a serialized form of a blank state // with only the remote info. func blankState(conf *terraform.RemoteState) ([]byte, error) { blank := terraform.NewState() blank.Remote = conf buf := bytes.NewBuffer(nil) err := terraform.WriteState(blank, buf) return buf.Bytes(), err }
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) } }
func TestPlan_stateDefault(t *testing.T) { originalState := &terraform.State{ Resources: map[string]*terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{ ID: "bar", Type: "test_instance", }, }, } // 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 expectedState := originalState.Resources["test_instance.foo"] if !reflect.DeepEqual(p.DiffState, expectedState) { t.Fatalf("bad: %#v", p.DiffState) } }
func TestPull_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 buf := bytes.NewBuffer(nil) terraform.WriteState(s, buf) remote.EnsureDirectory() remote.Persist(buf) ui := new(cli.MockUi) c := &PullCommand{ 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 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 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"] = &terraform.OutputState{ Type: "string", Sensitive: false, Value: "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.") } }
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) } }
// 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) } }
// 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()) }
// PersistState is used to persist out the given terraform state // in our local state cache location. func PersistState(s *terraform.State) error { buf := bytes.NewBuffer(nil) if err := terraform.WriteState(s, buf); err != nil { return fmt.Errorf("Failed to encode state: %v", err) } if err := Persist(buf); err != nil { return err } return nil }
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{ Resources: map[string]*terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{ ID: "bar", Type: "test_instance", }, }, } 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 expectedState := originalState.Resources["test_instance.foo"] if !reflect.DeepEqual(p.DiffState, expectedState) { t.Fatalf("bad: %#v", p.DiffState) } // Ensure there is no backup _, err = os.Stat(statePath + DefaultBackupExtention) if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } }
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 WriteState(state *terraform.State, outFile string) error { // TODO: Make this file location customizable, and also don't re-open this // file every time. f, err := os.OpenFile(outFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("error opening state file: %s\n", err) } err = terraform.WriteState(state, f) if err != nil { return fmt.Errorf("error writing state: %s\n", err) } return nil }
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]interface{}{ "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()) } }
// 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]*terraform.OutputState{ "foo": &terraform.OutputState{ Type: "string", Sensitive: false, Value: "bar", }, }, }, }, } var scratch bytes.Buffer terraform.WriteState(initial, &scratch) return initial }
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(500 * time.Millisecond): t.Fatalf("Timed out after 500ms, probably because retrying infinitely.") } }
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()) } }
// 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 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 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) } }
// WriteState for LocalState always persists the state as well. // // StateWriter impl. func (s *LocalState) WriteState(state *terraform.State) error { s.state = state path := s.PathOut if path == "" { path = s.Path } // If we don't have any state, we actually delete the file if it exists if state == nil { err := os.Remove(path) if err != nil && os.IsNotExist(err) { return nil } return err } // Create all the directories if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } f, err := os.Create(path) if err != nil { return err } defer f.Close() s.state.IncrementSerialMaybe(s.readState) s.readState = s.state if err := terraform.WriteState(s.state, f); err != nil { return err } s.written = true return nil }
// Test disabling remote management with a state file in the way func TestRemote_disable_otherState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) // Persist local remote state s := terraform.NewState() s.Serial = 5 if err := remote.EnsureDirectory(); err != nil { t.Fatalf("err: %v", err) } if err := remote.PersistState(s); err != nil { t.Fatalf("err: %v", 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 := &RemoteCommand{ Meta: Meta{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, }, } args := []string{"-disable"} if code := c.Run(args); code != 1 { 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) } }
// persistLocalState is used to handle persisting a state file // when remote state management is disabled. func (m *Meta) persistLocalState(s *terraform.State) error { m.initStatePaths() // Create a backup of the state before updating if m.backupPath != "-" { log.Printf("[INFO] Writing backup state to: %s", m.backupPath) if err := remote.CopyFile(m.statePath, m.backupPath); err != nil { return fmt.Errorf("Failed to backup state: %v", err) } } // Open the new state file fh, err := os.Create(m.stateOutPath) if err != nil { return fmt.Errorf("Failed to open state file: %v", err) } defer fh.Close() // Write out the state if err := terraform.WriteState(s, fh); err != nil { return fmt.Errorf("Failed to encode the state: %v", err) } return nil }