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