github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/command/push.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"os/signal"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/atlas-go/archive"
    13  	"github.com/hashicorp/atlas-go/v1"
    14  	"github.com/mitchellh/packer/template"
    15  )
    16  
    17  // archiveTemplateEntry is the name the template always takes within the slug.
    18  const archiveTemplateEntry = ".packer-template"
    19  
    20  var (
    21  	reName         = regexp.MustCompile("^[a-zA-Z0-9-_./]+$")
    22  	errInvalidName = fmt.Errorf("Your build name can only contain these characters: %s", reName.String())
    23  )
    24  
    25  type PushCommand struct {
    26  	Meta
    27  
    28  	client *atlas.Client
    29  
    30  	// For tests:
    31  	uploadFn pushUploadFn
    32  }
    33  
    34  // pushUploadFn is the callback type used for tests to stub out the uploading
    35  // logic of the push command.
    36  type pushUploadFn func(
    37  	io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error)
    38  
    39  func (c *PushCommand) Run(args []string) int {
    40  	var token string
    41  	var message string
    42  	var name string
    43  	var create bool
    44  
    45  	flags := c.Meta.FlagSet("push", FlagSetVars)
    46  	flags.Usage = func() { c.Ui.Error(c.Help()) }
    47  	flags.StringVar(&token, "token", "", "token")
    48  	flags.StringVar(&message, "m", "", "message")
    49  	flags.StringVar(&message, "message", "", "message")
    50  	flags.StringVar(&name, "name", "", "name")
    51  	flags.BoolVar(&create, "create", false, "create (deprecated)")
    52  	if err := flags.Parse(args); err != nil {
    53  		return 1
    54  	}
    55  
    56  	if message != "" {
    57  		c.Ui.Say("[DEPRECATED] -m/-message is deprecated and will be removed in a future Packer release")
    58  	}
    59  
    60  	args = flags.Args()
    61  	if len(args) != 1 {
    62  		flags.Usage()
    63  		return 1
    64  	}
    65  
    66  	// Print deprecations
    67  	if create {
    68  		c.Ui.Error(fmt.Sprintf("The '-create' option is now the default and is\n" +
    69  			"longer used. It will be removed in the next version."))
    70  	}
    71  
    72  	// Parse the template
    73  	tpl, err := template.ParseFile(args[0])
    74  	if err != nil {
    75  		c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
    76  		return 1
    77  	}
    78  
    79  	// Get the core
    80  	core, err := c.Meta.Core(tpl)
    81  	if err != nil {
    82  		c.Ui.Error(err.Error())
    83  		return 1
    84  	}
    85  	push := core.Template.Push
    86  
    87  	// If we didn't pass name from the CLI, use the template
    88  	if name == "" {
    89  		name = push.Name
    90  	}
    91  
    92  	// Validate some things
    93  	if name == "" {
    94  		c.Ui.Error(fmt.Sprintf(
    95  			"The 'push' section must be specified in the template with\n" +
    96  				"at least the 'name' option set. Alternatively, you can pass the\n" +
    97  				"name parameter from the CLI."))
    98  		return 1
    99  	}
   100  
   101  	if !reName.MatchString(name) {
   102  		c.Ui.Error(errInvalidName.Error())
   103  		return 1
   104  	}
   105  
   106  	// Determine our token
   107  	if token == "" {
   108  		token = push.Token
   109  	}
   110  
   111  	// Build our client
   112  	defer func() { c.client = nil }()
   113  	c.client = atlas.DefaultClient()
   114  	if push.Address != "" {
   115  		c.client, err = atlas.NewClient(push.Address)
   116  		if err != nil {
   117  			c.Ui.Error(fmt.Sprintf(
   118  				"Error setting up API client: %s", err))
   119  			return 1
   120  		}
   121  	}
   122  	if token != "" {
   123  		c.client.Token = token
   124  	}
   125  
   126  	// Build the archiving options
   127  	var opts archive.ArchiveOpts
   128  	opts.Include = push.Include
   129  	opts.Exclude = push.Exclude
   130  	opts.VCS = push.VCS
   131  	opts.Extra = map[string]string{
   132  		archiveTemplateEntry: args[0],
   133  	}
   134  
   135  	// Determine the path we're archiving. This logic is a bit complicated
   136  	// as there are three possibilities:
   137  	//
   138  	//   1.) BaseDir is an absolute path, just use that.
   139  	//
   140  	//   2.) BaseDir is empty, so we use the directory of the template.
   141  	//
   142  	//   3.) BaseDir is relative, so we use the path relative to the directory
   143  	//       of the template.
   144  	//
   145  	path := push.BaseDir
   146  	if path == "" || !filepath.IsAbs(path) {
   147  		tplPath, err := filepath.Abs(args[0])
   148  		if err != nil {
   149  			c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
   150  			return 1
   151  		}
   152  		tplPath = filepath.Dir(tplPath)
   153  		if path != "" {
   154  			tplPath = filepath.Join(tplPath, path)
   155  		}
   156  		path, err = filepath.Abs(tplPath)
   157  		if err != nil {
   158  			c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
   159  			return 1
   160  		}
   161  	}
   162  
   163  	// Find the Atlas post-processors, if possible
   164  	var atlasPPs []*template.PostProcessor
   165  	for _, list := range tpl.PostProcessors {
   166  		for _, pp := range list {
   167  			if pp.Type == "atlas" {
   168  				atlasPPs = append(atlasPPs, pp)
   169  			}
   170  		}
   171  	}
   172  
   173  	// Build the upload options
   174  	var uploadOpts uploadOpts
   175  	uploadOpts.Slug = name
   176  	uploadOpts.Builds = make(map[string]*uploadBuildInfo)
   177  	for _, b := range tpl.Builders {
   178  		info := &uploadBuildInfo{Type: b.Type}
   179  
   180  		// Determine if we're artifacting this build
   181  		for _, pp := range atlasPPs {
   182  			if !pp.Skip(b.Name) {
   183  				info.Artifact = true
   184  				break
   185  			}
   186  		}
   187  
   188  		uploadOpts.Builds[b.Name] = info
   189  	}
   190  
   191  	// Collect the variables from CLI args and any var files
   192  	uploadOpts.Vars = core.Context().UserVariables
   193  
   194  	// Add the upload metadata
   195  	metadata := make(map[string]interface{})
   196  	if message != "" {
   197  		metadata["message"] = message
   198  	}
   199  	metadata["template"] = tpl.RawContents
   200  	metadata["template_name"] = filepath.Base(args[0])
   201  	uploadOpts.Metadata = metadata
   202  
   203  	// Warn about builds not having post-processors.
   204  	var badBuilds []string
   205  	for name, b := range uploadOpts.Builds {
   206  		if b.Artifact {
   207  			continue
   208  		}
   209  
   210  		badBuilds = append(badBuilds, name)
   211  	}
   212  	if len(badBuilds) > 0 {
   213  		c.Ui.Error(fmt.Sprintf(
   214  			"Warning! One or more of the builds in this template does not\n"+
   215  				"have an Atlas post-processor. Artifacts from this template will\n"+
   216  				"not appear in the Atlas artifact registry.\n\n"+
   217  				"This is just a warning. Atlas will still build your template\n"+
   218  				"and assume other post-processors are sending the artifacts where\n"+
   219  				"they need to go.\n\n"+
   220  				"Builds: %s\n\n", strings.Join(badBuilds, ", ")))
   221  	}
   222  
   223  	// Start the archiving process
   224  	r, err := archive.CreateArchive(path, &opts)
   225  	if err != nil {
   226  		c.Ui.Error(fmt.Sprintf("Error archiving: %s", err))
   227  		return 1
   228  	}
   229  	defer r.Close()
   230  
   231  	// Start the upload process
   232  	doneCh, uploadErrCh, err := c.upload(r, &uploadOpts)
   233  	if err != nil {
   234  		c.Ui.Error(fmt.Sprintf("Error starting upload: %s", err))
   235  		return 1
   236  	}
   237  
   238  	// Make a ctrl-C channel
   239  	sigCh := make(chan os.Signal, 1)
   240  	signal.Notify(sigCh, os.Interrupt)
   241  	defer signal.Stop(sigCh)
   242  
   243  	err = nil
   244  	select {
   245  	case err = <-uploadErrCh:
   246  		err = fmt.Errorf("Error uploading: %s", err)
   247  	case <-sigCh:
   248  		err = fmt.Errorf("Push cancelled from Ctrl-C")
   249  	case <-doneCh:
   250  	}
   251  
   252  	if err != nil {
   253  		c.Ui.Error(err.Error())
   254  		return 1
   255  	}
   256  
   257  	c.Ui.Say(fmt.Sprintf("Push successful to '%s'", name))
   258  	return 0
   259  }
   260  
   261  func (*PushCommand) Help() string {
   262  	helpText := `
   263  Usage: packer push [options] TEMPLATE
   264  
   265    Push the given template and supporting files to a Packer build service such as
   266    Atlas.
   267  
   268    If a build configuration for the given template does not exist, it will be
   269    created automatically. If the build configuration already exists, a new
   270    version will be created with this template and the supporting files.
   271  
   272    Additional configuration options (such as the Atlas server URL and files to
   273    include) may be specified in the "push" section of the Packer template. Please
   274    see the online documentation for more information about these configurables.
   275  
   276  Options:
   277  
   278    -name=<name>             The destination build in Atlas. This is in a format
   279                             "username/name".
   280  
   281    -token=<token>           The access token to use to when uploading
   282  
   283    -var 'key=value'         Variable for templates, can be used multiple times.
   284  
   285    -var-file=path           JSON file containing user variables.
   286  `
   287  
   288  	return strings.TrimSpace(helpText)
   289  }
   290  
   291  func (*PushCommand) Synopsis() string {
   292  	return "push a template and supporting files to a Packer build service"
   293  }
   294  
   295  func (c *PushCommand) upload(
   296  	r *archive.Archive, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
   297  	if c.uploadFn != nil {
   298  		return c.uploadFn(r, opts)
   299  	}
   300  
   301  	// Separate the slug into the user and name components
   302  	user, name, err := atlas.ParseSlug(opts.Slug)
   303  	if err != nil {
   304  		return nil, nil, fmt.Errorf("upload: %s", err)
   305  	}
   306  
   307  	// Get the build configuration
   308  	bc, err := c.client.BuildConfig(user, name)
   309  	if err != nil {
   310  		if err == atlas.ErrNotFound {
   311  			// Build configuration doesn't exist, attempt to create it
   312  			bc, err = c.client.CreateBuildConfig(user, name)
   313  		}
   314  
   315  		if err != nil {
   316  			return nil, nil, fmt.Errorf("upload: %s", err)
   317  		}
   318  	}
   319  
   320  	// Build the version to send up
   321  	version := atlas.BuildConfigVersion{
   322  		User:   bc.User,
   323  		Name:   bc.Name,
   324  		Builds: make([]atlas.BuildConfigBuild, 0, len(opts.Builds)),
   325  	}
   326  
   327  	// Build the BuildVars struct
   328  
   329  	buildVars := atlas.BuildVars{}
   330  	for k, v := range opts.Vars {
   331  		buildVars = append(buildVars, atlas.BuildVar{
   332  			Key:   k,
   333  			Value: v,
   334  		})
   335  	}
   336  
   337  	for name, info := range opts.Builds {
   338  		version.Builds = append(version.Builds, atlas.BuildConfigBuild{
   339  			Name:     name,
   340  			Type:     info.Type,
   341  			Artifact: info.Artifact,
   342  		})
   343  	}
   344  
   345  	// Start the upload
   346  	doneCh, errCh := make(chan struct{}), make(chan error)
   347  	go func() {
   348  		err := c.client.UploadBuildConfigVersion(&version, opts.Metadata, buildVars, r, r.Size)
   349  		if err != nil {
   350  			errCh <- err
   351  			return
   352  		}
   353  
   354  		close(doneCh)
   355  	}()
   356  
   357  	return doneCh, errCh, nil
   358  }
   359  
   360  type uploadOpts struct {
   361  	URL      string
   362  	Slug     string
   363  	Builds   map[string]*uploadBuildInfo
   364  	Metadata map[string]interface{}
   365  	Vars     map[string]string
   366  }
   367  
   368  type uploadBuildInfo struct {
   369  	Type     string
   370  	Artifact bool
   371  }