github.com/aspring/packer@v0.8.1-0.20150629211158-9db281ac0f89/command/push.go (about)

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