github.com/hashicorp/packer@v1.14.3/command/build.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package command 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "log" 12 "math" 13 "strconv" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/hashicorp/hcl/v2" 19 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 20 "github.com/hashicorp/packer/internal/hcp/registry" 21 "github.com/hashicorp/packer/packer" 22 "golang.org/x/sync/semaphore" 23 24 "github.com/hako/durafmt" 25 "github.com/posener/complete" 26 ) 27 28 const ( 29 hcpReadyIntegrationURL = "https://developer.hashicorp.com/packer/integrations?flags=hcp-ready" 30 ) 31 32 type BuildCommand struct { 33 Meta 34 } 35 36 func (c *BuildCommand) Run(args []string) int { 37 ctx, cleanup := handleTermInterrupt(c.Ui) 38 defer cleanup() 39 40 cfg, ret := c.ParseArgs(args) 41 if ret != 0 { 42 return ret 43 } 44 45 return c.RunContext(ctx, cfg) 46 } 47 48 func (c *BuildCommand) ParseArgs(args []string) (*BuildArgs, int) { 49 var cfg BuildArgs 50 flags := c.Meta.FlagSet("build") 51 flags.Usage = func() { c.Ui.Say(c.Help()) } 52 cfg.AddFlagSets(flags) 53 if err := flags.Parse(args); err != nil { 54 return &cfg, 1 55 } 56 57 if cfg.ParallelBuilds < 1 { 58 cfg.ParallelBuilds = math.MaxInt64 59 } 60 61 args = flags.Args() 62 if len(args) != 1 { 63 flags.Usage() 64 return &cfg, 1 65 } 66 cfg.Path = args[0] 67 return &cfg, 0 68 } 69 70 func writeDiags(ui packersdk.Ui, files map[string]*hcl.File, diags hcl.Diagnostics) int { 71 // write HCL errors/diagnostics if any. 72 b := bytes.NewBuffer(nil) 73 err := hcl.NewDiagnosticTextWriter(b, files, 80, false).WriteDiagnostics(diags) 74 if err != nil { 75 ui.Error("could not write diagnostic: " + err.Error()) 76 return 1 77 } 78 if b.Len() != 0 { 79 if diags.HasErrors() { 80 ui.Error(b.String()) 81 return 1 82 } 83 ui.Say(b.String()) 84 } 85 return 0 86 } 87 88 func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int { 89 // Set the release only flag if specified as argument 90 // 91 // This deactivates the capacity for Packer to load development binaries. 92 c.CoreConfig.Components.PluginConfig.ReleasesOnly = cla.ReleaseOnly 93 94 packerStarter, ret := c.GetConfig(&cla.MetaArgs) 95 if ret != 0 { 96 return ret 97 } 98 99 diags := packerStarter.DetectPluginBinaries() 100 ret = writeDiags(c.Ui, nil, diags) 101 if ret != 0 { 102 return ret 103 } 104 105 diags = packerStarter.Initialize(packer.InitializeOptions{ 106 UseSequential: cla.UseSequential, 107 }) 108 109 if packer.PackerUseProto { 110 log.Printf("[TRACE] Using protobuf for communication with plugins") 111 } 112 113 ret = writeDiags(c.Ui, nil, diags) 114 if ret != 0 { 115 return ret 116 } 117 118 hcpRegistry, diags := registry.New(packerStarter, c.Ui) 119 ret = writeDiags(c.Ui, nil, diags) 120 if ret != 0 { 121 return ret 122 } 123 hcpRegistry.Metadata().Gather(GetCleanedBuildArgs(cla)) 124 125 defer hcpRegistry.VersionStatusSummary() 126 127 err := hcpRegistry.PopulateVersion(buildCtx) 128 if err != nil { 129 return writeDiags(c.Ui, nil, hcl.Diagnostics{ 130 &hcl.Diagnostic{ 131 Summary: "HCP: populating version failed", 132 Severity: hcl.DiagError, 133 Detail: err.Error(), 134 }, 135 }) 136 } 137 138 builds, diags := packerStarter.GetBuilds(packer.GetBuildsOptions{ 139 Only: cla.Only, 140 Except: cla.Except, 141 Debug: cla.Debug, 142 Force: cla.Force, 143 OnError: cla.OnError, 144 }) 145 146 // here, something could have gone wrong but we still want to run valid 147 // builds. 148 ret = writeDiags(c.Ui, nil, diags) 149 if len(builds) == 0 && ret != 0 { 150 return ret 151 } 152 153 if cla.Debug { 154 c.Ui.Say("Debug mode enabled. Builds will not be parallelized.") 155 } 156 157 // Compile all the UIs for the builds 158 colors := [5]packer.UiColor{ 159 packer.UiColorGreen, 160 packer.UiColorCyan, 161 packer.UiColorMagenta, 162 packer.UiColorYellow, 163 packer.UiColorBlue, 164 } 165 buildUis := make(map[*packer.CoreBuild]packersdk.Ui) 166 for i := range builds { 167 ui := c.Ui 168 if cla.Color { 169 // Only set up UI colors if -machine-readable isn't set. 170 if _, ok := c.Ui.(*packer.MachineReadableUi); !ok { 171 ui = &packer.ColoredUi{ 172 Color: colors[i%len(colors)], 173 Ui: ui, 174 } 175 ui.Say(fmt.Sprintf("%s: output will be in this color.", builds[i].Name())) 176 if i+1 == len(builds) { 177 // Add a newline between the color output and the actual output 178 c.Ui.Say("") 179 } 180 } 181 } 182 // Now add timestamps if requested 183 if cla.TimestampUi { 184 ui = &packer.TimestampedUi{ 185 Ui: ui, 186 } 187 } 188 189 buildUis[builds[i]] = ui 190 } 191 log.Printf("Build debug mode: %v", cla.Debug) 192 log.Printf("Force build: %v", cla.Force) 193 log.Printf("On error: %v", cla.OnError) 194 195 if len(builds) == 0 { 196 return writeDiags(c.Ui, nil, hcl.Diagnostics{ 197 &hcl.Diagnostic{ 198 Summary: "No builds to run", 199 Detail: "A build command cannot run without at least one build to process. " + 200 "If the only or except flags have been specified at run time check that" + 201 " at least one build is selected for execution.", 202 Severity: hcl.DiagError, 203 }, 204 }) 205 } 206 207 // Get the start of the build command 208 buildCommandStart := time.Now() 209 210 // Run all the builds in parallel and wait for them to complete 211 var wg sync.WaitGroup 212 var artifacts = struct { 213 sync.RWMutex 214 m map[string][]packersdk.Artifact 215 }{m: make(map[string][]packersdk.Artifact)} 216 // Get the builds we care about 217 var errs = struct { 218 sync.RWMutex 219 m map[string]error 220 }{m: make(map[string]error)} 221 limitParallel := semaphore.NewWeighted(cla.ParallelBuilds) 222 223 for i := range builds { 224 if err := buildCtx.Err(); err != nil { 225 log.Println("Interrupted, not going to start any more builds.") 226 break 227 } 228 229 b := builds[i] 230 name := b.Name() 231 ui := buildUis[b] 232 if err := limitParallel.Acquire(buildCtx, 1); err != nil { 233 ui.Error(fmt.Sprintf("Build '%s' failed to acquire semaphore: %s", name, err)) 234 errs.Lock() 235 errs.m[name] = err 236 errs.Unlock() 237 break 238 } 239 // Increment the waitgroup so we wait for this item to finish properly 240 wg.Add(1) 241 242 // Run the build in a goroutine 243 go func() { 244 // Get the start of the build 245 buildStart := time.Now() 246 247 defer wg.Done() 248 249 defer limitParallel.Release(1) 250 251 err := hcpRegistry.StartBuild(buildCtx, b) 252 // Seems odd to require this error check here. Now that it is an error we can just exit with diag 253 if err != nil { 254 // If the build is already done, we skip without a warning 255 if errors.As(err, ®istry.ErrBuildAlreadyDone{}) { 256 ui.Say(fmt.Sprintf("skipping already done build %q", name)) 257 return 258 } 259 writeDiags(c.Ui, nil, hcl.Diagnostics{ 260 &hcl.Diagnostic{ 261 Summary: fmt.Sprintf( 262 "hcp: failed to start build %q", 263 name), 264 Severity: hcl.DiagError, 265 Detail: err.Error(), 266 }, 267 }) 268 return 269 } 270 271 log.Printf("Starting build run: %s", name) 272 runArtifacts, err := b.Run(buildCtx, ui) 273 274 // Get the duration of the build and parse it 275 buildEnd := time.Now() 276 buildDuration := buildEnd.Sub(buildStart) 277 fmtBuildDuration := durafmt.Parse(buildDuration).LimitFirstN(2) 278 279 runArtifacts, hcperr := hcpRegistry.CompleteBuild( 280 buildCtx, 281 b, 282 runArtifacts, 283 err) 284 if hcperr != nil { 285 if _, ok := hcperr.(*registry.NotAHCPArtifactError); ok { 286 writeDiags(c.Ui, nil, hcl.Diagnostics{ 287 &hcl.Diagnostic{ 288 Severity: hcl.DiagError, 289 Summary: fmt.Sprintf("The %q builder produced an artifact that cannot be pushed to HCP Packer", b.Name()), 290 Detail: fmt.Sprintf( 291 `%s 292 Check that you are using an HCP Ready integration before trying again: 293 %s`, 294 hcperr, hcpReadyIntegrationURL), 295 }, 296 }) 297 } else { 298 writeDiags(c.Ui, nil, hcl.Diagnostics{ 299 &hcl.Diagnostic{ 300 Summary: fmt.Sprintf( 301 "publishing build metadata to HCP Packer for %q failed", 302 name), 303 Severity: hcl.DiagError, 304 Detail: hcperr.Error(), 305 }, 306 }) 307 } 308 } 309 310 if err != nil { 311 ui.Error(fmt.Sprintf("Build '%s' errored after %s: %s", name, fmtBuildDuration, err)) 312 errs.Lock() 313 errs.m[name] = err 314 errs.Unlock() 315 } else { 316 ui.Say(fmt.Sprintf("Build '%s' finished after %s.", name, fmtBuildDuration)) 317 if runArtifacts != nil { 318 artifacts.Lock() 319 artifacts.m[name] = runArtifacts 320 artifacts.Unlock() 321 } 322 } 323 324 // If the build succeeded but uploading to HCP failed, 325 // Packer should exit non-zero, so we re-assign the 326 // error to account for this case. 327 if hcperr != nil && err == nil { 328 errs.Lock() 329 errs.m[name] = hcperr 330 errs.Unlock() 331 } 332 }() 333 334 if cla.Debug { 335 log.Printf("Debug enabled, so waiting for build to finish: %s", b.Name()) 336 wg.Wait() 337 } 338 339 if cla.ParallelBuilds == 1 { 340 log.Printf("Parallelization disabled, waiting for build to finish: %s", b.Name()) 341 wg.Wait() 342 } 343 } 344 345 // Wait for both the builds to complete and the interrupt handler, 346 // if it is interrupted. 347 log.Printf("Waiting on builds to complete...") 348 wg.Wait() 349 350 // Get the duration of the buildCommand command and parse it 351 buildCommandEnd := time.Now() 352 buildCommandDuration := buildCommandEnd.Sub(buildCommandStart) 353 fmtBuildCommandDuration := durafmt.Parse(buildCommandDuration).LimitFirstN(2) 354 c.Ui.Say(fmt.Sprintf("\n==> Wait completed after %s", fmtBuildCommandDuration)) 355 356 if err := buildCtx.Err(); err != nil { 357 c.Ui.Say("Cleanly cancelled builds after being interrupted.") 358 return 1 359 } 360 361 if len(errs.m) > 0 { 362 c.Ui.Machine("error-count", strconv.FormatInt(int64(len(errs.m)), 10)) 363 364 c.Ui.Error("\n==> Some builds didn't complete successfully and had errors:") 365 for name, err := range errs.m { 366 // Create a UI for the machine readable stuff to be targeted 367 ui := &packer.TargetedUI{ 368 Target: name, 369 Ui: c.Ui, 370 } 371 372 ui.Machine("error", err.Error()) 373 374 c.Ui.Error(fmt.Sprintf("--> %s: %s", name, err)) 375 } 376 } 377 378 if len(artifacts.m) > 0 { 379 c.Ui.Say("\n==> Builds finished. The artifacts of successful builds are:") 380 for name, buildArtifacts := range artifacts.m { 381 // Create a UI for the machine readable stuff to be targeted 382 ui := &packer.TargetedUI{ 383 Target: name, 384 Ui: c.Ui, 385 } 386 387 // Machine-readable helpful 388 ui.Machine("artifact-count", strconv.FormatInt(int64(len(buildArtifacts)), 10)) 389 390 for i, artifact := range buildArtifacts { 391 var message bytes.Buffer 392 fmt.Fprintf(&message, "--> %s: ", name) 393 394 if artifact != nil { 395 fmt.Fprint(&message, artifact.String()) 396 } else { 397 fmt.Fprint(&message, "<nothing>") 398 } 399 400 iStr := strconv.FormatInt(int64(i), 10) 401 if artifact != nil { 402 ui.Machine("artifact", iStr, "builder-id", artifact.BuilderId()) 403 ui.Machine("artifact", iStr, "id", artifact.Id()) 404 ui.Machine("artifact", iStr, "string", artifact.String()) 405 406 files := artifact.Files() 407 ui.Machine("artifact", 408 iStr, 409 "files-count", strconv.FormatInt(int64(len(files)), 10)) 410 for fi, file := range files { 411 fiStr := strconv.FormatInt(int64(fi), 10) 412 ui.Machine("artifact", iStr, "file", fiStr, file) 413 } 414 } else { 415 ui.Machine("artifact", iStr, "nil") 416 } 417 418 ui.Machine("artifact", iStr, "end") 419 c.Ui.Say(message.String()) 420 421 } 422 423 } 424 } else { 425 c.Ui.Say("\n==> Builds finished but no artifacts were created.") 426 } 427 428 if len(errs.m) > 0 { 429 // If any errors occurred, exit with a non-zero exit status 430 ret = 1 431 } 432 433 return ret 434 } 435 436 func (*BuildCommand) Help() string { 437 helpText := ` 438 Usage: packer build [options] TEMPLATE 439 440 Will execute multiple builds in parallel as defined in the template. 441 The various artifacts created by the template will be outputted. 442 443 Options: 444 445 -color=false Disable color output. (Default: color) 446 -debug Debug mode enabled for builds. 447 -except=foo,bar,baz Run all builds and post-processors other than these. 448 -only=foo,bar,baz Build only the specified builds. 449 -force Force a build to continue if artifacts exist, deletes existing artifacts. 450 -machine-readable Produce machine-readable output. 451 -on-error=[cleanup|abort|ask|run-cleanup-provisioner] If the build fails do: clean up (default), abort, ask, or run-cleanup-provisioner. 452 -parallel-builds=1 Number of builds to run in parallel. 1 disables parallelization. 0 means no limit (Default: 0) 453 -timestamp-ui Enable prefixing of each ui output with an RFC3339 timestamp. 454 -var 'key=value' Variable for templates, can be used multiple times. 455 -var-file=path JSON or HCL2 file containing user variables, can be used multiple times. 456 -warn-on-undeclared-var Display warnings for user variable files containing undeclared variables. 457 -ignore-prerelease-plugins Disable the loading of prerelease plugin binaries (x.y.z-dev). 458 -use-sequential-evaluation Fallback to using a sequential approach for local/datasource evaluation. 459 ` 460 461 return strings.TrimSpace(helpText) 462 } 463 464 func (*BuildCommand) Synopsis() string { 465 return "build image(s) from template" 466 } 467 468 func (*BuildCommand) AutocompleteArgs() complete.Predictor { 469 return complete.PredictNothing 470 } 471 472 func (*BuildCommand) AutocompleteFlags() complete.Flags { 473 return complete.Flags{ 474 "-color": complete.PredictNothing, 475 "-debug": complete.PredictNothing, 476 "-except": complete.PredictNothing, 477 "-only": complete.PredictNothing, 478 "-force": complete.PredictNothing, 479 "-machine-readable": complete.PredictNothing, 480 "-on-error": complete.PredictNothing, 481 "-parallel": complete.PredictNothing, 482 "-timestamp-ui": complete.PredictNothing, 483 "-var": complete.PredictNothing, 484 "-var-file": complete.PredictNothing, 485 } 486 }