command/pull: Adding the pull command

This commit is contained in:
Armon Dadgar 2014-10-08 12:08:35 -07:00 committed by Mitchell Hashimoto
parent 7febe57ae6
commit 34df217514
4 changed files with 237 additions and 0 deletions

View File

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

92
command/pull.go Normal file
View File

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

118
command/pull_test.go Normal file
View File

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

View File

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