Commit fbc15510 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

command/push: partially implemented, tests

parent 84c83447
package command package command
import ( import (
"path/filepath"
"testing" "testing"
"github.com/mitchellh/cli" "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 { func testMeta(t *testing.T) Meta {
return Meta{ return Meta{
Ui: new(cli.MockUi), Ui: new(cli.MockUi),
......
...@@ -3,18 +3,30 @@ package command ...@@ -3,18 +3,30 @@ package command
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"path/filepath"
"strings" "strings"
"github.com/hashicorp/harmony-go/archive"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
// archiveTemplateEntry is the name the template always takes within the slug.
const archiveTemplateEntry = ".packer-template.json"
type PushCommand struct { type PushCommand struct {
Meta Meta
// For tests:
uploadFn func(io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error)
} }
func (c *PushCommand) Run(args []string) int { func (c *PushCommand) Run(args []string) int {
var token string
f := flag.NewFlagSet("push", flag.ContinueOnError) f := flag.NewFlagSet("push", flag.ContinueOnError)
f.Usage = func() { c.Ui.Error(c.Help()) } f.Usage = func() { c.Ui.Error(c.Help()) }
f.StringVar(&token, "token", "", "token")
if err := f.Parse(args); err != nil { if err := f.Parse(args); err != nil {
return 1 return 1
} }
...@@ -32,8 +44,66 @@ func (c *PushCommand) Run(args []string) int { ...@@ -32,8 +44,66 @@ func (c *PushCommand) Run(args []string) int {
return 1 return 1
} }
// TODO: validate the template // Validate some things
println(tpl.Push.Name) 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 return 0
} }
...@@ -45,6 +115,11 @@ Usage: packer push [options] TEMPLATE ...@@ -45,6 +115,11 @@ Usage: packer push [options] TEMPLATE
Push the template and the files it needs to a Packer build service. 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 This will not initiate any builds, it will only update the templates
used for builds. used for builds.
Options:
-token=<token> Access token to use to upload. If blank, the
TODO environmental variable will be used.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
...@@ -53,3 +128,18 @@ Usage: packer push [options] TEMPLATE ...@@ -53,3 +128,18 @@ Usage: packer push [options] TEMPLATE
func (*PushCommand) Synopsis() string { func (*PushCommand) Synopsis() string {
return "push template files to a Packer build service" 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
}
package command package command
import ( import (
"fmt"
"archive/tar"
"bytes"
"compress/gzip"
"io"
"path/filepath"
"reflect"
"sort"
"testing" "testing"
) )
...@@ -19,3 +27,117 @@ func TestPush_multiArgs(t *testing.T) { ...@@ -19,3 +27,117 @@ func TestPush_multiArgs(t *testing.T) {
t.Fatalf("bad: %#v", code) 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
}
{
"builders": [{"type": "dummy"}]
}
{
"builders": [{"type": "dummy"}],
"push": {
"name": "foo/bar"
}
}
...@@ -45,6 +45,7 @@ type Template struct { ...@@ -45,6 +45,7 @@ type Template struct {
// PushConfig is the configuration structure for the push settings. // PushConfig is the configuration structure for the push settings.
type PushConfig struct { type PushConfig struct {
Name string Name string
BaseDir string
Include []string Include []string
Exclude []string Exclude []string
VCS bool VCS bool
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment