diff --git a/command/state_push.go b/command/state_push.go index d23f8b7e3..95f2b6ed7 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "io" "os" "strings" @@ -31,14 +32,28 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } - // Read the state - f, err := os.Open(args[0]) - if err != nil { - c.Ui.Error(err.Error()) - 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() } - sourceState, err := terraform.ReadState(f) - f.Close() if err != nil { c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) return 1 @@ -109,6 +124,10 @@ Usage: terraform state push [options] PATH This command works with local state (it will overwrite the local state), but is less useful for this use case. + If PATH is "-", then this command will read the state to push from stdin. + Data from stdin is not streamed to the backend: it is loaded completely + (until pipe close), verified, and then pushed. + Options: -force Write the state even if lineages don't match or the diff --git a/command/state_push_test.go b/command/state_push_test.go index d63b193f6..18e821769 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -1,10 +1,12 @@ package command import ( + "bytes" "os" "testing" "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -66,6 +68,42 @@ func TestStatePush_replaceMatch(t *testing.T) { } } +func TestStatePush_replaceMatchStdin(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-replace-match"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "replace.tfstate") + + // Setup the replacement to come from stdin + var buf bytes.Buffer + if err := terraform.WriteState(expected, &buf); err != nil { + t.Fatalf("err: %s") + } + defer testStdinPipe(t, &buf)() + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"-"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestStatePush_lineageMismatch(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) diff --git a/website/source/docs/commands/state/push.html.md b/website/source/docs/commands/state/push.html.md index c2723c2c3..4db414405 100644 --- a/website/source/docs/commands/state/push.html.md +++ b/website/source/docs/commands/state/push.html.md @@ -22,6 +22,10 @@ Usage: `terraform state push [options] PATH` This command will push the state specified by PATH to the currently configured [backend](/docs/backends). +If PATH is "-" then the state data to push is read from stdin. This data +is loaded completely into memory and verified prior to being written to +the destination state. + Terraform will perform a number of safety checks to prevent you from making changes that appear to be unsafe: