Merge pull request #19130 from hashicorp/f-state-push-pull

command/state: update and fix the state push and pull
This commit is contained in:
Sander van Harmelen 2018-10-19 19:16:00 +02:00 committed by GitHub
commit 5e11de460a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 340 additions and 142 deletions

View File

@ -1,9 +1,12 @@
package command package command
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -34,37 +37,34 @@ func (c *StatePullCommand) Run(args []string) int {
// Get the state // Get the state
env := c.Workspace() env := c.Workspace()
state, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 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)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
s := state.State() state := stateMgr.State()
if s == nil { if state == nil {
// Output on "error" so it shows up on stderr // Output on "error" so it shows up on stderr
c.Ui.Error("Empty state (no state)") c.Ui.Error("Empty state (no state)")
return 0 return 0
} }
c.Ui.Error("state pull not yet updated for new state types") // Get the state file.
return 1 stateFile := statemgr.StateFile(stateMgr, state)
/*
var buf bytes.Buffer var buf bytes.Buffer
if err := terraform.WriteState(s, &buf); err != nil { err = statefile.Write(stateFile, &buf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
c.Ui.Output(buf.String()) c.Ui.Output(buf.String())
*/
return 0 return 0
} }

View File

@ -1,21 +1,26 @@
package command package command
import ( import (
"strings" "bytes"
"io/ioutil"
"os"
"testing" "testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func TestStatePull(t *testing.T) { func TestStatePull(t *testing.T) {
tmp, cwd := testCwd(t) // Create a temporary working directory that is empty
defer testFixCwd(t, tmp, cwd) td := tempDir(t)
copy.CopyDir(testFixturePath("state-pull-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Create some legacy remote state expected, err := ioutil.ReadFile("local-state.tfstate")
legacyState := testState() if err != nil {
backendState, srv := testRemoteState(t, legacyState, 200) t.Fatalf("error reading state: %v", err)
defer srv.Close() }
testStateFileRemote(t, backendState)
p := testProvider() p := testProvider()
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -31,9 +36,8 @@ func TestStatePull(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
} }
expected := "test_instance.foo" actual := ui.OutputWriter.Bytes()
actual := ui.OutputWriter.String() if bytes.Equal(actual, expected) {
if !strings.Contains(actual, expected) {
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
} }
} }

View File

@ -1,8 +1,13 @@
package command package command
import ( import (
"fmt"
"io"
"os"
"strings" "strings"
"github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -31,10 +36,6 @@ func (c *StatePushCommand) Run(args []string) int {
return 1 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 // Determine our reader for the input state. This is the filepath
// or stdin if "-" is given. // or stdin if "-" is given.
var r io.Reader = os.Stdin var r io.Reader = os.Stdin
@ -52,7 +53,7 @@ func (c *StatePushCommand) Run(args []string) int {
} }
// Read the state // Read the state
sourceState, err := terraform.ReadState(r) srcStateFile, err := statefile.Read(r)
if c, ok := r.(io.Closer); ok { if c, ok := r.(io.Closer); ok {
// Close the reader if possible right now since we're done with it. // Close the reader if possible right now since we're done with it.
c.Close() c.Close()
@ -71,46 +72,40 @@ func (c *StatePushCommand) Run(args []string) int {
// Get the state // Get the state
env := c.Workspace() env := c.Workspace()
state, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
return 1 return 1
} }
if err := state.RefreshState(); err != nil { if err := stateMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
return 1 return 1
} }
dstState := stateMgr.State()
dstState := state.State()
// If we're not forcing, then perform safety checks // If we're not forcing, then perform safety checks
if !flagForce && !dstState.Empty() { if !flagForce && !dstState.Empty() {
if !dstState.SameLineage(sourceState) { dstStateFile := statemgr.StateFile(stateMgr, dstState)
if dstStateFile.Lineage != srcStateFile.Lineage {
c.Ui.Error(strings.TrimSpace(errStatePushLineage)) c.Ui.Error(strings.TrimSpace(errStatePushLineage))
return 1 return 1
} }
if dstStateFile.Serial > srcStateFile.Serial {
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)) c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer))
return 1 return 1
} }
} }
// Overwrite it // Overwrite it
if err := state.WriteState(sourceState); err != nil { if err := stateMgr.WriteState(srcStateFile.State); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1 return 1
} }
if err := state.PersistState(); err != nil { if err := stateMgr.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1 return 1
} }
*/
return 0 return 0
} }

View File

@ -95,7 +95,7 @@ func TestStatePush_replaceMatchStdin(t *testing.T) {
}, },
} }
args := []string{"-"} args := []string{"-force", "-"}
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 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"} args := []string{"replace.tfstate"}
if code := c.Run(args); code != 1 { 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") actual := testStateRead(t, "local-state.tfstate")

View File

@ -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": []
}
]
}

View File

@ -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
}
}
]
}
]
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state.tfstate"
}
}

View File

@ -5,9 +5,10 @@
"backend": { "backend": {
"type": "local", "type": "local",
"config": { "config": {
"path": "local-state.tfstate" "path": "local-state.tfstate",
"workspace_dir": null
}, },
"hash": 9073424445967744180 "hash": 4282859327
}, },
"modules": [ "modules": [
{ {

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 0, "serial": 0,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "b",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "9051675049789185374",
"triggers": null
}
}
]
}
]
} }

View File

@ -5,9 +5,10 @@
"backend": { "backend": {
"type": "local", "type": "local",
"config": { "config": {
"path": "local-state.tfstate" "path": "local-state.tfstate",
"workspace_dir": null
}, },
"hash": 9073424445967744180 "hash": 4282859327
}, },
"modules": [ "modules": [
{ {

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 1, "serial": 1,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "a",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "8521602373864259745",
"triggers": null
}
}
]
}
]
} }

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 2, "serial": 2,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "b",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "9051675049789185374",
"triggers": null
}
}
]
}
]
} }

View File

@ -5,9 +5,10 @@
"backend": { "backend": {
"type": "local", "type": "local",
"config": { "config": {
"path": "local-state.tfstate" "path": "local-state.tfstate",
"workspace_dir": null
}, },
"hash": 9073424445967744180 "hash": 4282859327
}, },
"modules": [ "modules": [
{ {

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 3, "serial": 3,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "a",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "8521602373864259745",
"triggers": null
}
}
]
}
]
} }

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 2, "serial": 2,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "b",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "9051675049789185374",
"triggers": null
}
}
]
}
]
} }

View File

@ -5,9 +5,10 @@
"backend": { "backend": {
"type": "local", "type": "local",
"config": { "config": {
"path": "local-state.tfstate" "path": "local-state.tfstate",
"workspace_dir": null
}, },
"hash": 9073424445967744180 "hash": 4282859327
}, },
"modules": [ "modules": [
{ {

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 1, "serial": 1,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "a",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "8521602373864259745",
"triggers": null
}
}
]
}
]
} }

View File

@ -1,5 +1,23 @@
{ {
"version": 3, "version": 4,
"serial": 2, "serial": 2,
"lineage": "hello" "lineage": "hello",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "b",
"provider": "provider.null",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "9051675049789185374",
"triggers": null
}
}
]
}
]
} }

View File

@ -5,8 +5,38 @@ package statemgr
import ( import (
"github.com/hashicorp/terraform/states" "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 // RefreshAndRead refreshes the persistent snapshot in the given state manager
// and then returns it. // and then returns it.
// //

View File

@ -1 +0,0 @@
package statemgr

View File

@ -4,9 +4,6 @@ import (
"fmt" "fmt"
uuid "github.com/hashicorp/go-uuid" 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 // NewLineage generates a new lineage identifier string. A lineage identifier
@ -21,12 +18,3 @@ func NewLineage() string {
} }
return lineage 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,
}
}