command/state: update and fix the state push and pull

This commit is contained in:
Sander van Harmelen 2018-10-18 21:41:05 +02:00
parent 17b883f592
commit 7fbd93b5cd
21 changed files with 340 additions and 142 deletions

View File

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

View File

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

View File

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

View File

@ -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")

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": {
"type": "local",
"config": {
"path": "local-state.tfstate"
"path": "local-state.tfstate",
"workspace_dir": null
},
"hash": 9073424445967744180
"hash": 4282859327
},
"modules": [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
//

View File

@ -1 +0,0 @@
package statemgr

View File

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