This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
383 lines
9.8 KiB
Go
383 lines
9.8 KiB
Go
// This is the main package for the `packer` application.
|
|
|
|
//go:generate go run ./scripts/generate-plugins.go
|
|
//go:generate go generate ./common/bootcommand/...
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/packer/command"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/hashicorp/packer/packer/plugin"
|
|
"github.com/hashicorp/packer/packer/tmp"
|
|
"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 !panicwrap.Wrapped(&wrapConfig) {
|
|
// 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
|
|
}
|
|
|
|
packer.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()
|
|
|
|
// Tell the logger to log to this file
|
|
os.Setenv(EnvLog, "")
|
|
os.Setenv(EnvLogFile, "")
|
|
|
|
// 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, &packer.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()
|
|
}
|
|
|
|
// Call the real main
|
|
return wrappedMain()
|
|
}
|
|
|
|
// wrappedMain is called only when we're wrapped by panicwrap and
|
|
// returns the exit status to exit with.
|
|
func wrappedMain() int {
|
|
// If there is no explicit number of Go threads to use, then set it
|
|
if os.Getenv("GOMAXPROCS") == "" {
|
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
}
|
|
|
|
packer.LogSecretFilter.SetOutput(os.Stderr)
|
|
log.SetOutput(&packer.LogSecretFilter)
|
|
|
|
inPlugin := os.Getenv(plugin.MagicCookieKey) == plugin.MagicCookieValue
|
|
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)
|
|
|
|
config, err := loadConfig()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
|
|
return 1
|
|
}
|
|
|
|
// Fire off the checkpoint.
|
|
go runCheckpoint(config)
|
|
if !config.DisableCheckpoint {
|
|
packer.CheckpointReporter = packer.NewCheckpointReporter(
|
|
config.DisableCheckpointSignature,
|
|
)
|
|
}
|
|
|
|
cacheDir, err := packer.CachePath()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
|
return 1
|
|
}
|
|
log.Printf("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 plugin.CleanupClients()
|
|
|
|
var ui packer.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 {
|
|
fmt.Fprintf(os.Stderr, "Packer failed to initialize UI: %s\n", err)
|
|
return 1
|
|
}
|
|
} else {
|
|
basicUi := &packer.BasicUi{
|
|
Reader: os.Stdin,
|
|
Writer: os.Stdout,
|
|
ErrorWriter: os.Stdout,
|
|
}
|
|
ui = basicUi
|
|
if !inPlugin {
|
|
currentPID := os.Getpid()
|
|
backgrounded, err := checkProcess(currentPID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "cannot determine if process is in "+
|
|
"background: %s\n", err)
|
|
}
|
|
if backgrounded {
|
|
fmt.Fprint(os.Stderr, "Running in background, not using a TTY\n")
|
|
} else if TTY, err := openTTY(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "No tty available: %s\n", err)
|
|
} else {
|
|
basicUi.TTY = TTY
|
|
defer TTY.Close()
|
|
}
|
|
}
|
|
}
|
|
// Create the CLI meta
|
|
CommandMeta = &command.Meta{
|
|
CoreConfig: &packer.CoreConfig{
|
|
Components: packer.ComponentFinder{
|
|
Hook: config.StarHook,
|
|
|
|
BuilderStore: config.Builders,
|
|
ProvisionerStore: config.Provisioners,
|
|
PostProcessorStore: config.PostProcessors,
|
|
},
|
|
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 {
|
|
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", 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.PluginMinPort = 10000
|
|
config.PluginMaxPort = 25000
|
|
config.Builders = packer.MapOfBuilder{}
|
|
config.PostProcessors = packer.MapOfPostProcessor{}
|
|
config.Provisioners = packer.MapOfProvisioner{}
|
|
if err := config.Discover(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configFilePath := os.Getenv("PACKER_CONFIG")
|
|
if configFilePath != "" {
|
|
log.Printf("'PACKER_CONFIG' set, loading config from environment.")
|
|
} else {
|
|
var err error
|
|
configFilePath, err = packer.ConfigFile()
|
|
|
|
if err != nil {
|
|
log.Printf("Error detecting default config file path: %s", err)
|
|
}
|
|
}
|
|
|
|
if configFilePath == "" {
|
|
return &config, nil
|
|
}
|
|
|
|
log.Printf("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()
|
|
|
|
if err := decodeConfig(f, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 init() {
|
|
// Seed the random number generator
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
}
|