github.com/kikitux/packer@v0.10.1-0.20160322154024-6237df566f9f/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 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 }