From 34df2175149cd27eedf4757e0322c1f9c87aaf8b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 8 Oct 2014 12:08:35 -0700 Subject: [PATCH] command/pull: Adding the pull command --- command/command_test.go | 21 +++++++ command/pull.go | 92 +++++++++++++++++++++++++++++++ command/pull_test.go | 118 ++++++++++++++++++++++++++++++++++++++++ commands.go | 6 ++ 4 files changed, 237 insertions(+) create mode 100644 command/pull.go create mode 100644 command/pull_test.go diff --git a/command/command_test.go b/command/command_test.go index d3090240e..295dc1579 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -174,3 +174,24 @@ func testTempDir(t *testing.T) string { return d } + +// testCwdDir is used to change the current working directory +// into a test directory that should be remoted after +func testCwd(t *testing.T) (string, string) { + tmp, err := ioutil.TempDir("", "remote") + if err != nil { + t.Fatalf("err: %v", err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %v", err) + } + os.Chdir(tmp) + return tmp, cwd +} + +// fixDir is used to as a defer to testDir +func fixDir(tmp, cwd string) { + os.Chdir(cwd) + os.RemoveAll(tmp) +} diff --git a/command/pull.go b/command/pull.go new file mode 100644 index 000000000..07268d6b1 --- /dev/null +++ b/command/pull.go @@ -0,0 +1,92 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" +) + +type PullCommand struct { + Meta +} + +func (c *PullCommand) Run(args []string) int { + var remoteConf terraform.RemoteState + args = c.Meta.process(args, false) + cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError) + cmdFlags.StringVar(&remoteConf.Name, "remote", "", "") + cmdFlags.StringVar(&remoteConf.Server, "remote-server", "", "") + cmdFlags.StringVar(&remoteConf.AuthToken, "remote-auth", "", "") + 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 the state refresh + change, err := remote.RefreshState(conf) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Failed to refresh from remote state: %v", err)) + return 1 + } + + // Use an error exit code if the update was not a success + if !change.SuccessfulPull() { + c.Ui.Error(fmt.Sprintf("%s", change)) + return 1 + } else { + c.Ui.Output(fmt.Sprintf("%s", change)) + } + return 0 +} + +func (c *PullCommand) Help() string { + helpText := ` +Usage: terraform pull [options] + + Refreshes the cached state file from the remote server. It can also + be used to perform the initial clone of the state file and setup the + remote server configuration to use remote state storage. + +Options: + + -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 *PullCommand) Synopsis() string { + return "Refreshes the local state copy from the remote server" +} diff --git a/command/pull_test.go b/command/pull_test.go new file mode 100644 index 000000000..d5f47dd65 --- /dev/null +++ b/command/pull_test.go @@ -0,0 +1,118 @@ +package command + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestPull_noRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + ui := new(cli.MockUi) + c := &PullCommand{ + 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 TestPull_cliRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + s := terraform.NewState() + remote, srv := testRemoteState(t, s) + defer srv.Close() + + ui := new(cli.MockUi) + c := &PullCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"-remote", remote.Name, "-remote-server", remote.Server} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestPull_localRemote(t *testing.T) { + tmp, cwd := testCwd(t) + defer fixDir(tmp, cwd) + + s := terraform.NewState() + s.Serial = 10 + conf, srv := testRemoteState(t, s) + + s = terraform.NewState() + s.Serial = 5 + 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 := &PullCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +// 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) { + var b64md5 string + buf := bytes.NewBuffer(nil) + + if s != nil { + enc := json.NewEncoder(buf) + if err := enc.Encode(s); err != nil { + t.Fatalf("err: %v", err) + } + md5 := md5.Sum(buf.Bytes()) + b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) + } + + cb := func(resp http.ResponseWriter, req *http.Request) { + if s == nil { + resp.WriteHeader(404) + return + } + resp.Header().Set("Content-MD5", b64md5) + resp.Write(buf.Bytes()) + } + srv := httptest.NewServer(http.HandlerFunc(cb)) + remote := &terraform.RemoteState{ + Name: "foo", + Server: srv.URL, + } + return remote, srv +} diff --git a/commands.go b/commands.go index 7c8a06eb2..f3d465e70 100644 --- a/commands.go +++ b/commands.go @@ -78,6 +78,12 @@ func init() { }, nil }, + "pull": func() (cli.Command, error) { + return &command.PullCommand{ + Meta: meta, + }, nil + }, + "refresh": func() (cli.Command, error) { return &command.RefreshCommand{ Meta: meta,