Files
Packer-Cn/command/build.go
T
Adrien Delorme 0785c2f6fc build using HCL2 (#8423)
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
}
```
2019-12-17 11:25:56 +01:00

449 lines
12 KiB
Go

package command
import (
"bytes"
"context"
"fmt"
"log"
"math"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/packer/hcl2template"
"github.com/hashicorp/packer/helper/enumflag"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template"
"golang.org/x/sync/semaphore"
"github.com/posener/complete"
)
type BuildCommand struct {
Meta
}
func (c *BuildCommand) Run(args []string) int {
buildCtx, cancelBuildCtx := context.WithCancel(context.Background())
// Handle interrupts for this build
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
defer func() {
cancelBuildCtx()
signal.Stop(sigCh)
close(sigCh)
}()
go func() {
select {
case sig := <-sigCh:
if sig == nil {
// context got cancelled and this closed chan probably
// triggered first
return
}
c.Ui.Error(fmt.Sprintf("Cancelling build after receiving %s", sig))
cancelBuildCtx()
case <-buildCtx.Done():
}
}()
return c.RunContext(buildCtx, args)
}
// Config is the command-configuration parsed from the command line.
type Config struct {
Color, Debug, Force, Timestamp bool
ParallelBuilds int64
OnError string
Path string
}
func (c *BuildCommand) ParseArgs(args []string) (Config, int) {
var cfg Config
var parallel bool
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
flags.Usage = func() { c.Ui.Say(c.Help()) }
flags.BoolVar(&cfg.Color, "color", true, "")
flags.BoolVar(&cfg.Debug, "debug", false, "")
flags.BoolVar(&cfg.Force, "force", false, "")
flags.BoolVar(&cfg.Timestamp, "timestamp-ui", false, "")
flagOnError := enumflag.New(&cfg.OnError, "cleanup", "abort", "ask")
flags.Var(flagOnError, "on-error", "")
flags.BoolVar(&parallel, "parallel", true, "")
flags.Int64Var(&cfg.ParallelBuilds, "parallel-builds", 0, "")
if err := flags.Parse(args); err != nil {
return cfg, 1
}
if parallel == false && cfg.ParallelBuilds == 0 {
cfg.ParallelBuilds = 1
}
if cfg.ParallelBuilds < 1 {
cfg.ParallelBuilds = math.MaxInt64
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
return cfg, 1
}
cfg.Path = args[0]
return cfg, 0
}
func (c *BuildCommand) GetBuildsFromHCL(path string) ([]packer.Build, int) {
parser := &hcl2template.Parser{
Parser: hclparse.NewParser(),
BuilderSchemas: c.CoreConfig.Components.BuilderStore,
ProvisionersSchemas: c.CoreConfig.Components.ProvisionerStore,
PostProcessorsSchemas: c.CoreConfig.Components.PostProcessorStore,
}
builds, diags := parser.Parse(path)
{
// write HCL errors/diagnostics if any.
b := bytes.NewBuffer(nil)
err := hcl.NewDiagnosticTextWriter(b, parser.Files(), 80, false).WriteDiagnostics(diags)
if err != nil {
c.Ui.Error("could not write diagnostic: " + err.Error())
return nil, 1
}
if b.Len() != 0 {
c.Ui.Message(b.String())
}
}
ret := 0
if diags.HasErrors() {
ret = 1
}
return builds, ret
}
func (c *BuildCommand) GetBuilds(path string) ([]packer.Build, int) {
isHCLLoaded, err := isHCLLoaded(path)
if path != "-" && err != nil {
c.Ui.Error(fmt.Sprintf("Could not tell wether %s is hcl enabled: %s", path, err))
return nil, 1
}
if isHCLLoaded {
return c.GetBuildsFromHCL(path)
}
c.Ui.Say(`Legacy JSON Configuration Will Be Used.
The template will be parsed in the legacy configuration style. This style
Will continue to work but users are encouraged to move to the new style,
See: https://packer.io/guides/hcl2 .`)
// Parse the template
var tpl *template.Template
tpl, err = template.ParseFile(path)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return nil, 1
}
// Get the core
core, err := c.Meta.Core(tpl)
if err != nil {
c.Ui.Error(err.Error())
return nil, 1
}
ret := 0
buildNames := c.Meta.BuildNames(core)
builds := make([]packer.Build, 0, len(buildNames))
for _, n := range buildNames {
b, err := core.Build(n)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to initialize build '%s': %s",
n, err))
ret = 1
continue
}
builds = append(builds, b)
}
return builds, ret
}
func (c *BuildCommand) RunContext(buildCtx context.Context, args []string) int {
cfg, ret := c.ParseArgs(args)
if ret != 0 {
return ret
}
builds, ret := c.GetBuilds(cfg.Path)
if cfg.Debug {
c.Ui.Say("Debug mode enabled. Builds will not be parallelized.")
}
// Compile all the UIs for the builds
colors := [5]packer.UiColor{
packer.UiColorGreen,
packer.UiColorCyan,
packer.UiColorMagenta,
packer.UiColorYellow,
packer.UiColorBlue,
}
buildUis := make(map[packer.Build]packer.Ui)
for i := range builds {
ui := c.Ui
if cfg.Color {
ui = &packer.ColoredUi{
Color: colors[i%len(colors)],
Ui: ui,
}
if _, ok := c.Ui.(*packer.MachineReadableUi); !ok {
ui.Say(fmt.Sprintf("%s: output will be in this color.", builds[i].Name()))
if i+1 == len(builds) {
// Add a newline between the color output and the actual output
c.Ui.Say("")
}
}
}
// Now add timestamps if requested
if cfg.Timestamp {
ui = &packer.TimestampedUi{
Ui: ui,
}
}
buildUis[builds[i]] = ui
}
log.Printf("Build debug mode: %v", cfg.Debug)
log.Printf("Force build: %v", cfg.Force)
log.Printf("On error: %v", cfg.OnError)
// Set the debug and force mode and prepare all the builds
for i := range builds {
b := builds[i]
log.Printf("Preparing build: %s", b.Name())
b.SetDebug(cfg.Debug)
b.SetForce(cfg.Force)
b.SetOnError(cfg.OnError)
warnings, err := b.Prepare()
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if len(warnings) > 0 {
ui := buildUis[b]
ui.Say(fmt.Sprintf("Warnings for build '%s':\n", b.Name()))
for _, warning := range warnings {
ui.Say(fmt.Sprintf("* %s", warning))
}
ui.Say("")
}
}
// Run all the builds in parallel and wait for them to complete
var wg sync.WaitGroup
var artifacts = struct {
sync.RWMutex
m map[string][]packer.Artifact
}{m: make(map[string][]packer.Artifact)}
// Get the builds we care about
var errors = struct {
sync.RWMutex
m map[string]error
}{m: make(map[string]error)}
limitParallel := semaphore.NewWeighted(cfg.ParallelBuilds)
for i := range builds {
if err := buildCtx.Err(); err != nil {
log.Println("Interrupted, not going to start any more builds.")
break
}
b := builds[i]
name := b.Name()
ui := buildUis[b]
if err := limitParallel.Acquire(buildCtx, 1); err != nil {
ui.Error(fmt.Sprintf("Build '%s' failed to acquire semaphore: %s", name, err))
errors.Lock()
errors.m[name] = err
errors.Unlock()
break
}
// Increment the waitgroup so we wait for this item to finish properly
wg.Add(1)
// Run the build in a goroutine
go func() {
defer wg.Done()
defer limitParallel.Release(1)
log.Printf("Starting build run: %s", name)
runArtifacts, err := b.Run(buildCtx, ui)
if err != nil {
ui.Error(fmt.Sprintf("Build '%s' errored: %s", name, err))
errors.Lock()
errors.m[name] = err
errors.Unlock()
} else {
ui.Say(fmt.Sprintf("Build '%s' finished.", name))
if nil != runArtifacts {
artifacts.Lock()
artifacts.m[name] = runArtifacts
artifacts.Unlock()
}
}
}()
if cfg.Debug {
log.Printf("Debug enabled, so waiting for build to finish: %s", b.Name())
wg.Wait()
}
if cfg.ParallelBuilds == 1 {
log.Printf("Parallelization disabled, waiting for build to finish: %s", b.Name())
wg.Wait()
}
}
// Wait for both the builds to complete and the interrupt handler,
// if it is interrupted.
log.Printf("Waiting on builds to complete...")
wg.Wait()
if err := buildCtx.Err(); err != nil {
c.Ui.Say("Cleanly cancelled builds after being interrupted.")
return 1
}
if len(errors.m) > 0 {
c.Ui.Machine("error-count", strconv.FormatInt(int64(len(errors.m)), 10))
c.Ui.Error("\n==> Some builds didn't complete successfully and had errors:")
for name, err := range errors.m {
// Create a UI for the machine readable stuff to be targeted
ui := &packer.TargetedUI{
Target: name,
Ui: c.Ui,
}
ui.Machine("error", err.Error())
c.Ui.Error(fmt.Sprintf("--> %s: %s", name, err))
}
}
if len(artifacts.m) > 0 {
c.Ui.Say("\n==> Builds finished. The artifacts of successful builds are:")
for name, buildArtifacts := range artifacts.m {
// Create a UI for the machine readable stuff to be targeted
ui := &packer.TargetedUI{
Target: name,
Ui: c.Ui,
}
// Machine-readable helpful
ui.Machine("artifact-count", strconv.FormatInt(int64(len(buildArtifacts)), 10))
for i, artifact := range buildArtifacts {
var message bytes.Buffer
fmt.Fprintf(&message, "--> %s: ", name)
if artifact != nil {
fmt.Fprint(&message, artifact.String())
} else {
fmt.Fprint(&message, "<nothing>")
}
iStr := strconv.FormatInt(int64(i), 10)
if artifact != nil {
ui.Machine("artifact", iStr, "builder-id", artifact.BuilderId())
ui.Machine("artifact", iStr, "id", artifact.Id())
ui.Machine("artifact", iStr, "string", artifact.String())
files := artifact.Files()
ui.Machine("artifact",
iStr,
"files-count", strconv.FormatInt(int64(len(files)), 10))
for fi, file := range files {
fiStr := strconv.FormatInt(int64(fi), 10)
ui.Machine("artifact", iStr, "file", fiStr, file)
}
} else {
ui.Machine("artifact", iStr, "nil")
}
ui.Machine("artifact", iStr, "end")
c.Ui.Say(message.String())
}
}
} else {
c.Ui.Say("\n==> Builds finished but no artifacts were created.")
}
if len(errors.m) > 0 {
// If any errors occurred, exit with a non-zero exit status
ret = 1
}
return ret
}
func (*BuildCommand) Help() string {
helpText := `
Usage: packer build [options] TEMPLATE
Will execute multiple builds in parallel as defined in the template.
The various artifacts created by the template will be outputted.
Options:
-color=false Disable color output. (Default: color)
-debug Debug mode enabled for builds.
-except=foo,bar,baz Run all builds and post-procesors other than these.
-only=foo,bar,baz Build only the specified builds.
-force Force a build to continue if artifacts exist, deletes existing artifacts.
-machine-readable Produce machine-readable output.
-on-error=[cleanup|abort|ask] If the build fails do: clean up (default), abort, or ask.
-parallel=false Disable parallelization. (Default: true)
-parallel-builds=1 Number of builds to run in parallel. 0 means no limit (Default: 0)
-timestamp-ui Enable prefixing of each ui output with an RFC3339 timestamp.
-var 'key=value' Variable for templates, can be used multiple times.
-var-file=path JSON file containing user variables.
`
return strings.TrimSpace(helpText)
}
func (*BuildCommand) Synopsis() string {
return "build image(s) from template"
}
func (*BuildCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (*BuildCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-color": complete.PredictNothing,
"-debug": complete.PredictNothing,
"-except": complete.PredictNothing,
"-only": complete.PredictNothing,
"-force": complete.PredictNothing,
"-machine-readable": complete.PredictNothing,
"-on-error": complete.PredictNothing,
"-parallel": complete.PredictNothing,
"-timestamp-ui": complete.PredictNothing,
"-var": complete.PredictNothing,
"-var-file": complete.PredictNothing,
}
}