From 22087181af296b118d34f61f2d015b927d0627c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2015 14:55:15 -0800 Subject: [PATCH] command/push: archive, upload --- command/command_test.go | 21 ++++++ command/push.go | 52 ++++++++++++- command/push_test.go | 116 +++++++++++++++++++++++++++++ command/test-fixtures/push/main.tf | 1 + terraform/context.go | 3 - 5 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 command/test-fixtures/push/main.tf diff --git a/command/command_test.go b/command/command_test.go index 303fc4b2b..2544cf531 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -148,6 +148,27 @@ func testStateFileDefault(t *testing.T, s *terraform.State) string { return DefaultStateFilename } +// testStateFileRemote writes the state out to the remote statefile +// in the cwd. Use `testCwd` to change into a temp cwd. +func testStateFileRemote(t *testing.T, s *terraform.State) string { + path := filepath.Join(DefaultDataDir, DefaultStateFilename) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Create(path) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + if err := terraform.WriteState(s, f); err != nil { + t.Fatalf("err: %s", err) + } + + return path +} + // testStateOutput tests that the state at the given path contains // the expected state string. func testStateOutput(t *testing.T, path string, expected string) { diff --git a/command/push.go b/command/push.go index cee61b7f6..98ad92d65 100644 --- a/command/push.go +++ b/command/push.go @@ -3,6 +3,7 @@ package command import ( "flag" "fmt" + "io" "os" "path/filepath" "strings" @@ -12,6 +13,11 @@ import ( type PushCommand struct { Meta + + // client is the client to use for the actual push operations. + // If this isn't set, then the Atlas client is used. This should + // really only be set for testing reasons (and is hence not exported). + client pushClient } func (c *PushCommand) Run(args []string) int { @@ -64,7 +70,7 @@ func (c *PushCommand) Run(args []string) int { } // Build the context based on the arguments given - _, planned, err := c.Context(contextOpts{ + ctx, planned, err := c.Context(contextOpts{ Path: configPath, StatePath: c.Meta.statePath, }) @@ -79,6 +85,13 @@ func (c *PushCommand) Run(args []string) int { return 1 } + // Ask for input + if err := ctx.Input(c.InputMode()); err != nil { + c.Ui.Error(fmt.Sprintf( + "Error while asking for variable input:\n\n%s", err)) + return 1 + } + // Build the archiving options, which includes everything it can // by default according to VCS rules but forcing the data directory. archiveOpts := &archive.ArchiveOpts{ @@ -92,7 +105,7 @@ func (c *PushCommand) Run(args []string) int { filepath.Join(c.DataDir(), "modules")) } - _, err = archive.CreateArchive(configPath, archiveOpts) + archiveR, err := archive.CreateArchive(configPath, archiveOpts) if err != nil { c.Ui.Error(fmt.Sprintf( "An error has occurred while archiving the module for uploading:\n"+ @@ -100,6 +113,13 @@ func (c *PushCommand) Run(args []string) int { return 1 } + // Upsert! + if err := c.client.Upsert(archiveR, archiveR.Size); err != nil { + c.Ui.Error(fmt.Sprintf( + "An error occurred while uploading the module:\n\n%s", err)) + return 1 + } + return 0 } @@ -126,3 +146,31 @@ Options: func (c *PushCommand) Synopsis() string { return "Upload this Terraform module to Atlas to run" } + +// pushClient is implementd internally to control where pushes go. This is +// either to Atlas or a mock for testing. +type pushClient interface { + Upsert(io.Reader, int64) error +} + +type mockPushClient struct { + File string + + UpsertCalled bool + UpsertError error +} + +func (c *mockPushClient) Upsert(data io.Reader, size int64) error { + f, err := os.Create(c.File) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.CopyN(f, data, size); err != nil { + return err + } + + c.UpsertCalled = true + return c.UpsertError +} diff --git a/command/push_test.go b/command/push_test.go index 8b52421b8..0cbe4cf23 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -1,12 +1,61 @@ package command import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "reflect" + "sort" "testing" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) +func TestPush_good(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create remote state file, this should be pulled + conf, srv := testRemoteState(t, testState(), 200) + defer srv.Close() + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + s.Remote = conf + testStateFileRemote(t, s) + + // Path where the archive will be "uploaded" to + archivePath := testTempFile(t) + defer os.Remove(archivePath) + + client := &mockPushClient{File: archivePath} + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + + client: client, + } + + args := []string{ + testFixturePath("push"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testArchiveStr(t, archivePath) + expected := []string{} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestPush_noState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) @@ -57,3 +106,70 @@ func TestPush_noRemoteState(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } + +func TestPush_plan(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create remote state file, this should be pulled + conf, srv := testRemoteState(t, testState(), 200) + defer srv.Close() + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + s.Remote = conf + testStateFileRemote(t, s) + + // Create a plan + planPath := testPlanFile(t, &terraform.Plan{ + Module: testModule(t, "apply"), + }) + + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{planPath} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +func testArchiveStr(t *testing.T, path string) []string { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + // Ungzip + gzipR, err := gzip.NewReader(f) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Accumulator + result := make([]string, 0, 10) + + // Untar + tarR := tar.NewReader(gzipR) + for { + header, err := tarR.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("err: %s", err) + } + + result = append(result, header.Name) + } + + sort.Strings(result) + return result +} diff --git a/command/test-fixtures/push/main.tf b/command/test-fixtures/push/main.tf new file mode 100644 index 000000000..919f140bb --- /dev/null +++ b/command/test-fixtures/push/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" {} diff --git a/terraform/context.go b/terraform/context.go index 62a06f431..fd59bd671 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -376,8 +376,6 @@ func (c *Context) Validate() ([]string, []error) { return walker.ValidationWarnings, rerrs.Errors } -<<<<<<< Updated upstream -======= // Variables will return the mapping of variables that were defined // for this Context. If Input was called, this mapping may be different // than what was given. @@ -390,7 +388,6 @@ func (c *Context) SetVariable(k, v string) { c.variables[k] = v } ->>>>>>> Stashed changes func (c *Context) acquireRun() chan<- struct{} { c.l.Lock() defer c.l.Unlock()