mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-01-03 07:00:28 -05:00
Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
302 lines
7.3 KiB
Go
302 lines
7.3 KiB
Go
package command
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/backend/local"
|
|
"github.com/hashicorp/terraform/state"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/cli"
|
|
)
|
|
|
|
func TestWorkspace_createAndChange(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
newCmd := &WorkspaceNewCommand{}
|
|
|
|
current := newCmd.Workspace()
|
|
if current != backend.DefaultStateName {
|
|
t.Fatal("current workspace should be 'default'")
|
|
}
|
|
|
|
args := []string{"test"}
|
|
ui := new(cli.MockUi)
|
|
newCmd.Meta = Meta{Ui: ui}
|
|
if code := newCmd.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
|
|
current = newCmd.Workspace()
|
|
if current != "test" {
|
|
t.Fatalf("current workspace should be 'test', got %q", current)
|
|
}
|
|
|
|
selCmd := &WorkspaceSelectCommand{}
|
|
args = []string{backend.DefaultStateName}
|
|
ui = new(cli.MockUi)
|
|
selCmd.Meta = Meta{Ui: ui}
|
|
if code := selCmd.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
|
|
current = newCmd.Workspace()
|
|
if current != backend.DefaultStateName {
|
|
t.Fatal("current workspace should be 'default'")
|
|
}
|
|
|
|
}
|
|
|
|
// Create some workspaces and test the list output.
|
|
// This also ensures we switch to the correct env after each call
|
|
func TestWorkspace_createAndList(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// make sure a vars file doesn't interfere
|
|
err := ioutil.WriteFile(
|
|
DefaultVarsFilename,
|
|
[]byte(`foo = "bar"`),
|
|
0644,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newCmd := &WorkspaceNewCommand{}
|
|
|
|
envs := []string{"test_a", "test_b", "test_c"}
|
|
|
|
// create multiple workspaces
|
|
for _, env := range envs {
|
|
ui := new(cli.MockUi)
|
|
newCmd.Meta = Meta{Ui: ui}
|
|
if code := newCmd.Run([]string{env}); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
}
|
|
|
|
listCmd := &WorkspaceListCommand{}
|
|
ui := new(cli.MockUi)
|
|
listCmd.Meta = Meta{Ui: ui}
|
|
|
|
if code := listCmd.Run(nil); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
|
|
actual := strings.TrimSpace(ui.OutputWriter.String())
|
|
expected := "default\n test_a\n test_b\n* test_c"
|
|
|
|
if actual != expected {
|
|
t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual)
|
|
}
|
|
}
|
|
|
|
// Don't allow names that aren't URL safe
|
|
func TestWorkspace_createInvalid(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
newCmd := &WorkspaceNewCommand{}
|
|
|
|
envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}
|
|
|
|
// create multiple workspaces
|
|
for _, env := range envs {
|
|
ui := new(cli.MockUi)
|
|
newCmd.Meta = Meta{Ui: ui}
|
|
if code := newCmd.Run([]string{env}); code == 0 {
|
|
t.Fatalf("expected failure: \n%s", ui.OutputWriter)
|
|
}
|
|
}
|
|
|
|
// list workspaces to make sure none were created
|
|
listCmd := &WorkspaceListCommand{}
|
|
ui := new(cli.MockUi)
|
|
listCmd.Meta = Meta{Ui: ui}
|
|
|
|
if code := listCmd.Run(nil); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
|
|
actual := strings.TrimSpace(ui.OutputWriter.String())
|
|
expected := "* default"
|
|
|
|
if actual != expected {
|
|
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
|
|
}
|
|
}
|
|
|
|
func TestWorkspace_createWithState(t *testing.T) {
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// create a non-empty state
|
|
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 := (&state.LocalState{Path: "test.tfstate"}).WriteState(originalState)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
args := []string{"-state", "test.tfstate", "test"}
|
|
ui := new(cli.MockUi)
|
|
newCmd := &WorkspaceNewCommand{
|
|
Meta: Meta{Ui: ui},
|
|
}
|
|
if code := newCmd.Run(args); code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
}
|
|
|
|
newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
|
|
envState := state.LocalState{Path: newPath}
|
|
err = envState.RefreshState()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newState := envState.State()
|
|
originalState.Version = newState.Version // the round-trip through the state manager implicitly populates version
|
|
if !originalState.Equal(newState) {
|
|
t.Fatalf("states not equal\norig: %s\nnew: %s", originalState, newState)
|
|
}
|
|
}
|
|
|
|
func TestWorkspace_delete(t *testing.T) {
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// create the workspace directories
|
|
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// create the workspace file
|
|
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ui := new(cli.MockUi)
|
|
delCmd := &WorkspaceDeleteCommand{
|
|
Meta: Meta{Ui: ui},
|
|
}
|
|
|
|
current := delCmd.Workspace()
|
|
if current != "test" {
|
|
t.Fatal("wrong workspace:", current)
|
|
}
|
|
|
|
// we can't delete our current workspace
|
|
args := []string{"test"}
|
|
if code := delCmd.Run(args); code == 0 {
|
|
t.Fatal("expected error deleting current workspace")
|
|
}
|
|
|
|
// change back to default
|
|
if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// try the delete again
|
|
ui = new(cli.MockUi)
|
|
delCmd.Meta.Ui = ui
|
|
if code := delCmd.Run(args); code != 0 {
|
|
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
|
|
}
|
|
|
|
current = delCmd.Workspace()
|
|
if current != backend.DefaultStateName {
|
|
t.Fatalf("wrong workspace: %q", current)
|
|
}
|
|
}
|
|
func TestWorkspace_deleteWithState(t *testing.T) {
|
|
td := tempDir(t)
|
|
os.MkdirAll(td, 0755)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// create the workspace directories
|
|
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// create a non-empty state
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
envStatePath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
|
|
err := (&state.LocalState{Path: envStatePath}).WriteState(originalState)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ui := new(cli.MockUi)
|
|
delCmd := &WorkspaceDeleteCommand{
|
|
Meta: Meta{Ui: ui},
|
|
}
|
|
args := []string{"test"}
|
|
if code := delCmd.Run(args); code == 0 {
|
|
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
|
|
}
|
|
|
|
ui = new(cli.MockUi)
|
|
delCmd.Meta.Ui = ui
|
|
|
|
args = []string{"-force", "test"}
|
|
if code := delCmd.Run(args); code != 0 {
|
|
t.Fatalf("failure: %s", ui.ErrorWriter)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
|
|
t.Fatal("env 'test' still exists!")
|
|
}
|
|
}
|