From fbc1551048f538a0854090a5004f9e9362c74719 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 28 Oct 2014 19:29:51 -0700 Subject: [PATCH] command/push: partially implemented, tests --- command/command_test.go | 15 +++ command/push.go | 94 +++++++++++++- command/push_test.go | 122 ++++++++++++++++++ .../test-fixtures/push-no-name/template.json | 3 + command/test-fixtures/push/template.json | 7 + packer/template.go | 1 + 6 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 command/test-fixtures/push-no-name/template.json create mode 100644 command/test-fixtures/push/template.json diff --git a/command/command_test.go b/command/command_test.go index 7574d663d..500ea7f9e 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -1,11 +1,26 @@ package command import ( + "path/filepath" "testing" "github.com/mitchellh/cli" ) +const fixturesDir = "./test-fixtures" + +func fatalCommand(t *testing.T, m Meta) { + ui := m.Ui.(*cli.MockUi) + t.Fatalf( + "Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s", + ui.OutputWriter.String(), + ui.ErrorWriter.String()) +} + +func testFixture(n string) string { + return filepath.Join(fixturesDir, n) +} + func testMeta(t *testing.T) Meta { return Meta{ Ui: new(cli.MockUi), diff --git a/command/push.go b/command/push.go index d50d549d1..1b43be6b9 100644 --- a/command/push.go +++ b/command/push.go @@ -3,18 +3,30 @@ package command import ( "flag" "fmt" + "io" + "path/filepath" "strings" + "github.com/hashicorp/harmony-go/archive" "github.com/mitchellh/packer/packer" ) +// archiveTemplateEntry is the name the template always takes within the slug. +const archiveTemplateEntry = ".packer-template.json" + type PushCommand struct { Meta + + // For tests: + uploadFn func(io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error) } func (c *PushCommand) Run(args []string) int { + var token string + f := flag.NewFlagSet("push", flag.ContinueOnError) f.Usage = func() { c.Ui.Error(c.Help()) } + f.StringVar(&token, "token", "", "token") if err := f.Parse(args); err != nil { return 1 } @@ -32,8 +44,66 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // TODO: validate the template - println(tpl.Push.Name) + // Validate some things + if tpl.Push.Name == "" { + c.Ui.Error(fmt.Sprintf( + "The 'push' section must be specified in the template with\n" + + "at least the 'name' option set.")) + return 1 + } + + // Build the archiving options + var opts archive.ArchiveOpts + opts.Include = tpl.Push.Include + opts.Exclude = tpl.Push.Exclude + opts.VCS = tpl.Push.VCS + opts.Extra = map[string]string{ + archiveTemplateEntry: args[0], + } + + // Determine the path we're archiving + path := tpl.Push.BaseDir + if path == "" { + path, err = filepath.Abs(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err)) + return 1 + } + path = filepath.Dir(path) + } + + // Build the upload options + var uploadOpts uploadOpts + uploadOpts.Slug = tpl.Push.Name + uploadOpts.Token = token + + // Start the archiving process + r, archiveErrCh, err := archive.Archive(path, &opts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error archiving: %s", err)) + return 1 + } + + // Start the upload process + doneCh, uploadErrCh, err := c.upload(r, &uploadOpts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error starting upload: %s", err)) + return 1 + } + + err = nil + select { + case err = <-archiveErrCh: + err = fmt.Errorf("Error archiving: %s", err) + case err = <-uploadErrCh: + err = fmt.Errorf("Error uploading: %s", err) + case <-doneCh: + } + + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } return 0 } @@ -45,6 +115,11 @@ Usage: packer push [options] TEMPLATE Push the template and the files it needs to a Packer build service. This will not initiate any builds, it will only update the templates used for builds. + +Options: + + -token= Access token to use to upload. If blank, the + TODO environmental variable will be used. ` return strings.TrimSpace(helpText) @@ -53,3 +128,18 @@ Usage: packer push [options] TEMPLATE func (*PushCommand) Synopsis() string { return "push template files to a Packer build service" } + +func (c *PushCommand) upload( + r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) { + if c.uploadFn != nil { + return c.uploadFn(r, opts) + } + + return nil, nil, nil +} + +type uploadOpts struct { + URL string + Slug string + Token string +} diff --git a/command/push_test.go b/command/push_test.go index b7e1f800a..b57276e5f 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -1,6 +1,14 @@ package command import ( + "fmt" + "archive/tar" + "bytes" + "compress/gzip" + "io" + "path/filepath" + "reflect" + "sort" "testing" ) @@ -19,3 +27,117 @@ func TestPush_multiArgs(t *testing.T) { t.Fatalf("bad: %#v", code) } } + +func TestPush(t *testing.T) { + var actualR io.Reader + var actualOpts *uploadOpts + uploadFn := func(r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) { + actualR = r + actualOpts = opts + + doneCh := make(chan struct{}) + close(doneCh) + return doneCh, nil, nil + } + + c := &PushCommand{ + Meta: testMeta(t), + uploadFn: uploadFn, + } + + args := []string{filepath.Join(testFixture("push"), "template.json")} + if code := c.Run(args); code != 0 { + fatalCommand(t, c.Meta) + } + + actual := testArchive(t, actualR) + expected := []string{ + archiveTemplateEntry, + "template.json", + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestPush_noName(t *testing.T) { + uploadFn := func(r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) { + return nil, nil, nil + } + + c := &PushCommand{ + Meta: testMeta(t), + uploadFn: uploadFn, + } + + args := []string{filepath.Join(testFixture("push-no-name"), "template.json")} + if code := c.Run(args); code != 1 { + fatalCommand(t, c.Meta) + } +} + +func TestPush_uploadError(t *testing.T) { + uploadFn := func(r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) { + return nil, nil, fmt.Errorf("bad") + } + + c := &PushCommand{ + Meta: testMeta(t), + uploadFn: uploadFn, + } + + args := []string{filepath.Join(testFixture("push"), "template.json")} + if code := c.Run(args); code != 1 { + fatalCommand(t, c.Meta) + } +} + +func TestPush_uploadErrorCh(t *testing.T) { + uploadFn := func(r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) { + errCh := make(chan error, 1) + errCh <- fmt.Errorf("bad") + return nil, errCh, nil + } + + c := &PushCommand{ + Meta: testMeta(t), + uploadFn: uploadFn, + } + + args := []string{filepath.Join(testFixture("push"), "template.json")} + if code := c.Run(args); code != 1 { + fatalCommand(t, c.Meta) + } +} + +func testArchive(t *testing.T, r io.Reader) []string { + // Finish the archiving process in-memory + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("err: %s", err) + } + + gzipR, err := gzip.NewReader(&buf) + if err != nil { + t.Fatalf("err: %s", err) + } + tarR := tar.NewReader(gzipR) + + // Read all the entries + result := make([]string, 0, 5) + for { + hdr, err := tarR.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("err: %s", err) + } + + result = append(result, hdr.Name) + } + + sort.Strings(result) + return result +} diff --git a/command/test-fixtures/push-no-name/template.json b/command/test-fixtures/push-no-name/template.json new file mode 100644 index 000000000..5b5ede505 --- /dev/null +++ b/command/test-fixtures/push-no-name/template.json @@ -0,0 +1,3 @@ +{ + "builders": [{"type": "dummy"}] +} diff --git a/command/test-fixtures/push/template.json b/command/test-fixtures/push/template.json new file mode 100644 index 000000000..63b0f9037 --- /dev/null +++ b/command/test-fixtures/push/template.json @@ -0,0 +1,7 @@ +{ + "builders": [{"type": "dummy"}], + + "push": { + "name": "foo/bar" + } +} diff --git a/packer/template.go b/packer/template.go index 50b1fffd6..562d23edd 100644 --- a/packer/template.go +++ b/packer/template.go @@ -45,6 +45,7 @@ type Template struct { // PushConfig is the configuration structure for the push settings. type PushConfig struct { Name string + BaseDir string Include []string Exclude []string VCS bool