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, &registry.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  }