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