From 7fbd93b5cd51d2bc4eab9cc51368d9652bd75325 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Thu, 18 Oct 2018 21:41:05 +0200 Subject: [PATCH] command/state: update and fix the state push and pull --- command/state_pull.go | 32 ++-- command/state_pull_test.go | 26 ++-- command/state_push.go | 137 +++++++++--------- command/state_push_test.go | 4 +- .../.terraform/terraform.tfstate | 23 +++ .../state-pull-backend/local-state.tfstate | 24 +++ .../test-fixtures/state-pull-backend/main.tf | 5 + .../.terraform/terraform.tfstate | 5 +- .../state-push-good/replace.tfstate | 24 ++- .../.terraform/terraform.tfstate | 5 +- .../local-state.tfstate | 24 ++- .../state-push-replace-match/replace.tfstate | 24 ++- .../.terraform/terraform.tfstate | 5 +- .../local-state.tfstate | 24 ++- .../state-push-serial-newer/replace.tfstate | 24 ++- .../.terraform/terraform.tfstate | 5 +- .../local-state.tfstate | 24 ++- .../state-push-serial-older/replace.tfstate | 24 ++- states/statemgr/helper.go | 30 ++++ states/statemgr/interfaces.go | 1 - states/statemgr/lineage.go | 12 -- 21 files changed, 340 insertions(+), 142 deletions(-) create mode 100644 command/test-fixtures/state-pull-backend/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-pull-backend/local-state.tfstate create mode 100644 command/test-fixtures/state-pull-backend/main.tf delete mode 100644 states/statemgr/interfaces.go diff --git a/command/state_pull.go b/command/state_pull.go index 0f12aaf74..4a5f9be2c 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -1,9 +1,12 @@ package command import ( + "bytes" "fmt" "strings" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/mitchellh/cli" ) @@ -34,37 +37,34 @@ func (c *StatePullCommand) Run(args []string) int { // Get the state env := c.Workspace() - state, err := b.StateMgr(env) + stateMgr, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 } - if err := state.RefreshState(); err != nil { + if err := stateMgr.RefreshState(); err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 } - s := state.State() - if s == nil { + state := stateMgr.State() + if state == nil { // Output on "error" so it shows up on stderr c.Ui.Error("Empty state (no state)") - return 0 } - c.Ui.Error("state pull not yet updated for new state types") - return 1 + // Get the state file. + stateFile := statemgr.StateFile(stateMgr, state) - /* - var buf bytes.Buffer - if err := terraform.WriteState(s, &buf); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) - return 1 - } - - c.Ui.Output(buf.String()) - */ + var buf bytes.Buffer + err = statefile.Write(stateFile, &buf) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + c.Ui.Output(buf.String()) return 0 } diff --git a/command/state_pull_test.go b/command/state_pull_test.go index 9e1935444..7338f8493 100644 --- a/command/state_pull_test.go +++ b/command/state_pull_test.go @@ -1,21 +1,26 @@ package command import ( - "strings" + "bytes" + "io/ioutil" + "os" "testing" + "github.com/hashicorp/terraform/helper/copy" "github.com/mitchellh/cli" ) func TestStatePull(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-pull-backend"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() - // Create some legacy remote state - legacyState := testState() - backendState, srv := testRemoteState(t, legacyState, 200) - defer srv.Close() - testStateFileRemote(t, backendState) + expected, err := ioutil.ReadFile("local-state.tfstate") + if err != nil { + t.Fatalf("error reading state: %v", err) + } p := testProvider() ui := new(cli.MockUi) @@ -31,9 +36,8 @@ func TestStatePull(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } - expected := "test_instance.foo" - actual := ui.OutputWriter.String() - if !strings.Contains(actual, expected) { + actual := ui.OutputWriter.Bytes() + if bytes.Equal(actual, expected) { t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) } } diff --git a/command/state_push.go b/command/state_push.go index 401d5ee1b..74928a95e 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -1,8 +1,13 @@ package command import ( + "fmt" + "io" + "os" "strings" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/mitchellh/cli" ) @@ -31,86 +36,76 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } - c.Ui.Error("state push not yet updated for new state types") - return 1 - - /* - // Determine our reader for the input state. This is the filepath - // or stdin if "-" is given. - var r io.Reader = os.Stdin - if args[0] != "-" { - f, err := os.Open(args[0]) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // Note: we don't need to defer a Close here because we do a close - // automatically below directly after the read. - - r = f - } - - // Read the state - sourceState, err := terraform.ReadState(r) - if c, ok := r.(io.Closer); ok { - // Close the reader if possible right now since we're done with it. - c.Close() - } + // Determine our reader for the input state. This is the filepath + // or stdin if "-" is given. + var r io.Reader = os.Stdin + if args[0] != "-" { + f, err := os.Open(args[0]) if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) + c.Ui.Error(err.Error()) return 1 } - // Load the backend - b, backendDiags := c.Backend(nil) - if backendDiags.HasErrors() { - c.showDiagnostics(backendDiags) + // Note: we don't need to defer a Close here because we do a close + // automatically below directly after the read. + + r = f + } + + // Read the state + srcStateFile, err := statefile.Read(r) + if c, ok := r.(io.Closer); ok { + // Close the reader if possible right now since we're done with it. + c.Close() + } + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) + return 1 + } + + // Load the backend + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) + return 1 + } + + // Get the state + env := c.Workspace() + stateMgr, err := b.StateMgr(env) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) + return 1 + } + if err := stateMgr.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) + return 1 + } + dstState := stateMgr.State() + + // If we're not forcing, then perform safety checks + if !flagForce && !dstState.Empty() { + dstStateFile := statemgr.StateFile(stateMgr, dstState) + + if dstStateFile.Lineage != srcStateFile.Lineage { + c.Ui.Error(strings.TrimSpace(errStatePushLineage)) return 1 } - - // Get the state - env := c.Workspace() - state, err := b.StateMgr(env) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) - return 1 - } - if err := state.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) + if dstStateFile.Serial > srcStateFile.Serial { + c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer)) return 1 } + } - dstState := state.State() - - // If we're not forcing, then perform safety checks - if !flagForce && !dstState.Empty() { - if !dstState.SameLineage(sourceState) { - c.Ui.Error(strings.TrimSpace(errStatePushLineage)) - return 1 - } - - age, err := dstState.CompareAges(sourceState) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if age == terraform.StateAgeReceiverNewer { - c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer)) - return 1 - } - } - - // Overwrite it - if err := state.WriteState(sourceState); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) - return 1 - } - if err := state.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) - return 1 - } - */ + // Overwrite it + if err := stateMgr.WriteState(srcStateFile.State); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } + if err := stateMgr.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } return 0 } diff --git a/command/state_push_test.go b/command/state_push_test.go index e7fd5f446..437da55d7 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -95,7 +95,7 @@ func TestStatePush_replaceMatchStdin(t *testing.T) { }, } - args := []string{"-"} + args := []string{"-force", "-"} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -155,7 +155,7 @@ func TestStatePush_serialNewer(t *testing.T) { args := []string{"replace.tfstate"} if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + t.Fatalf("bad: %d", code) } actual := testStateRead(t, "local-state.tfstate") diff --git a/command/test-fixtures/state-pull-backend/.terraform/terraform.tfstate b/command/test-fixtures/state-pull-backend/.terraform/terraform.tfstate new file mode 100644 index 000000000..122adb812 --- /dev/null +++ b/command/test-fixtures/state-pull-backend/.terraform/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate", + "workspace_dir": null + }, + "hash": 4282859327 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-pull-backend/local-state.tfstate b/command/test-fixtures/state-pull-backend/local-state.tfstate new file mode 100644 index 000000000..db3d0b7c7 --- /dev/null +++ b/command/test-fixtures/state-pull-backend/local-state.tfstate @@ -0,0 +1,24 @@ +{ + "version": 4, + "terraform_version": "0.12.0", + "serial": 7, + "lineage": "configuredUnchanged", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "a", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8521602373864259745", + "triggers": null + } + } + ] + } + ] +} diff --git a/command/test-fixtures/state-pull-backend/main.tf b/command/test-fixtures/state-pull-backend/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-pull-backend/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-good/.terraform/terraform.tfstate b/command/test-fixtures/state-push-good/.terraform/terraform.tfstate index 073bd7a82..122adb812 100644 --- a/command/test-fixtures/state-push-good/.terraform/terraform.tfstate +++ b/command/test-fixtures/state-push-good/.terraform/terraform.tfstate @@ -5,9 +5,10 @@ "backend": { "type": "local", "config": { - "path": "local-state.tfstate" + "path": "local-state.tfstate", + "workspace_dir": null }, - "hash": 9073424445967744180 + "hash": 4282859327 }, "modules": [ { diff --git a/command/test-fixtures/state-push-good/replace.tfstate b/command/test-fixtures/state-push-good/replace.tfstate index 48be87380..9921bc076 100644 --- a/command/test-fixtures/state-push-good/replace.tfstate +++ b/command/test-fixtures/state-push-good/replace.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 0, - "lineage": "hello" + "version": 4, + "serial": 0, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "b", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "9051675049789185374", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate b/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate index 073bd7a82..122adb812 100644 --- a/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate +++ b/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate @@ -5,9 +5,10 @@ "backend": { "type": "local", "config": { - "path": "local-state.tfstate" + "path": "local-state.tfstate", + "workspace_dir": null }, - "hash": 9073424445967744180 + "hash": 4282859327 }, "modules": [ { diff --git a/command/test-fixtures/state-push-replace-match/local-state.tfstate b/command/test-fixtures/state-push-replace-match/local-state.tfstate index 8dd356bc9..b4cf81ff1 100644 --- a/command/test-fixtures/state-push-replace-match/local-state.tfstate +++ b/command/test-fixtures/state-push-replace-match/local-state.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 1, - "lineage": "hello" + "version": 4, + "serial": 1, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "a", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8521602373864259745", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-replace-match/replace.tfstate b/command/test-fixtures/state-push-replace-match/replace.tfstate index 0e3b7013a..a5789c5fe 100644 --- a/command/test-fixtures/state-push-replace-match/replace.tfstate +++ b/command/test-fixtures/state-push-replace-match/replace.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 2, - "lineage": "hello" + "version": 4, + "serial": 2, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "b", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "9051675049789185374", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate b/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate index 073bd7a82..122adb812 100644 --- a/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate +++ b/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate @@ -5,9 +5,10 @@ "backend": { "type": "local", "config": { - "path": "local-state.tfstate" + "path": "local-state.tfstate", + "workspace_dir": null }, - "hash": 9073424445967744180 + "hash": 4282859327 }, "modules": [ { diff --git a/command/test-fixtures/state-push-serial-newer/local-state.tfstate b/command/test-fixtures/state-push-serial-newer/local-state.tfstate index c114b190d..5d4c977bb 100644 --- a/command/test-fixtures/state-push-serial-newer/local-state.tfstate +++ b/command/test-fixtures/state-push-serial-newer/local-state.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 3, - "lineage": "hello" + "version": 4, + "serial": 3, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "a", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8521602373864259745", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-serial-newer/replace.tfstate b/command/test-fixtures/state-push-serial-newer/replace.tfstate index 0e3b7013a..a5789c5fe 100644 --- a/command/test-fixtures/state-push-serial-newer/replace.tfstate +++ b/command/test-fixtures/state-push-serial-newer/replace.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 2, - "lineage": "hello" + "version": 4, + "serial": 2, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "b", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "9051675049789185374", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate b/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate index 073bd7a82..122adb812 100644 --- a/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate +++ b/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate @@ -5,9 +5,10 @@ "backend": { "type": "local", "config": { - "path": "local-state.tfstate" + "path": "local-state.tfstate", + "workspace_dir": null }, - "hash": 9073424445967744180 + "hash": 4282859327 }, "modules": [ { diff --git a/command/test-fixtures/state-push-serial-older/local-state.tfstate b/command/test-fixtures/state-push-serial-older/local-state.tfstate index 8dd356bc9..b4cf81ff1 100644 --- a/command/test-fixtures/state-push-serial-older/local-state.tfstate +++ b/command/test-fixtures/state-push-serial-older/local-state.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 1, - "lineage": "hello" + "version": 4, + "serial": 1, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "a", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8521602373864259745", + "triggers": null + } + } + ] + } + ] } diff --git a/command/test-fixtures/state-push-serial-older/replace.tfstate b/command/test-fixtures/state-push-serial-older/replace.tfstate index 0e3b7013a..a5789c5fe 100644 --- a/command/test-fixtures/state-push-serial-older/replace.tfstate +++ b/command/test-fixtures/state-push-serial-older/replace.tfstate @@ -1,5 +1,23 @@ { - "version": 3, - "serial": 2, - "lineage": "hello" + "version": 4, + "serial": 2, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "b", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "9051675049789185374", + "triggers": null + } + } + ] + } + ] } diff --git a/states/statemgr/helper.go b/states/statemgr/helper.go index 72486aad6..5feb09e94 100644 --- a/states/statemgr/helper.go +++ b/states/statemgr/helper.go @@ -5,8 +5,38 @@ package statemgr import ( "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/version" ) +// NewStateFile creates a new statefile.File object, with a newly-minted +// lineage identifier and serial 0, and returns a pointer to it. +func NewStateFile() *statefile.File { + return &statefile.File{ + Lineage: NewLineage(), + TerraformVersion: version.SemVer, + } +} + +// StateFile is a special helper to obtain a statefile representation +// of a state snapshot that can be written later by a call +func StateFile(mgr Storage, state *states.State) *statefile.File { + ret := &statefile.File{ + State: state.DeepCopy(), + TerraformVersion: version.SemVer, + } + + // If the given manager uses snapshot metadata then we'll save that + // in our file so we can check it again during WritePlannedStateUpdate. + if mr, ok := mgr.(PersistentMeta); ok { + m := mr.StateSnapshotMeta() + ret.Lineage = m.Lineage + ret.Serial = m.Serial + } + + return ret +} + // RefreshAndRead refreshes the persistent snapshot in the given state manager // and then returns it. // diff --git a/states/statemgr/interfaces.go b/states/statemgr/interfaces.go deleted file mode 100644 index 37f621d2d..000000000 --- a/states/statemgr/interfaces.go +++ /dev/null @@ -1 +0,0 @@ -package statemgr diff --git a/states/statemgr/lineage.go b/states/statemgr/lineage.go index 73010081a..b06b12c23 100644 --- a/states/statemgr/lineage.go +++ b/states/statemgr/lineage.go @@ -4,9 +4,6 @@ import ( "fmt" uuid "github.com/hashicorp/go-uuid" - - "github.com/hashicorp/terraform/states/statefile" - "github.com/hashicorp/terraform/version" ) // NewLineage generates a new lineage identifier string. A lineage identifier @@ -21,12 +18,3 @@ func NewLineage() string { } return lineage } - -// NewStateFile creates a new statefile.File object, with a newly-minted -// lineage identifier and serial 0, and returns a pointer to it. -func NewStateFile() *statefile.File { - return &statefile.File{ - Lineage: NewLineage(), - TerraformVersion: version.SemVer, - } -}