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 }