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 }