github.com/rothwerx/packer@v0.9.0/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  	if message != "" {
    57  		c.Ui.Say("[DEPRECATED] -m/-message is deprecated and will be removed in a future Packer release")
    58  	}
    59  
    60  	args = f.Args()
    61  	if len(args) != 1 {
    62  		f.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  	// Add the upload metadata
   192  	metadata := make(map[string]interface{})
   193  	if message != "" {
   194  		metadata["message"] = message
   195  	}
   196  	metadata["template"] = tpl.RawContents
   197  	metadata["template_name"] = filepath.Base(args[0])
   198  	uploadOpts.Metadata = metadata
   199  
   200  	// Warn about builds not having post-processors.
   201  	var badBuilds []string
   202  	for name, b := range uploadOpts.Builds {
   203  		if b.Artifact {
   204  			continue
   205  		}
   206  
   207  		badBuilds = append(badBuilds, name)
   208  	}
   209  	if len(badBuilds) > 0 {
   210  		c.Ui.Error(fmt.Sprintf(
   211  			"Warning! One or more of the builds in this template does not\n"+
   212  				"have an Atlas post-processor. Artifacts from this template will\n"+
   213  				"not appear in the Atlas artifact registry.\n\n"+
   214  				"This is just a warning. Atlas will still build your template\n"+
   215  				"and assume other post-processors are sending the artifacts where\n"+
   216  				"they need to go.\n\n"+
   217  				"Builds: %s\n\n", strings.Join(badBuilds, ", ")))
   218  	}
   219  
   220  	// Start the archiving process
   221  	r, err := archive.CreateArchive(path, &opts)
   222  	if err != nil {
   223  		c.Ui.Error(fmt.Sprintf("Error archiving: %s", err))
   224  		return 1
   225  	}
   226  	defer r.Close()
   227  
   228  	// Start the upload process
   229  	doneCh, uploadErrCh, err := c.upload(r, &uploadOpts)
   230  	if err != nil {
   231  		c.Ui.Error(fmt.Sprintf("Error starting upload: %s", err))
   232  		return 1
   233  	}
   234  
   235  	// Make a ctrl-C channel
   236  	sigCh := make(chan os.Signal, 1)
   237  	signal.Notify(sigCh, os.Interrupt)
   238  	defer signal.Stop(sigCh)
   239  
   240  	err = nil
   241  	select {
   242  	case err = <-uploadErrCh:
   243  		err = fmt.Errorf("Error uploading: %s", err)
   244  	case <-sigCh:
   245  		err = fmt.Errorf("Push cancelled from Ctrl-C")
   246  	case <-doneCh:
   247  	}
   248  
   249  	if err != nil {
   250  		c.Ui.Error(err.Error())
   251  		return 1
   252  	}
   253  
   254  	c.Ui.Say(fmt.Sprintf("Push successful to '%s'", name))
   255  	return 0
   256  }
   257  
   258  func (*PushCommand) Help() string {
   259  	helpText := `
   260  Usage: packer push [options] TEMPLATE
   261  
   262    Push the given template and supporting files to a Packer build service such as
   263    Atlas.
   264  
   265    If a build configuration for the given template does not exist, it will be
   266    created automatically. If the build configuration already exists, a new
   267    version will be created with this template and the supporting files.
   268  
   269    Additional configuration options (such as the Atlas server URL and files to
   270    include) may be specified in the "push" section of the Packer template. Please
   271    see the online documentation for more information about these configurables.
   272  
   273  Options:
   274  
   275    -name=<name>             The destination build in Atlas. This is in a format
   276                             "username/name".
   277  
   278    -token=<token>           The access token to use to when uploading
   279  
   280    -var 'key=value'         Variable for templates, can be used multiple times.
   281  
   282    -var-file=path           JSON file containing user variables.
   283  `
   284  
   285  	return strings.TrimSpace(helpText)
   286  }
   287  
   288  func (*PushCommand) Synopsis() string {
   289  	return "push a template and supporting files to a Packer build service"
   290  }
   291  
   292  func (c *PushCommand) upload(
   293  	r *archive.Archive, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
   294  	if c.uploadFn != nil {
   295  		return c.uploadFn(r, opts)
   296  	}
   297  
   298  	// Separate the slug into the user and name components
   299  	user, name, err := atlas.ParseSlug(opts.Slug)
   300  	if err != nil {
   301  		return nil, nil, fmt.Errorf("upload: %s", err)
   302  	}
   303  
   304  	// Get the build configuration
   305  	bc, err := c.client.BuildConfig(user, name)
   306  	if err != nil {
   307  		if err == atlas.ErrNotFound {
   308  			// Build configuration doesn't exist, attempt to create it
   309  			bc, err = c.client.CreateBuildConfig(user, name)
   310  		}
   311  
   312  		if err != nil {
   313  			return nil, nil, fmt.Errorf("upload: %s", err)
   314  		}
   315  	}
   316  
   317  	// Build the version to send up
   318  	version := atlas.BuildConfigVersion{
   319  		User:   bc.User,
   320  		Name:   bc.Name,
   321  		Builds: make([]atlas.BuildConfigBuild, 0, len(opts.Builds)),
   322  	}
   323  	for name, info := range opts.Builds {
   324  		version.Builds = append(version.Builds, atlas.BuildConfigBuild{
   325  			Name:     name,
   326  			Type:     info.Type,
   327  			Artifact: info.Artifact,
   328  		})
   329  	}
   330  
   331  	// Start the upload
   332  	doneCh, errCh := make(chan struct{}), make(chan error)
   333  	go func() {
   334  		err := c.client.UploadBuildConfigVersion(&version, opts.Metadata, r, r.Size)
   335  		if err != nil {
   336  			errCh <- err
   337  			return
   338  		}
   339  
   340  		close(doneCh)
   341  	}()
   342  
   343  	return doneCh, errCh, nil
   344  }
   345  
   346  type uploadOpts struct {
   347  	URL      string
   348  	Slug     string
   349  	Builds   map[string]*uploadBuildInfo
   350  	Metadata map[string]interface{}
   351  }
   352  
   353  type uploadBuildInfo struct {
   354  	Type     string
   355  	Artifact bool
   356  }