packer-cn/command/push.go

356 lines
8.6 KiB
Go
Raw Normal View History

2014-10-28 18:09:22 -04:00
package command
import (
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"regexp"
2014-10-28 18:09:22 -04:00
"strings"
2014-12-01 18:20:10 -05:00
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
2015-05-26 12:38:09 -04:00
"github.com/mitchellh/packer/template"
2014-10-28 18:09:22 -04:00
)
// archiveTemplateEntry is the name the template always takes within the slug.
const archiveTemplateEntry = ".packer-template"
var (
reName = regexp.MustCompile("^[a-zA-Z0-9-_/]+$")
errInvalidName = fmt.Errorf("Your build name can only contain these characters: [a-zA-Z0-9-_]+")
)
2014-10-28 18:09:22 -04:00
type PushCommand struct {
Meta
2014-12-01 18:20:10 -05:00
client *atlas.Client
// For tests:
uploadFn pushUploadFn
2014-10-28 18:09:22 -04:00
}
// pushUploadFn is the callback type used for tests to stub out the uploading
// logic of the push command.
type pushUploadFn func(
io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error)
2014-10-28 18:09:22 -04:00
func (c *PushCommand) Run(args []string) int {
var token string
2015-02-03 18:15:02 -05:00
var message string
var name string
var create bool
2015-05-29 18:35:55 -04:00
f := c.Meta.FlagSet("push", FlagSetVars)
2014-10-28 18:09:22 -04:00
f.Usage = func() { c.Ui.Error(c.Help()) }
f.StringVar(&token, "token", "", "token")
f.StringVar(&message, "m", "", "message")
2015-02-03 18:15:02 -05:00
f.StringVar(&message, "message", "", "message")
f.StringVar(&name, "name", "", "name")
f.BoolVar(&create, "create", false, "create (deprecated)")
2014-10-28 18:09:22 -04:00
if err := f.Parse(args); err != nil {
return 1
}
args = f.Args()
if len(args) != 1 {
f.Usage()
return 1
}
// Print deprecations
if create {
c.Ui.Error(fmt.Sprintf("The '-create' option is now the default and is\n" +
"longer used. It will be removed in the next version."))
}
2015-05-26 12:38:09 -04:00
// Parse the template
tpl, err := template.ParseFile(args[0])
2014-10-28 18:09:22 -04:00
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
}
2015-05-29 18:35:55 -04:00
// Get the core
core, err := c.Meta.Core(tpl)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
2015-05-29 18:41:52 -04:00
push := core.Template.Push
2015-05-29 18:35:55 -04:00
// If we didn't pass name from the CLI, use the template
if name == "" {
2015-05-29 18:35:55 -04:00
name = push.Name
}
// Validate some things
if name == "" {
c.Ui.Error(fmt.Sprintf(
"The 'push' section must be specified in the template with\n" +
2015-05-28 18:18:36 -04:00
"at least the 'name' option set. Alternatively, you can pass the\n" +
"name parameter from the CLI."))
return 1
}
if !reName.MatchString(name) {
c.Ui.Error(errInvalidName.Error())
return 1
}
2014-11-06 11:43:31 -05:00
// Determine our token
if token == "" {
2015-05-29 18:35:55 -04:00
token = push.Token
2014-11-06 11:43:31 -05:00
}
// Build our client
defer func() { c.client = nil }()
2014-12-01 18:20:10 -05:00
c.client = atlas.DefaultClient()
2015-05-29 18:35:55 -04:00
if push.Address != "" {
c.client, err = atlas.NewClient(push.Address)
2014-10-28 23:06:19 -04:00
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error setting up API client: %s", err))
return 1
}
}
if token != "" {
c.client.Token = token
}
// Build the archiving options
var opts archive.ArchiveOpts
2015-05-29 18:35:55 -04:00
opts.Include = push.Include
opts.Exclude = push.Exclude
opts.VCS = push.VCS
opts.Extra = map[string]string{
archiveTemplateEntry: args[0],
}
2014-12-01 22:49:55 -05:00
// Determine the path we're archiving. This logic is a bit complicated
// as there are three possibilities:
//
// 1.) BaseDir is an absolute path, just use that.
//
// 2.) BaseDir is empty, so we use the directory of the template.
//
// 3.) BaseDir is relative, so we use the path relative to the directory
// of the template.
//
2015-05-29 18:35:55 -04:00
path := push.BaseDir
2014-12-01 22:49:55 -05:00
if path == "" || !filepath.IsAbs(path) {
tplPath, err := filepath.Abs(args[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
return 1
}
tplPath = filepath.Dir(tplPath)
if path != "" {
tplPath = filepath.Join(tplPath, path)
}
path, err = filepath.Abs(tplPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
return 1
}
}
// Find the Atlas post-processors, if possible
2015-05-26 12:38:09 -04:00
var atlasPPs []*template.PostProcessor
for _, list := range tpl.PostProcessors {
for _, pp := range list {
if pp.Type == "atlas" {
atlasPPs = append(atlasPPs, pp)
}
}
}
// Build the upload options
var uploadOpts uploadOpts
uploadOpts.Slug = name
uploadOpts.Builds = make(map[string]*uploadBuildInfo)
for _, b := range tpl.Builders {
info := &uploadBuildInfo{Type: b.Type}
// Determine if we're artifacting this build
for _, pp := range atlasPPs {
if !pp.Skip(b.Name) {
info.Artifact = true
break
}
}
uploadOpts.Builds[b.Name] = info
}
// Add the upload metadata
metadata := make(map[string]interface{})
if message != "" {
metadata["message"] = message
}
metadata["template"] = tpl.RawContents
metadata["template_name"] = filepath.Base(args[0])
uploadOpts.Metadata = metadata
2014-12-03 15:04:01 -05:00
// Warn about builds not having post-processors.
var badBuilds []string
for name, b := range uploadOpts.Builds {
if b.Artifact {
continue
}
badBuilds = append(badBuilds, name)
}
if len(badBuilds) > 0 {
c.Ui.Error(fmt.Sprintf(
"Warning! One or more of the builds in this template does not\n"+
"have an Atlas post-processor. Artifacts from this template will\n"+
"not appear in the Atlas artifact registry.\n\n"+
"This is just a warning. Atlas will still build your template\n"+
"and assume other post-processors are sending the artifacts where\n"+
"they need to go.\n\n"+
"Builds: %s\n\n", strings.Join(badBuilds, ", ")))
2014-12-03 15:04:01 -05:00
}
// Start the archiving process
2014-12-01 18:20:10 -05:00
r, err := archive.CreateArchive(path, &opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error archiving: %s", err))
return 1
}
2014-10-28 22:34:19 -04:00
defer r.Close()
// 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
}
// Make a ctrl-C channel
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
defer signal.Stop(sigCh)
err = nil
select {
case err = <-uploadErrCh:
err = fmt.Errorf("Error uploading: %s", err)
case <-sigCh:
err = fmt.Errorf("Push cancelled from Ctrl-C")
case <-doneCh:
}
if err != nil {
c.Ui.Error(err.Error())
return 1
}
2014-10-28 18:09:22 -04:00
2015-06-10 00:09:56 -04:00
c.Ui.Say(fmt.Sprintf("Push successful to '%s'", name))
2014-10-28 18:09:22 -04:00
return 0
}
func (*PushCommand) Help() string {
helpText := `
Usage: packer push [options] TEMPLATE
Push the given template and supporting files to a Packer build service such as
Atlas.
If a build configuration for the given template does not exist, it will be
created automatically. If the build configuration already exists, a new
version will be created with this template and the supporting files.
Additional configuration options (such as the Atlas server URL and files to
include) may be specified in the "push" section of the Packer template. Please
see the online documentation for more information about these configurables.
Options:
-m, -message=<detail> A message to identify the purpose or changes in this
Packer template much like a VCS commit message
2015-02-03 18:15:02 -05:00
-name=<name> The destination build in Atlas. This is in a format
"username/name".
-token=<token> The access token to use to when uploading
2015-05-29 18:35:55 -04:00
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
2014-10-28 18:09:22 -04:00
`
return strings.TrimSpace(helpText)
}
func (*PushCommand) Synopsis() string {
return "push a template and supporting files to a Packer build service"
}
func (c *PushCommand) upload(
2014-12-01 18:20:10 -05:00
r *archive.Archive, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
if c.uploadFn != nil {
return c.uploadFn(r, opts)
}
// Separate the slug into the user and name components
2014-12-01 18:20:10 -05:00
user, name, err := atlas.ParseSlug(opts.Slug)
if err != nil {
return nil, nil, fmt.Errorf("upload: %s", err)
}
// Get the build configuration
bc, err := c.client.BuildConfig(user, name)
if err != nil {
if err == atlas.ErrNotFound {
// Build configuration doesn't exist, attempt to create it
bc, err = c.client.CreateBuildConfig(user, name)
}
if err != nil {
return nil, nil, fmt.Errorf("upload: %s", err)
}
}
// Build the version to send up
2014-12-01 18:20:10 -05:00
version := atlas.BuildConfigVersion{
User: bc.User,
Name: bc.Name,
2014-12-01 18:20:10 -05:00
Builds: make([]atlas.BuildConfigBuild, 0, len(opts.Builds)),
}
for name, info := range opts.Builds {
2014-12-01 18:20:10 -05:00
version.Builds = append(version.Builds, atlas.BuildConfigBuild{
Name: name,
Type: info.Type,
Artifact: info.Artifact,
})
}
// Start the upload
doneCh, errCh := make(chan struct{}), make(chan error)
go func() {
err := c.client.UploadBuildConfigVersion(&version, opts.Metadata, r, r.Size)
if err != nil {
errCh <- err
return
}
close(doneCh)
}()
return doneCh, errCh, nil
}
type uploadOpts struct {
URL string
Slug string
Builds map[string]*uploadBuildInfo
Metadata map[string]interface{}
}
type uploadBuildInfo struct {
Type string
Artifact bool
}