Merge pull request #15652 from hashicorp/jbardin/state-command

state commands with remote state backends
This commit is contained in:
James Bardin 2017-08-03 13:29:45 -04:00 committed by GitHub
commit eadda50f02
9 changed files with 406 additions and 104 deletions

View File

@ -11,30 +11,32 @@ import (
)
// StateMeta is the meta struct that should be embedded in state subcommands.
type StateMeta struct{}
type StateMeta struct {
Meta
}
// State returns the state for this meta. This gets the appropriate state from
// the backend, but changes the way that backups are done. This configures
// backups to be timestamped rather than just the original state path plus a
// backup path.
func (c *StateMeta) State(m *Meta) (state.State, error) {
func (c *StateMeta) State() (state.State, error) {
var realState state.State
backupPath := m.backupPath
stateOutPath := m.statePath
backupPath := c.backupPath
stateOutPath := c.statePath
// use the specified state
if m.statePath != "" {
if c.statePath != "" {
realState = &state.LocalState{
Path: m.statePath,
Path: c.statePath,
}
} else {
// Load the backend
b, err := m.Backend(nil)
b, err := c.Backend(nil)
if err != nil {
return nil, err
}
env := m.Workspace()
env := c.Workspace()
// Get the state
s, err := b.State(env)
if err != nil {
@ -42,7 +44,7 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
}
// Get a local backend
localRaw, err := m.Backend(&BackendOpts{ForceLocal: true})
localRaw, err := c.Backend(&BackendOpts{ForceLocal: true})
if err != nil {
// This should never fail
panic(err)

View File

@ -10,7 +10,6 @@ import (
// StateMvCommand is a Command implementation that shows a single resource.
type StateMvCommand struct {
Meta
StateMeta
}
@ -21,12 +20,13 @@ func (c *StateMvCommand) Run(args []string) int {
}
// We create two metas to track the two states
var meta1, meta2 Meta
var backupPathOut, statePathOut string
cmdFlags := c.Meta.flagSet("state mv")
cmdFlags.StringVar(&meta1.backupPath, "backup", "-", "backup")
cmdFlags.StringVar(&meta1.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&meta2.backupPath, "backup-out", "-", "backup")
cmdFlags.StringVar(&meta2.statePath, "state-out", "", "path")
cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
cmdFlags.StringVar(&c.statePath, "state", "", "path")
cmdFlags.StringVar(&backupPathOut, "backup-out", "-", "backup")
cmdFlags.StringVar(&statePathOut, "state-out", "", "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
@ -36,16 +36,11 @@ func (c *StateMvCommand) Run(args []string) int {
return cli.RunResultHelp
}
// Copy the `-state` flag for output if we weren't given a custom one
if meta2.statePath == "" {
meta2.statePath = meta1.statePath
}
// Read the from state
stateFrom, err := c.StateMeta.State(&meta1)
stateFrom, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
return 1
}
if err := stateFrom.RefreshState(); err != nil {
@ -62,11 +57,14 @@ func (c *StateMvCommand) Run(args []string) int {
// Read the destination state
stateTo := stateFrom
stateToReal := stateFromReal
if meta2.statePath != meta1.statePath {
stateTo, err = c.StateMeta.State(&meta2)
if statePathOut != "" {
c.statePath = statePathOut
c.backupPath = backupPathOut
stateTo, err = c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
return 1
}
if err := stateTo.RefreshState(); err != nil {
@ -185,28 +183,30 @@ func (c *StateMvCommand) addableResult(results []*terraform.StateFilterResult) i
func (c *StateMvCommand) Help() string {
helpText := `
Usage: terraform state mv [options] ADDRESS ADDRESS
Usage: terraform state mv [options] SOURCE DESTINATION
Move an item in the state to another location or to a completely different
state file.
This command will move an item matched by the address given to the
destination address. This command can also move to a destination address
in a completely different state file.
This command is useful for module refactors (moving items into a module),
configuration refactors (moving items to a completely different or new
state file), or generally renaming of resources.
This can be used for simple resource renaming, moving items to and from
a module, moving entire modules, and more. And because this command can also
move data to a completely new state, it can also be used for refactoring
one configuration into multiple separately managed Terraform configurations.
This command creates a timestamped backup of the state on every invocation.
This can't be disabled. Due to the destructive nature of this command,
the backup is ensured by Terraform for safety reasons.
This command will output a backup copy of the state prior to saving any
changes. The backup cannot be disabled. Due to the destructive nature
of this command, backups are required.
If you're moving from one state file to a different state file, a backup
will be created for each state file.
If you're moving an item to a different state file, a backup will be created
for each state file.
Options:
-backup=PATH Path where Terraform should write the backup for the original
state. This can't be disabled. If not set, Terraform
will write it to the same path as the statefile with
a backup extension.
a ".backup" extension.
-backup-out=PATH Path where Terraform should write the backup for the destination
state. This can't be disabled. If not set, Terraform
@ -215,13 +215,12 @@ Options:
to be specified if -state-out is set to a different path
than -state.
-state=PATH Path to a Terraform state file to use to look
up Terraform-managed resources. By default it will
use the state "terraform.tfstate" if it exists.
-state=PATH Path to the source state file. Defaults to the configured
backend, or "terraform.tfstate"
-state-out=PATH Path to the destination state file to move the item
to. This defaults to the same statefile. This will
overwrite the destination state file.
-state-out=PATH Path to the destination state file to write to. If this
isn't specified, the source state file will be used. This
can be a new or existing path.
`
return strings.TrimSpace(helpText)

View File

@ -47,9 +47,11 @@ func TestStateMv(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -133,9 +135,11 @@ func TestStateMv_explicitWithBackend(t *testing.T) {
p := testProvider()
ui = new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -194,9 +198,11 @@ func TestStateMv_backupExplicit(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -244,9 +250,11 @@ func TestStateMv_stateOutNew(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -316,9 +324,11 @@ func TestStateMv_stateOutExisting(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -357,9 +367,11 @@ func TestStateMv_noState(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -418,9 +430,11 @@ func TestStateMv_stateOutNew_count(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -596,9 +610,11 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -677,9 +693,11 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -705,6 +723,160 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) {
testStateOutput(t, backups[0], testStateMvNestedModule_stateOutOriginal)
}
func TestStateMv_withinBackend(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-unchanged"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
state := &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",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
"test_instance.baz": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "foo",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
// the local backend state file is "foo"
statePath := "local-state.tfstate"
backupPath := "local-state.backup"
f, err := os.Create(statePath)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := terraform.WriteState(state, f); err != nil {
t.Fatal(err)
}
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
args := []string{
"-backup", backupPath,
"test_instance.foo",
"test_instance.bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
testStateOutput(t, statePath, testStateMvOutput)
testStateOutput(t, backupPath, testStateMvOutputOriginal)
}
func TestStateMv_fromBackendToLocal(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-unchanged"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
state := &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",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
"test_instance.baz": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "foo",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
// the local backend state file is "foo"
statePath := "local-state.tfstate"
// real "local" state file
statePathOut := "real-local.tfstate"
f, err := os.Create(statePath)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := terraform.WriteState(state, f); err != nil {
t.Fatal(err)
}
p := testProvider()
ui := new(cli.MockUi)
c := &StateMvCommand{
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
args := []string{
"-state-out", statePathOut,
"test_instance.foo",
"test_instance.bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
testStateOutput(t, statePathOut, testStateMvCount_stateOutSrc)
// the backend state should be left with only baz
testStateOutput(t, statePath, testStateMvOriginal_backend)
}
const testStateMvOutputOriginal = `
test_instance.baz:
ID = foo
@ -943,3 +1115,10 @@ const testStateMvExisting_stateDstOriginal = `
test_instance.qux:
ID = bar
`
const testStateMvOriginal_backend = `
test_instance.baz:
ID = foo
bar = value
foo = value
`

View File

@ -9,7 +9,6 @@ import (
// StateRmCommand is a Command implementation that shows a single resource.
type StateRmCommand struct {
Meta
StateMeta
}
@ -20,8 +19,8 @@ func (c *StateRmCommand) Run(args []string) int {
}
cmdFlags := c.Meta.flagSet("state show")
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "-", "backup")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
cmdFlags.StringVar(&c.statePath, "state", "", "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
@ -32,10 +31,10 @@ func (c *StateRmCommand) Run(args []string) int {
return 1
}
state, err := c.StateMeta.State(&c.Meta)
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -6,6 +6,7 @@ import (
"strings"
"testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -47,9 +48,11 @@ func TestStateRm(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -109,9 +112,11 @@ func TestStateRmNoArgs(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -169,9 +174,11 @@ func TestStateRm_backupExplicit(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -198,9 +205,11 @@ func TestStateRm_noState(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
@ -210,6 +219,110 @@ func TestStateRm_noState(t *testing.T) {
}
}
func TestStateRm_needsInit(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-change"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
args := []string{"foo"}
if code := c.Run(args); code == 0 {
t.Fatal("expected error\noutput:", ui.OutputWriter)
}
if !strings.Contains(ui.ErrorWriter.String(), "Initialization") {
t.Fatal("expected initialization error, got:\n", ui.ErrorWriter)
}
}
func TestStateRm_backendState(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-unchanged"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
state := &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",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
"test_instance.bar": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "foo",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
// the local backend state file is "foo"
statePath := "local-state.tfstate"
backupPath := "local-state.backup"
f, err := os.Create(statePath)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := terraform.WriteState(state, f); err != nil {
t.Fatal(err)
}
p := testProvider()
ui := new(cli.MockUi)
c := &StateRmCommand{
StateMeta{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
},
}
args := []string{
"-backup", backupPath,
"test_instance.foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test it is correct
testStateOutput(t, statePath, testStateRmOutput)
// Test backup
testStateOutput(t, backupPath, testStateRmOutputOriginal)
}
const testStateRmOutputOriginal = `
test_instance.bar:
ID = foo

View File

@ -28,7 +28,7 @@ func TestStateDefaultBackupExtension(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
s, err := (&StateMeta{}).State(&Meta{})
s, err := (&StateMeta{}).State()
if err != nil {
t.Fatal(err)
}

View File

@ -276,13 +276,17 @@ func init() {
"state rm": func() (cli.Command, error) {
return &command.StateRmCommand{
Meta: meta,
StateMeta: command.StateMeta{
Meta: meta,
},
}, nil
},
"state mv": func() (cli.Command, error) {
return &command.StateMvCommand{
Meta: meta,
StateMeta: command.StateMeta{
Meta: meta,
},
}, nil
},

View File

@ -40,19 +40,22 @@ in [resource addressing format](/docs/commands/state/addressing.html).
The command-line flags are all optional. The list of available flags are:
* `-backup=path` - Path to a backup file Defaults to the state path plus
a timestamp with the ".backup" extension.
* `-backup=path` - Path where Terraform should write the backup for the
original state. This can't be disabled. If not set, Terraform will write it
to the same path as the statefile with a ".backup" extension.
* `-backup-out=path` - Path to the backup file for the output state.
This is only necessary if `-state-out` is specified.
* `-backup-out=path` - Path where Terraform should write the backup for the
destination state. This can't be disabled. If not set, Terraform will write
it to the same path as the destination state file with a backup extension.
This only needs to be specified if -state-out is set to a different path than
-state.
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
Ignored when [remote state](/docs/state/remote.html) is used.
* `-state=path` - Path to the source state file to read from. Defaults to the
configured backend, or "terraform.tfstate".
* `-state-out=path` - Path to the state file to write to. If this isn't specified
the state specified by `-state` will be used. This can be
a new or existing path. Ignored when
[remote state](/docs/state/remote.html) is used.
* `-state-out=path` - Path to the destination state file to write to. If this
isn't specified the source state file will be used. This can be a new or
existing path.
## Example: Rename a Resource

View File

@ -17,7 +17,7 @@ and more.
Usage: `terraform state rm [options] ADDRESS...`
The command will remove all the items matched by the addresses given.
Remove one or more items from the Terraform state.
Items removed from the Terraform state are _not physically destroyed_.
Items removed from the Terraform state are only no longer managed by
@ -43,10 +43,13 @@ in [resource addressing format](/docs/commands/state/addressing.html).
The command-line flags are all optional. The list of available flags are:
* `-backup=path` - Path to a backup file Defaults to the state path plus
a timestamp with the ".backup" extension.
* `-backup=path` - Path where Terraform should write the backup state. This
can't be disabled. If not set, Terraform will write it to the same path as
the statefile with a backup extension.
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
* `-state=path` - Path to a Terraform state file to use to look up
Terraform-managed resources. By default it will use the configured backend,
or the default "terraform.tfstate" if it exists.
## Example: Remove a Resource