command/push: archive, upload

This commit is contained in:
Mitchell Hashimoto 2015-03-05 14:55:15 -08:00
parent fdded8ca14
commit 22087181af
5 changed files with 188 additions and 5 deletions

View File

@ -148,6 +148,27 @@ func testStateFileDefault(t *testing.T, s *terraform.State) string {
return DefaultStateFilename 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 // testStateOutput tests that the state at the given path contains
// the expected state string. // the expected state string.
func testStateOutput(t *testing.T, path string, expected string) { func testStateOutput(t *testing.T, path string, expected string) {

View File

@ -3,6 +3,7 @@ package command
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -12,6 +13,11 @@ import (
type PushCommand struct { type PushCommand struct {
Meta 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 { 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 // Build the context based on the arguments given
_, planned, err := c.Context(contextOpts{ ctx, planned, err := c.Context(contextOpts{
Path: configPath, Path: configPath,
StatePath: c.Meta.statePath, StatePath: c.Meta.statePath,
}) })
@ -79,6 +85,13 @@ func (c *PushCommand) Run(args []string) int {
return 1 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 // Build the archiving options, which includes everything it can
// by default according to VCS rules but forcing the data directory. // by default according to VCS rules but forcing the data directory.
archiveOpts := &archive.ArchiveOpts{ archiveOpts := &archive.ArchiveOpts{
@ -92,7 +105,7 @@ func (c *PushCommand) Run(args []string) int {
filepath.Join(c.DataDir(), "modules")) filepath.Join(c.DataDir(), "modules"))
} }
_, err = archive.CreateArchive(configPath, archiveOpts) archiveR, err := archive.CreateArchive(configPath, archiveOpts)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"An error has occurred while archiving the module for uploading:\n"+ "An error has occurred while archiving the module for uploading:\n"+
@ -100,6 +113,13 @@ func (c *PushCommand) Run(args []string) int {
return 1 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 return 0
} }
@ -126,3 +146,31 @@ Options:
func (c *PushCommand) Synopsis() string { func (c *PushCommand) Synopsis() string {
return "Upload this Terraform module to Atlas to run" 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
}

View File

@ -1,12 +1,61 @@
package command package command
import ( import (
"archive/tar"
"compress/gzip"
"io"
"os"
"reflect"
"sort"
"testing" "testing"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "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) { func TestPush_noState(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) 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()) 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
}

View File

@ -0,0 +1 @@
resource "aws_instance" "foo" {}

View File

@ -376,8 +376,6 @@ func (c *Context) Validate() ([]string, []error) {
return walker.ValidationWarnings, rerrs.Errors return walker.ValidationWarnings, rerrs.Errors
} }
<<<<<<< Updated upstream
=======
// Variables will return the mapping of variables that were defined // Variables will return the mapping of variables that were defined
// for this Context. If Input was called, this mapping may be different // for this Context. If Input was called, this mapping may be different
// than what was given. // than what was given.
@ -390,7 +388,6 @@ func (c *Context) SetVariable(k, v string) {
c.variables[k] = v c.variables[k] = v
} }
>>>>>>> Stashed changes
func (c *Context) acquireRun() chan<- struct{} { func (c *Context) acquireRun() chan<- struct{} {
c.l.Lock() c.l.Lock()
defer c.l.Unlock() defer c.l.Unlock()