// This is the main package for the `packer` application. //go:generate go run ./scripts/generate-plugins.go package main import ( "fmt" "io" "io/ioutil" "log" "math/rand" "os" "runtime" "sync" "syscall" "time" "github.com/hashicorp/go-uuid" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/pathing" pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer-plugin-sdk/tmp" "github.com/hashicorp/packer/command" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/version" "github.com/mitchellh/cli" "github.com/mitchellh/panicwrap" "github.com/mitchellh/prefixedio" ) func main() { // Call realMain instead of doing the work here so we can use // `defer` statements within the function and have them work properly. // (defers aren't called with os.Exit) os.Exit(realMain()) } // realMain is executed from main and returns the exit status to exit with. func realMain() int { var wrapConfig panicwrap.WrapConfig // When following env variable is set, packer // wont panic wrap itself as it's already wrapped. // i.e.: when terraform runs it. wrapConfig.CookieKey = "PACKER_WRAP_COOKIE" wrapConfig.CookieValue = "49C22B1A-3A93-4C98-97FA-E07D18C787B5" if inPlugin() || panicwrap.Wrapped(&wrapConfig) { // Call the real main return wrappedMain() } // Generate a UUID for this packer run and pass it to the environment. // GenerateUUID always returns a nil error (based on rand.Read) so we'll // just ignore it. UUID, _ := uuid.GenerateUUID() os.Setenv("PACKER_RUN_UUID", UUID) // Determine where logs should go in general (requested by the user) logWriter, err := logOutput() if err != nil { fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err) return 1 } if logWriter == nil { logWriter = ioutil.Discard } packersdk.LogSecretFilter.SetOutput(logWriter) // Disable logging here log.SetOutput(ioutil.Discard) // We always send logs to a temporary file that we use in case // there is a panic. Otherwise, we delete it. logTempFile, err := tmp.File("packer-log") if err != nil { fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) return 1 } defer os.Remove(logTempFile.Name()) defer logTempFile.Close() // Setup the prefixed readers that send data properly to // stdout/stderr. doneCh := make(chan struct{}) outR, outW := io.Pipe() go copyOutput(outR, doneCh) // Enable checkpoint for panic reporting if config, _ := loadConfig(); config != nil && !config.DisableCheckpoint { packer.CheckpointReporter = packer.NewCheckpointReporter( config.DisableCheckpointSignature, ) } // Create the configuration for panicwrap and wrap our executable wrapConfig.Handler = panicHandler(logTempFile) wrapConfig.Writer = io.MultiWriter(logTempFile, &packersdk.LogSecretFilter) wrapConfig.Stdout = outW wrapConfig.DetectDuration = 500 * time.Millisecond wrapConfig.ForwardSignals = []os.Signal{syscall.SIGTERM} exitStatus, err := panicwrap.Wrap(&wrapConfig) if err != nil { fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err) return 1 } // If >= 0, we're the parent, so just exit if exitStatus >= 0 { // Close the stdout writer so that our copy process can finish outW.Close() // Wait for the output copying to finish <-doneCh return exitStatus } // We're the child, so just close the tempfile we made in order to // save file handles since the tempfile is only used by the parent. logTempFile.Close() return 0 } // wrappedMain is called only when we're wrapped by panicwrap and // returns the exit status to exit with. func wrappedMain() int { // WARNING: WrappedMain causes unexpected behaviors when writing to stderr // and stdout. Anything in this function written to stderr will be captured // by the logger, but will not be written to the terminal. Anything in // this function written to standard out must be prefixed with ErrorPrefix // or OutputPrefix to be sent to the right terminal stream, but adding // these prefixes can cause nondeterministic results for output from // other, asynchronous methods. Try to avoid modifying output in this // function if at all possible. // If there is no explicit number of Go threads to use, then set it if os.Getenv("GOMAXPROCS") == "" { runtime.GOMAXPROCS(runtime.NumCPU()) } packersdk.LogSecretFilter.SetOutput(os.Stderr) log.SetOutput(&packersdk.LogSecretFilter) inPlugin := inPlugin() if inPlugin { // This prevents double-logging timestamps log.SetFlags(0) } log.Printf("[INFO] Packer version: %s [%s %s %s]", version.FormattedVersion(), runtime.Version(), runtime.GOOS, runtime.GOARCH) // The config being loaded here is the Packer config -- it defines // the location of third party builder plugins, plugin ports to use, and // whether to disable telemetry. It is a global config. // Do not confuse this config with the .json Packer template which gets // passed into commands like `packer build` config, err := loadConfig() if err != nil { // Writing to Stdout here so that the error message bypasses panicwrap. By using the // ErrorPrefix this output will be redirected to Stderr by the copyOutput func. // TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout // without panicwrap fmt.Fprintf(os.Stdout, "%s Error loading configuration: \n\n%s\n", ErrorPrefix, err) return 1 } // Fire off the checkpoint. go runCheckpoint(config) if !config.DisableCheckpoint { packer.CheckpointReporter = packer.NewCheckpointReporter( config.DisableCheckpointSignature, ) } cacheDir, err := packersdk.CachePath() if err != nil { // Writing to Stdout here so that the error message bypasses panicwrap. By using the // ErrorPrefix this output will be redirected to Stderr by the copyOutput func. // TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout // without panicwrap fmt.Fprintf(os.Stdout, "%s Error preparing cache directory: \n\n%s\n", ErrorPrefix, err) return 1 } log.Printf("[INFO] Setting cache directory: %s", cacheDir) // Determine if we're in machine-readable mode by mucking around with // the arguments... args, machineReadable := extractMachineReadable(os.Args[1:]) defer packer.CleanupClients() var ui packersdk.Ui if machineReadable { // Setup the UI as we're being machine-readable ui = &packer.MachineReadableUi{ Writer: os.Stdout, } // Set this so that we don't get colored output in our machine- // readable UI. if err := os.Setenv("PACKER_NO_COLOR", "1"); err != nil { // Outputting error using Ui here to conform to the machine readable format. ui.Error(fmt.Sprintf("Packer failed to initialize UI: %s\n", err)) return 1 } } else { basicUi := &packersdk.BasicUi{ Reader: os.Stdin, Writer: os.Stdout, ErrorWriter: os.Stdout, PB: &packersdk.NoopProgressTracker{}, } ui = basicUi if !inPlugin { currentPID := os.Getpid() backgrounded, err := checkProcess(currentPID) if err != nil { // Writing to Stderr will ensure that the output gets captured by panicwrap. // This error message and any other message writing to Stderr after this point will only show up with PACKER_LOG=1 // TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout without panicwrap. fmt.Fprintf(os.Stderr, "%s cannot determine if process is in background: %s\n", ErrorPrefix, err) } if backgrounded { fmt.Fprintf(os.Stderr, "%s Running in background, not using a TTY\n", ErrorPrefix) } else if TTY, err := openTTY(); err != nil { fmt.Fprintf(os.Stderr, "%s No tty available: %s\n", ErrorPrefix, err) } else { basicUi.TTY = TTY basicUi.PB = &packer.UiProgressBar{} defer TTY.Close() } } } // Create the CLI meta CommandMeta = &command.Meta{ CoreConfig: &packer.CoreConfig{ Components: packer.ComponentFinder{ Hook: config.StarHook, PluginConfig: config.Plugins, }, Version: version.Version, }, Ui: ui, } cli := &cli.CLI{ Args: args, Autocomplete: true, Commands: Commands, HelpFunc: excludeHelpFunc(Commands, []string{"plugin"}), HelpWriter: os.Stdout, Name: "packer", Version: version.Version, } exitCode, err := cli.Run() if !inPlugin { if err := packer.CheckpointReporter.Finalize(cli.Subcommand(), exitCode, err); err != nil { log.Printf("[WARN] (telemetry) Error finalizing report. This is safe to ignore. %s", err.Error()) } } if err != nil { // Writing to Stdout here so that the error message bypasses panicwrap. By using the // ErrorPrefix this output will be redirected to Stderr by the copyOutput func. // TODO: nywilken need to revisit this setup to better output errors to Stderr, and output to Stdout // without panicwrap fmt.Fprintf(os.Stdout, "%s Error executing CLI: %s\n", ErrorPrefix, err) return 1 } return exitCode } // excludeHelpFunc filters commands we don't want to show from the list of // commands displayed in packer's help text. func excludeHelpFunc(commands map[string]cli.CommandFactory, exclude []string) cli.HelpFunc { // Make search slice into a map so we can use use the `if found` idiom // instead of a nested loop. var excludes = make(map[string]interface{}, len(exclude)) for _, item := range exclude { excludes[item] = nil } // Create filtered list of commands helpCommands := []string{} for command := range commands { if _, found := excludes[command]; !found { helpCommands = append(helpCommands, command) } } return cli.FilteredHelpFunc(helpCommands, cli.BasicHelpFunc("packer")) } // extractMachineReadable checks the args for the machine readable // flag and returns whether or not it is on. It modifies the args // to remove this flag. func extractMachineReadable(args []string) ([]string, bool) { for i, arg := range args { if arg == "-machine-readable" { // We found it. Slice it out. result := make([]string, len(args)-1) copy(result, args[:i]) copy(result[i:], args[i+1:]) return result, true } } return args, false } func loadConfig() (*config, error) { var config config config.Plugins = &packer.PluginConfig{ PluginMinPort: 10000, PluginMaxPort: 25000, KnownPluginFolders: packer.PluginFolders("."), // BuilderRedirects BuilderRedirects: map[string]string{ // "amazon-chroot": "github.com/hashicorp/amazon", // "amazon-ebs": "github.com/hashicorp/amazon", // "amazon-ebssurrogate": "github.com/hashicorp/amazon", // "amazon-ebsvolume": "github.com/hashicorp/amazon", // "amazon-instance": "github.com/hashicorp/amazon", // "azure-arm": "github.com/hashicorp/azure", // "azure-chroot": "github.com/hashicorp/azure", // "dtl": "github.com/hashicorp/azure", // "docker": "github.com/hashicorp/docker", // "googlecompute": "github.com/hashicorp/googlecompute", // "parallels-iso": "github.com/hashicorp/parallels", // "parallels-pvm": "github.com/hashicorp/parallels", // "qemu": "github.com/hashicorp/qemu", // "vagrant": "github.com/hashicorp/vagrant", // "virtualbox-iso": "github.com/hashicorp/virtualbox", // "virtualbox-ovf": "github.com/hashicorp/virtualbox", // "virtualbox-vm": "github.com/hashicorp/virtualbox", // "vmware-iso": "github.com/hashicorp/vmware", // "vmware-vmx": "github.com/hashicorp/vmware", // "vsphere-iso": "github.com/hashicorp/vsphere", // "vsphere-clone": "github.com/hashicorp/vsphere", }, DatasourceRedirects: map[string]string{ // "amazon-ami": "github.com/hashicorp/amazon", }, ProvisionerRedirects: map[string]string{ // "ansible": "github.com/hashicorp/ansible", // "ansible-local": "github.com/hashicorp/ansible", // "azure-dtlartifact": "github.com/hashicorp/azure", }, PostProcessorRedirects: map[string]string{ // "amazon-import": "github.com/hashicorp/amazon", // "docker-import": "github.com/hashicorp/docker", // "docker-push": "github.com/hashicorp/docker", // "docker-save": "github.com/hashicorp/docker", // "docker-tag": "github.com/hashicorp/docker", // "googlecompute-export": "github.com/hashicorp/googlecompute", // "googlecompute-import": "github.com/hashicorp/googlecompute", // "vagrant": "github.com/hashicorp/vagrant", // "vagrant-cloud": "github.com/hashicorp/vagrant", // "vsphere": "github.com/hashicorp/vsphere", // "vsphere-template": "github.com/hashicorp/vsphere", }, } if err := config.Plugins.Discover(); err != nil { return nil, err } // Finally, try to use an internal plugin. Note that this will not override // any previously-loaded plugins. if err := config.discoverInternalComponents(); err != nil { return nil, err } // start by loading from PACKER_CONFIG if available configFilePath := os.Getenv("PACKER_CONFIG") if configFilePath == "" { var err error log.Print("[INFO] PACKER_CONFIG env var not set; checking the default config file path") configFilePath, err = pathing.ConfigFile() if err != nil { log.Printf("Error detecting default config file path: %s", err) } } if configFilePath == "" { return &config, nil } log.Printf("[INFO] PACKER_CONFIG env var set; attempting to open config file: %s", configFilePath) f, err := os.Open(configFilePath) if err != nil { if !os.IsNotExist(err) { return nil, err } log.Printf("[WARN] Config file doesn't exist: %s", configFilePath) return &config, nil } defer f.Close() // This loads a json config, defined in packer/config.go if err := decodeConfig(f, &config); err != nil { return nil, err } config.LoadExternalComponentsFromConfig() return &config, nil } // copyOutput uses output prefixes to determine whether data on stdout // should go to stdout or stderr. This is due to panicwrap using stderr // as the log and error channel. func copyOutput(r io.Reader, doneCh chan<- struct{}) { defer close(doneCh) pr, err := prefixedio.NewReader(r) if err != nil { panic(err) } stderrR, err := pr.Prefix(ErrorPrefix) if err != nil { panic(err) } stdoutR, err := pr.Prefix(OutputPrefix) if err != nil { panic(err) } defaultR, err := pr.Prefix("") if err != nil { panic(err) } var wg sync.WaitGroup wg.Add(3) go func() { defer wg.Done() io.Copy(os.Stderr, stderrR) }() go func() { defer wg.Done() io.Copy(os.Stdout, stdoutR) }() go func() { defer wg.Done() io.Copy(os.Stdout, defaultR) }() wg.Wait() } func inPlugin() bool { return os.Getenv(pluginsdk.MagicCookieKey) == pluginsdk.MagicCookieValue } func init() { // Seed the random number generator rand.Seed(time.Now().UTC().UnixNano()) }