From 38002904f4b16c8fd604833429c53ee8660b8a9d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 8 Oct 2014 15:15:14 -0700 Subject: [PATCH] command/push: Adding the push command --- command/init_test.go | 2 +- command/pull_test.go | 10 +++-- command/push.go | 98 ++++++++++++++++++++++++++++++++++++++++++++ command/push_test.go | 83 +++++++++++++++++++++++++++++++++++++ commands.go | 6 +++ 5 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 command/push.go create mode 100644 command/push_test.go diff --git a/command/init_test.go b/command/init_test.go index 98495930b..b99bc0493 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -150,7 +150,7 @@ func TestInit_remoteState(t *testing.T) { defer fixDir(tmp, cwd) s := terraform.NewState() - conf, srv := testRemoteState(t, s) + conf, srv := testRemoteState(t, s, 200) defer srv.Close() ui := new(cli.MockUi) diff --git a/command/pull_test.go b/command/pull_test.go index 7a171e9b4..7b8a555b8 100644 --- a/command/pull_test.go +++ b/command/pull_test.go @@ -38,7 +38,7 @@ func TestPull_cliRemote(t *testing.T) { defer fixDir(tmp, cwd) s := terraform.NewState() - conf, srv := testRemoteState(t, s) + conf, srv := testRemoteState(t, s, 200) defer srv.Close() ui := new(cli.MockUi) @@ -67,7 +67,7 @@ func TestPull_local(t *testing.T) { s := terraform.NewState() s.Serial = 10 - conf, srv := testRemoteState(t, s) + conf, srv := testRemoteState(t, s, 200) s = terraform.NewState() s.Serial = 5 @@ -95,7 +95,7 @@ func TestPull_local(t *testing.T) { // testRemoteState is used to make a test HTTP server to // return a given state file -func testRemoteState(t *testing.T, s *terraform.State) (*terraform.RemoteState, *httptest.Server) { +func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { var b64md5 string buf := bytes.NewBuffer(nil) @@ -109,6 +109,10 @@ func testRemoteState(t *testing.T, s *terraform.State) (*terraform.RemoteState, } cb := func(resp http.ResponseWriter, req *http.Request) { + if req.Method == "PUT" { + resp.WriteHeader(c) + return + } if s == nil { resp.WriteHeader(404) return diff --git a/command/push.go b/command/push.go new file mode 100644 index 000000000..b186576c2 --- /dev/null +++ b/command/push.go @@ -0,0 +1,98 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" +) + +type PushCommand struct { + Meta +} + +func (c *PushCommand) Run(args []string) int { + var force bool + var remoteConf terraform.RemoteState + args = c.Meta.process(args, false) + cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError) + cmdFlags.StringVar(&remoteConf.Name, "remote", "", "") + cmdFlags.StringVar(&remoteConf.Server, "remote-server", "", "") + cmdFlags.StringVar(&remoteConf.AuthToken, "remote-auth", "", "") + cmdFlags.BoolVar(&force, "force", false, "") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Validate the remote configuration if given + var conf *terraform.RemoteState + if !remoteConf.Empty() { + if err := remote.ValidateConfig(&remoteConf); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + conf = &remoteConf + } else { + // Recover the local state if any + local, _, err := remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + if local == nil || local.Remote == nil { + c.Ui.Error("No remote state server configured") + return 1 + } + conf = local.Remote + } + + // Attempt to push the state + change, err := remote.PushState(conf, force) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Failed to push state: %v", err)) + return 1 + } + + // Use an error exit code if the update was not a success + if !change.SuccessfulPush() { + c.Ui.Error(fmt.Sprintf("%s", change)) + return 1 + } else { + c.Ui.Output(fmt.Sprintf("%s", change)) + } + return 0 +} + +func (c *PushCommand) Help() string { + helpText := ` +Usage: terraform push [options] + + Uploads the local state file to the remote server. This is done automatically + by commands when remote state if configured, but can also be done manually + using this command. + +Options: + + -force Forces the upload of the local state, ignoring any + conflicts. This should be used carefully, as force pushing + can cause remote state information to be lost. + + -remote=name Name of the state file in the state storage server. + Optional, default does not use remote storage. + + -remote-auth=token Authentication token for state storage server. + Optional, defaults to blank. + + -remote-server=url URL of the remote storage server. + +` + return strings.TrimSpace(helpText) +} + +func (c *PushCommand) Synopsis() string { + return "Uploads the the local state to the remote server" +} diff --git a/command/push_test.go b/command/push_test.go new file mode 100644 index 000000000..f603f8f79 --- /dev/null +++ b/command/push_test.go @@ -0,0 +1,83 @@ +package command + +import ( + "bytes" + "testing" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestPush_noRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestPush_cliRemote_noState(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + s := terraform.NewState() + conf, srv := testRemoteState(t, s, 200) + defer srv.Close() + + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + // Remote with no local state! + args := []string{"-remote", conf.Name, "-remote-server", conf.Server} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestPush_local(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + s := terraform.NewState() + s.Serial = 5 + conf, srv := testRemoteState(t, s, 200) + + s = terraform.NewState() + s.Serial = 10 + s.Remote = conf + defer srv.Close() + + // Store the local state + buf := bytes.NewBuffer(nil) + terraform.WriteState(s, buf) + remote.EnsureDirectory() + remote.Persist(buf) + + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} diff --git a/commands.go b/commands.go index f3d465e70..00f8a8a97 100644 --- a/commands.go +++ b/commands.go @@ -84,6 +84,12 @@ func init() { }, nil }, + "push": func() (cli.Command, error) { + return &command.PushCommand{ + Meta: meta, + }, nil + }, + "refresh": func() (cli.Command, error) { return &command.RefreshCommand{ Meta: meta,