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 }