github.com/hashicorp/packer@v1.14.3/main.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  // This is the main package for the `packer` application.
     5  
     6  //go:generate go run ./scripts/generate-plugins.go
     7  package main
     8  
     9  import (
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"math/rand"
    14  	"os"
    15  	"runtime"
    16  	"sync"
    17  	"syscall"
    18  	"time"
    19  
    20  	"github.com/hashicorp/go-uuid"
    21  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    22  	"github.com/hashicorp/packer-plugin-sdk/pathing"
    23  	pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
    24  	"github.com/hashicorp/packer-plugin-sdk/tmp"
    25  	"github.com/hashicorp/packer/command"
    26  	"github.com/hashicorp/packer/packer"
    27  	"github.com/hashicorp/packer/version"
    28  	"github.com/mitchellh/cli"
    29  	"github.com/mitchellh/panicwrap"
    30  	"github.com/mitchellh/prefixedio"
    31  )
    32  
    33  func main() {
    34  	// Call realMain instead of doing the work here so we can use
    35  	// `defer` statements within the function and have them work properly.
    36  	// (defers aren't called with os.Exit)
    37  	os.Exit(realMain())
    38  }
    39  
    40  // realMain is executed from main and returns the exit status to exit with.
    41  func realMain() int {
    42  	var wrapConfig panicwrap.WrapConfig
    43  	// When following env variable is set, packer
    44  	// won't panic wrap itself as it's already wrapped.
    45  	// i.e.: when terraform runs it.
    46  	wrapConfig.CookieKey = "PACKER_WRAP_COOKIE"
    47  	wrapConfig.CookieValue = "49C22B1A-3A93-4C98-97FA-E07D18C787B5"
    48  
    49  	if inPlugin() || panicwrap.Wrapped(&wrapConfig) {
    50  		// Call the real main
    51  		return wrappedMain()
    52  	}
    53  
    54  	// Generate a UUID for this packer run and pass it to the environment.
    55  	// GenerateUUID always returns a nil error (based on rand.Read) so we'll
    56  	// just ignore it.
    57  	UUID, _ := uuid.GenerateUUID()
    58  	os.Setenv("PACKER_RUN_UUID", UUID)
    59  
    60  	// Determine where logs should go in general (requested by the user)
    61  	logWriter, err := logOutput()
    62  	if err != nil {
    63  		fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
    64  		return 1
    65  	}
    66  	if logWriter == nil {
    67  		logWriter = io.Discard
    68  	}
    69  
    70  	packersdk.LogSecretFilter.SetOutput(logWriter)
    71  
    72  	// Disable logging here
    73  	log.SetOutput(io.Discard)
    74  
    75  	// We always send logs to a temporary file that we use in case
    76  	// there is a panic. Otherwise, we delete it.
    77  	logTempFile, err := tmp.File("packer-log")
    78  	if err != nil {
    79  		fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
    80  		return 1
    81  	}
    82  	defer os.Remove(logTempFile.Name())
    83  	defer logTempFile.Close()
    84  
    85  	// Setup the prefixed readers that send data properly to
    86  	// stdout/stderr.
    87  	doneCh := make(chan struct{})
    88  	outR, outW := io.Pipe()
    89  	go copyOutput(outR, doneCh)
    90  
    91  	// Enable checkpoint for panic reporting
    92  	if config, _ := loadConfig(); config != nil && !config.DisableCheckpoint {
    93  		packer.CheckpointReporter = packer.NewCheckpointReporter(
    94  			config.DisableCheckpointSignature,
    95  		)
    96  	}
    97  
    98  	// Create the configuration for panicwrap and wrap our executable
    99  	wrapConfig.Handler = panicHandler(logTempFile)
   100  	wrapConfig.Writer = io.MultiWriter(logTempFile, &packersdk.LogSecretFilter)
   101  	wrapConfig.Stdout = outW
   102  	wrapConfig.DetectDuration = 500 * time.Millisecond
   103  	wrapConfig.ForwardSignals = []os.Signal{syscall.SIGTERM}
   104  	exitStatus, err := panicwrap.Wrap(&wrapConfig)
   105  	if err != nil {
   106  		fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
   107  		return 1
   108  	}
   109  
   110  	// If >= 0, we're the parent, so just exit
   111  	if exitStatus >= 0 {
   112  		// Close the stdout writer so that our copy process can finish
   113  		outW.Close()
   114  
   115  		// Wait for the output copying to finish
   116  		<-doneCh
   117  
   118  		return exitStatus
   119  	}
   120  
   121  	// We're the child, so just close the tempfile we made in order to
   122  	// save file handles since the tempfile is only used by the parent.
   123  	logTempFile.Close()
   124  
   125  	return 0
   126  }
   127  
   128  // wrappedMain is called only when we're wrapped by panicwrap and
   129  // returns the exit status to exit with.
   130  func wrappedMain() int {
   131  	// WARNING: WrappedMain causes unexpected behaviors when writing to stderr
   132  	// and stdout.  Anything in this function written to stderr will be captured
   133  	// by the logger, but will not be written to the terminal. Anything in
   134  	// this function written to standard out must be prefixed with ErrorPrefix
   135  	// or OutputPrefix to be sent to the right terminal stream, but adding
   136  	// these prefixes can cause nondeterministic results for output from
   137  	// other, asynchronous methods. Try to avoid modifying output in this
   138  	// function if at all possible.
   139  
   140  	// If there is no explicit number of Go threads to use, then set it
   141  	if os.Getenv("GOMAXPROCS") == "" {
   142  		runtime.GOMAXPROCS(runtime.NumCPU())
   143  	}
   144  
   145  	packersdk.LogSecretFilter.SetOutput(os.Stderr)
   146  	log.SetOutput(&packersdk.LogSecretFilter)
   147  
   148  	inPlugin := inPlugin()
   149  	if inPlugin {
   150  		// This prevents double-logging timestamps
   151  		log.SetFlags(0)
   152  	}
   153  
   154  	log.Printf("[INFO] Packer version: %s [%s %s %s]",
   155  		version.FormattedVersion(),
   156  		runtime.Version(),
   157  		runtime.GOOS, runtime.GOARCH)
   158  
   159  	// The config being loaded here is the Packer config -- it defines
   160  	// the location of third party builder plugins, plugin ports to use, and
   161  	// whether to disable telemetry. It is a global config.
   162  	// Do not confuse this config with the .json Packer template which gets
   163  	// passed into commands like `packer build`
   164  	config, err := loadConfig()
   165  	if err != nil {
   166  		// Writing to Stdout here so that the error message bypasses panicwrap. By using the
   167  		// ErrorPrefix this output will be redirected to Stderr by the copyOutput func.
   168  		// TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout
   169  		// without panicwrap
   170  		fmt.Fprintf(os.Stdout, "%s Error loading configuration: \n\n%s\n", ErrorPrefix, err)
   171  		return 1
   172  	}
   173  
   174  	// Fire off the checkpoint.
   175  	go runCheckpoint(config)
   176  	if !config.DisableCheckpoint {
   177  		packer.CheckpointReporter = packer.NewCheckpointReporter(
   178  			config.DisableCheckpointSignature,
   179  		)
   180  	}
   181  
   182  	cacheDir, err := packersdk.CachePath()
   183  	if err != nil {
   184  		// Writing to Stdout here so that the error message bypasses panicwrap. By using the
   185  		// ErrorPrefix this output will be redirected to Stderr by the copyOutput func.
   186  		// TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout
   187  		// without panicwrap
   188  		fmt.Fprintf(os.Stdout, "%s Error preparing cache directory: \n\n%s\n", ErrorPrefix, err)
   189  		return 1
   190  	}
   191  	log.Printf("[INFO] Setting cache directory: %s", cacheDir)
   192  
   193  	// Determine if we're in machine-readable mode by mucking around with
   194  	// the arguments...
   195  	args, machineReadable := extractMachineReadable(os.Args[1:])
   196  
   197  	defer packer.CleanupClients()
   198  
   199  	var ui packersdk.Ui
   200  	if machineReadable {
   201  		// Setup the UI as we're being machine-readable
   202  		ui = &packer.MachineReadableUi{
   203  			Writer: os.Stdout,
   204  		}
   205  
   206  		// Set this so that we don't get colored output in our machine-
   207  		// readable UI.
   208  		if err := os.Setenv("PACKER_NO_COLOR", "1"); err != nil {
   209  			// Outputting error using Ui here to conform to the machine readable format.
   210  			ui.Error(fmt.Sprintf("Packer failed to initialize UI: %s\n", err))
   211  			return 1
   212  		}
   213  	} else {
   214  		basicUi := &packersdk.BasicUi{
   215  			Reader:      os.Stdin,
   216  			Writer:      os.Stdout,
   217  			ErrorWriter: os.Stdout,
   218  			PB:          &packersdk.NoopProgressTracker{},
   219  		}
   220  		ui = basicUi
   221  		if !inPlugin {
   222  			currentPID := os.Getpid()
   223  			backgrounded, err := checkProcess(currentPID)
   224  			if err != nil {
   225  				// Writing to Stderr will ensure that the output gets captured by panicwrap.
   226  				// This error message and any other message writing to Stderr after this point will only show up with PACKER_LOG=1
   227  				// TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout without panicwrap.
   228  				fmt.Fprintf(os.Stderr, "%s cannot determine if process is in background: %s\n", ErrorPrefix, err)
   229  			}
   230  
   231  			if backgrounded {
   232  				fmt.Fprintf(os.Stderr, "%s Running in background, not using a TTY\n", ErrorPrefix)
   233  			} else if TTY, err := openTTY(); err != nil {
   234  				fmt.Fprintf(os.Stderr, "%s No tty available: %s\n", ErrorPrefix, err)
   235  			} else {
   236  				basicUi.TTY = TTY
   237  				basicUi.PB = &packer.UiProgressBar{}
   238  				defer TTY.Close()
   239  			}
   240  		}
   241  	}
   242  	// Create the CLI meta
   243  	CommandMeta = &command.Meta{
   244  		CoreConfig: &packer.CoreConfig{
   245  			Components: packer.ComponentFinder{
   246  				Hook:         config.StarHook,
   247  				PluginConfig: config.Plugins,
   248  			},
   249  			Version: version.Version,
   250  		},
   251  		Ui: ui,
   252  	}
   253  
   254  	//versionCLIHelper shortcuts "--version" and "-v" to just show the version
   255  	versionCLIHelper := &cli.CLI{
   256  		Args:    args,
   257  		Version: version.Version,
   258  	}
   259  	if versionCLIHelper.IsVersion() && versionCLIHelper.Version != "" {
   260  		// by default version flags ignore all other args so there is no need to persist the original args.
   261  		args = []string{"version"}
   262  	}
   263  
   264  	cli := &cli.CLI{
   265  		Args:         args,
   266  		Autocomplete: true,
   267  		Commands:     Commands,
   268  		HelpFunc:     excludeHelpFunc(Commands, []string{"execute", "plugin"}),
   269  		HelpWriter:   os.Stdout,
   270  		Name:         "packer",
   271  		Version:      version.Version,
   272  	}
   273  
   274  	exitCode, err := cli.Run()
   275  	if !inPlugin {
   276  		if err := packer.CheckpointReporter.Finalize(cli.Subcommand(), exitCode, err); err != nil {
   277  			log.Printf("[WARN] (telemetry) Error finalizing report. This is safe to ignore. %s", err.Error())
   278  		}
   279  	}
   280  
   281  	if err != nil {
   282  		// Writing to Stdout here so that the error message bypasses panicwrap. By using the
   283  		// ErrorPrefix this output will be redirected to Stderr by the copyOutput func.
   284  		// TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout
   285  		// without panicwrap
   286  		fmt.Fprintf(os.Stdout, "%s Error executing CLI: %s\n", ErrorPrefix, err)
   287  		return 1
   288  	}
   289  
   290  	return exitCode
   291  }
   292  
   293  // excludeHelpFunc filters commands we don't want to show from the list of
   294  // commands displayed in packer's help text.
   295  func excludeHelpFunc(commands map[string]cli.CommandFactory, exclude []string) cli.HelpFunc {
   296  	// Make search slice into a map so we can use use the `if found` idiom
   297  	// instead of a nested loop.
   298  	var excludes = make(map[string]interface{}, len(exclude))
   299  	for _, item := range exclude {
   300  		excludes[item] = nil
   301  	}
   302  
   303  	// Create filtered list of commands
   304  	helpCommands := []string{}
   305  	for command := range commands {
   306  		if _, found := excludes[command]; !found {
   307  			helpCommands = append(helpCommands, command)
   308  		}
   309  	}
   310  
   311  	return cli.FilteredHelpFunc(helpCommands, cli.BasicHelpFunc("packer"))
   312  }
   313  
   314  // extractMachineReadable checks the args for the machine readable
   315  // flag and returns whether or not it is on. It modifies the args
   316  // to remove this flag.
   317  func extractMachineReadable(args []string) ([]string, bool) {
   318  	for i, arg := range args {
   319  		if arg == "-machine-readable" {
   320  			// We found it. Slice it out.
   321  			result := make([]string, len(args)-1)
   322  			copy(result, args[:i])
   323  			copy(result[i:], args[i+1:])
   324  			return result, true
   325  		}
   326  	}
   327  
   328  	return args, false
   329  }
   330  
   331  func loadConfig() (*config, error) {
   332  	pluginDir, err := packer.PluginFolder()
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	var config config
   338  	config.Plugins = &packer.PluginConfig{
   339  		PluginMinPort:   10000,
   340  		PluginMaxPort:   25000,
   341  		PluginDirectory: pluginDir,
   342  		Builders:        packer.MapOfBuilder{},
   343  		Provisioners:    packer.MapOfProvisioner{},
   344  		PostProcessors:  packer.MapOfPostProcessor{},
   345  		DataSources:     packer.MapOfDatasource{},
   346  	}
   347  
   348  	// Finally, try to use an internal plugin. Note that this will not override
   349  	// any previously-loaded plugins.
   350  	if err := config.discoverInternalComponents(); err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	// start by loading from PACKER_CONFIG if available
   355  	configFilePath := os.Getenv("PACKER_CONFIG")
   356  	if configFilePath == "" {
   357  		var err error
   358  		log.Print("[INFO] PACKER_CONFIG env var not set; checking the default config file path")
   359  		configFilePath, err = pathing.ConfigFile()
   360  		if err != nil {
   361  			log.Printf("Error detecting default config file path: %s", err)
   362  		}
   363  	}
   364  	if configFilePath == "" {
   365  		return &config, nil
   366  	}
   367  	log.Printf("[INFO] PACKER_CONFIG env var set; attempting to open config file: %s", configFilePath)
   368  	f, err := os.Open(configFilePath)
   369  	if err != nil {
   370  		if !os.IsNotExist(err) {
   371  			return nil, err
   372  		}
   373  
   374  		log.Printf("[WARN] Config file doesn't exist: %s", configFilePath)
   375  		return &config, nil
   376  	}
   377  	defer f.Close()
   378  
   379  	// This loads a json config, defined in packer/config.go
   380  	if err := decodeConfig(f, &config); err != nil {
   381  		return nil, err
   382  	}
   383  
   384  	if err := config.LoadExternalComponentsFromConfig(); err != nil {
   385  		return nil, fmt.Errorf("%s: %s", configFilePath, err)
   386  	}
   387  
   388  	return &config, nil
   389  }
   390  
   391  // copyOutput uses output prefixes to determine whether data on stdout
   392  // should go to stdout or stderr. This is due to panicwrap using stderr
   393  // as the log and error channel.
   394  func copyOutput(r io.Reader, doneCh chan<- struct{}) {
   395  	defer close(doneCh)
   396  
   397  	pr, err := prefixedio.NewReader(r)
   398  	if err != nil {
   399  		panic(err)
   400  	}
   401  
   402  	stderrR, err := pr.Prefix(ErrorPrefix)
   403  	if err != nil {
   404  		panic(err)
   405  	}
   406  	stdoutR, err := pr.Prefix(OutputPrefix)
   407  	if err != nil {
   408  		panic(err)
   409  	}
   410  	defaultR, err := pr.Prefix("")
   411  	if err != nil {
   412  		panic(err)
   413  	}
   414  
   415  	var wg sync.WaitGroup
   416  	wg.Add(3)
   417  	go func() {
   418  		defer wg.Done()
   419  		_, _ = io.Copy(os.Stderr, stderrR)
   420  	}()
   421  	go func() {
   422  		defer wg.Done()
   423  		_, _ = io.Copy(os.Stdout, stdoutR)
   424  	}()
   425  	go func() {
   426  		defer wg.Done()
   427  		_, _ = io.Copy(os.Stdout, defaultR)
   428  	}()
   429  
   430  	wg.Wait()
   431  }
   432  
   433  func inPlugin() bool {
   434  	return os.Getenv(pluginsdk.MagicCookieKey) == pluginsdk.MagicCookieValue
   435  }
   436  
   437  func init() {
   438  	// Seed the random number generator
   439  	rand.Seed(time.Now().UTC().UnixNano())
   440  }