push.go 5.49 KB
Newer Older
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
1 2 3 4 5
package command

import (
	"flag"
	"fmt"
6 7
	"io"
	"path/filepath"
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
8 9
	"strings"

10
	"github.com/hashicorp/harmony-go"
11
	"github.com/hashicorp/harmony-go/archive"
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
12 13 14
	"github.com/mitchellh/packer/packer"
)

15
// archiveTemplateEntry is the name the template always takes within the slug.
16
const archiveTemplateEntry = ".packer-template"
17

Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
18 19
type PushCommand struct {
	Meta
20

21 22
	client *harmony.Client

23 24
	// For tests:
	uploadFn func(io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error)
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
25 26 27
}

func (c *PushCommand) Run(args []string) int {
28
	var create bool
29 30
	var token string

Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
31 32
	f := flag.NewFlagSet("push", flag.ContinueOnError)
	f.Usage = func() { c.Ui.Error(c.Help()) }
33
	f.BoolVar(&create, "create", false, "create")
34
	f.StringVar(&token, "token", "", "token")
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
	if err := f.Parse(args); err != nil {
		return 1
	}

	args = f.Args()
	if len(args) != 1 {
		f.Usage()
		return 1
	}

	// Read the template
	tpl, err := packer.ParseTemplateFile(args[0], nil)
	if err != nil {
		c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
		return 1
	}

52 53 54 55 56 57 58 59
	// 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
	}

60 61
	// Build our client
	defer func() { c.client = nil }()
62 63 64 65 66 67 68 69 70
	c.client = harmony.DefaultClient()
	if tpl.Push.Address != "" {
		c.client, err = harmony.NewClient(tpl.Push.Address)
		if err != nil {
			c.Ui.Error(fmt.Sprintf(
				"Error setting up API client: %s", err))
			return 1
		}
	}
71

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
	// 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
96 97 98 99
	uploadOpts.Builds = make(map[string]string)
	for _, b := range tpl.Builders {
		uploadOpts.Builds[b.Name] = b.Type
	}
100

101 102 103 104 105 106
	// Create the build config if it doesn't currently exist.
	if err := c.create(uploadOpts.Slug, create); err != nil {
		c.Ui.Error(err.Error())
		return 1
	}

107 108 109 110 111 112
	// 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
	}
113
	defer r.Close()
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134

	// 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
	}
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
135 136 137 138 139 140 141 142 143 144 145

	return 0
}

func (*PushCommand) Help() string {
	helpText := `
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.
146

147 148 149
  The configuration about what is pushed is configured within the
  template's "push" section.

150 151
Options:

152 153
  -create             Create the build configuration if it doesn't exist.

154 155
  -token=<token>      Access token to use to upload. If blank, the
                      TODO environmental variable will be used.
Mitchell Hashimoto's avatar
Mitchell Hashimoto committed
156 157 158 159 160 161 162 163
`

	return strings.TrimSpace(helpText)
}

func (*PushCommand) Synopsis() string {
	return "push template files to a Packer build service"
}
164

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
func (c *PushCommand) create(name string, create bool) error {
	if c.uploadFn != nil {
		return nil
	}

	// Separate the slug into the user and name components
	user, name, err := harmony.ParseSlug(name)
	if err != nil {
		return fmt.Errorf("Malformed push name: %s", err)
	}

	// Check if it exists. If so, we're done.
	if _, err := c.client.BuildConfig(user, name); err == nil {
		return nil
	} else if err != harmony.ErrNotFound {
		return err
	}

	// Otherwise, show an error if we're not creating.
	if !create {
		return fmt.Errorf(
			"Push target doesn't exist: %s. Either create this online via\n" +
				"the website or pass the -create flag.")
	}

	// Create it
	if err := c.client.CreateBuildConfig(user, name); err != nil {
		return err
	}

	return nil
}

198 199 200 201 202 203
func (c *PushCommand) upload(
	r io.Reader, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
	if c.uploadFn != nil {
		return c.uploadFn(r, opts)
	}

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
	// Separate the slug into the user and name components
	user, name, err := harmony.ParseSlug(opts.Slug)
	if err != nil {
		return nil, nil, fmt.Errorf("upload: %s", err)
	}

	// Get the app
	bc, err := c.client.BuildConfig(user, name)
	if err != nil {
		return nil, nil, fmt.Errorf("upload: %s", err)
	}

	// Build the version to send up
	version := harmony.BuildConfigVersion{
		User:   bc.User,
		Name:   bc.Name,
		Builds: make([]harmony.BuildConfigBuild, 0, len(opts.Builds)),
	}
	for name, t := range opts.Builds {
		version.Builds = append(version.Builds, harmony.BuildConfigBuild{
			Name: name,
			Type: t,
		})
	}

	// Start the upload
	doneCh, errCh := make(chan struct{}), make(chan error)
	go func() {
		err := c.client.UploadBuildConfigVersion(&version, r)
		if err != nil {
			errCh <- err
			return
		}

		close(doneCh)
	}()

	return doneCh, errCh, nil
242 243 244
}

type uploadOpts struct {
245 246 247 248
	URL    string
	Slug   string
	Token  string
	Builds map[string]string
249
}