Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions command/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int
builds, diags := packerStarter.GetBuilds(packer.GetBuildsOptions{
Only: cla.Only,
Except: cla.Except,
Filters: cla.Filters,
Debug: cla.Debug,
Force: cla.Force,
OnError: cla.OnError,
Expand Down Expand Up @@ -466,6 +467,13 @@ Options:
-debug Debug mode enabled for builds.
-except=foo,bar,baz Run all builds and post-processors other than these.
-only=foo,bar,baz Build only the specified builds.
-filter='tags=prod,x86' Select builds by declared tags/labels. Repeatable;
multiple -filter flags are AND-ed together.
Grammar: KEY=VAL(,VAL) all of
KEY~=VAL(,VAL) any of
KEY!=VAL(,VAL) none of
KEY is either "tags" or a label key. Values are
globs. Applied after -only and -except.
-force Force a build to continue if artifacts exist, deletes existing artifacts.
-machine-readable Produce machine-readable output.
-on-error=[cleanup|abort|ask|run-cleanup-provisioner] If the build fails do: clean up (default), abort, ask, or run-cleanup-provisioner.
Expand Down Expand Up @@ -496,6 +504,7 @@ func (*BuildCommand) AutocompleteFlags() complete.Flags {
"-debug": complete.PredictNothing,
"-except": complete.PredictNothing,
"-only": complete.PredictNothing,
"-filter": complete.PredictNothing,
"-force": complete.PredictNothing,
"-machine-readable": complete.PredictNothing,
"-on-error": complete.PredictNothing,
Expand Down
58 changes: 58 additions & 0 deletions command/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,64 @@ func TestBuild(t *testing.T) {
},
},

// -filter: tags and labels (HCL2)
{
name: "hcl - '-filter tags=prod' selects sources tagged prod",
args: []string{
"-filter=tags=prod",
testFixture("hcl-filter"),
},
fileCheck: fileCheck{
expected: []string{"chocolate.txt", "cherry.txt"},
notExpected: []string{"vanilla.txt"},
},
},
{
name: "hcl - '-filter tags=prod,x86' selects intersection",
args: []string{
"-filter=tags=prod,x86",
testFixture("hcl-filter"),
},
fileCheck: fileCheck{
expected: []string{"chocolate.txt"},
notExpected: []string{"vanilla.txt", "cherry.txt"},
},
},
{
name: "hcl - '-filter tags!=dev' excludes dev-tagged sources",
args: []string{
"-filter=tags!=dev",
testFixture("hcl-filter"),
},
fileCheck: fileCheck{
expected: []string{"chocolate.txt", "cherry.txt"},
notExpected: []string{"vanilla.txt"},
},
},
{
name: "hcl - '-filter region~=us-*' selects by label glob",
args: []string{
"-filter=region~=us-*",
testFixture("hcl-filter"),
},
fileCheck: fileCheck{
expected: []string{"chocolate.txt", "vanilla.txt"},
notExpected: []string{"cherry.txt"},
},
},
{
name: "hcl - multiple -filter flags are AND-ed",
args: []string{
"-filter=tags=prod",
"-filter=region=us-east",
testFixture("hcl-filter"),
},
fileCheck: fileCheck{
expected: []string{"chocolate.txt"},
notExpected: []string{"vanilla.txt", "cherry.txt"},
},
},

// recipes
{
name: "hcl - recipes",
Expand Down
24 changes: 22 additions & 2 deletions command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ func (c *configType) Set(value string) error {
return err
}

// appendStringFlag implements flag.Value by appending each call's value
// verbatim. Unlike sliceflag.StringFlag, it does NOT split on commas — a
// comma is a meaningful character inside a -filter expression's value list
// (e.g. `tags=prod,x86`), so comma-splitting would mangle the input.
type appendStringFlag []string

func (a *appendStringFlag) String() string {
return strings.Join(*a, " ")
}

func (a *appendStringFlag) Set(v string) error {
*a = append(*a, v)
return nil
}

// ConfigType tells what type of config we should use, it can return values
// like "hcl" or "json".
// Make sure Args was correctly set before.
Expand Down Expand Up @@ -56,6 +71,7 @@ func (ma *MetaArgs) GetConfigType() (configType, error) {
func (ma *MetaArgs) AddFlagSets(fs *flag.FlagSet) {
fs.Var((*sliceflag.StringFlag)(&ma.Only), "only", "")
fs.Var((*sliceflag.StringFlag)(&ma.Except), "except", "")
fs.Var((*appendStringFlag)(&ma.Filters), "filter", "")
fs.Var((*kvflag.Flag)(&ma.Vars), "var", "")
fs.Var((*kvflag.StringSlice)(&ma.VarFiles), "var-file", "")
fs.Var(&ma.ConfigType, "config-type", "set to 'hcl2' to run in hcl2 mode when no file is passed.")
Expand All @@ -68,8 +84,11 @@ type MetaArgs struct {
Path string
Paths []string
Only, Except []string
Vars map[string]string
VarFiles []string
// Filters holds repeated -filter expressions that select builds by
// tags/labels. See packer/buildfilter for the grammar.
Filters []string
Vars map[string]string
VarFiles []string
// set to "hcl2" to force hcl2 mode
ConfigType configType

Expand Down Expand Up @@ -117,6 +136,7 @@ func GetCleanedBuildArgs(ba *BuildArgs) map[string]interface{} {
"force": ba.Force,
"only": ba.Only,
"except": ba.Except,
"filter": ba.Filters,
"var-files": ba.VarFiles,
"path": ba.Path,
}
Expand Down
38 changes: 38 additions & 0 deletions command/test-fixtures/hcl-filter/build.pkr.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
source "file" "chocolate" {
content = "chocolate"
target = "chocolate.txt"
metadata {
tags = ["prod", "x86"]
labels = { region = "us-east" }
}
}

source "file" "vanilla" {
content = "vanilla"
target = "vanilla.txt"
metadata {
tags = ["dev", "x86"]
labels = { region = "us-west" }
}
}

source "file" "cherry" {
content = "cherry"
target = "cherry.txt"
metadata {
tags = ["prod", "arm64"]
labels = { region = "eu-west" }
}
}

build {
name = "my_build"
metadata {
tags = ["nightly"]
}
sources = [
"file.chocolate",
"file.vanilla",
"file.cherry",
]
}
5 changes: 3 additions & 2 deletions command/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ func (c *ValidateCommand) RunContext(ctx context.Context, cla *ValidateArgs) int
}

_, diags = packerStarter.GetBuilds(packer.GetBuildsOptions{
Only: cla.Only,
Except: cla.Except,
Only: cla.Only,
Except: cla.Except,
Filters: cla.Filters,
})

fixerDiags := packerStarter.FixConfig(packer.FixConfigOptions{
Expand Down
8 changes: 7 additions & 1 deletion hcl2template/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,13 @@ func (cfg *PackerConfig) initializeBlocks() hcl.Diagnostics {
continue
}

body := sourceDefinition.block.Body
// Use sourceDefinition.Body (which has tags/labels stripped)
// rather than block.Body so plugin ConfigSpec decoding never
// sees the filter attributes.
body := sourceDefinition.Body
if body == nil {
body = sourceDefinition.block.Body
}
if srcUsage.Body != nil {
// merge additions into source definition to get a new body.
body = hcl.MergeBodies([]hcl.Body{body, srcUsage.Body})
Expand Down
31 changes: 31 additions & 0 deletions hcl2template/types.build.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var buildSchema = &hcl.BodySchema{
{Type: buildPostProcessorLabel, LabelNames: []string{"type"}},
{Type: buildPostProcessorsLabel, LabelNames: []string{}},
{Type: buildHCPPackerRegistryLabel},
{Type: metadataBlockLabel},
},
}

Expand All @@ -63,6 +64,17 @@ type BuildBlock struct {
// call for example.
Description string

// Tags is the list of tags declared on the build block. These are
// unioned with the per-source tags to produce the effective tag set
// used by the -filter CLI flag.
Tags []string

// Labels is the key/value metadata declared on the build block. These
// are merged with the per-source labels to produce the effective
// label set used by the -filter CLI flag. On conflict, source labels
// take precedence over build labels because sources are more specific.
Labels map[string]string

// HCPPackerRegistry contains the configuration for publishing the image to the HCP Packer Registry.
HCPPackerRegistry *HCPPackerRegistryBlock

Expand Down Expand Up @@ -154,8 +166,27 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB
if diags.HasErrors() {
return nil, diags
}
seenMetadata := false
for _, block := range content.Blocks {
switch block.Type {
case metadataBlockLabel:
if seenMetadata {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Only one %q block is allowed per build", metadataBlockLabel),
Subject: block.DefRange.Ptr(),
})
continue
}
seenMetadata = true
var meta metadataBody
moreDiags := gohcl.DecodeBody(block.Body, cfg.EvalContext(LocalContext, nil), &meta)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
continue
}
build.Tags = dedupStrings(meta.Tags)
build.Labels = meta.Labels
case buildHCPPackerRegistryLabel:
if build.HCPPackerRegistry != nil {
diags = append(diags, &hcl.Diagnostic{
Expand Down
83 changes: 83 additions & 0 deletions hcl2template/types.packer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax"
pkrfunction "github.com/hashicorp/packer/hcl2template/function"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/packer/buildfilter"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
Expand Down Expand Up @@ -711,6 +712,20 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
})
}

// Compile -filter expressions once up front so a parse error aborts
// before we start builder plugins.
filterExprs, err := buildfilter.Parse(opts.Filters)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid -filter expression",
Detail: err.Error(),
})
return nil, diags
}
filterMatches := 0
filterCandidates := 0

for _, build := range cfg.Builds {
for _, srcUsage := range build.Sources {
src, found := cfg.Sources[srcUsage.SourceRef]
Expand All @@ -727,6 +742,8 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
pcb := &packer.CoreBuild{
BuildName: build.Name,
Type: srcUsage.String(),
Tags: mergeTags(build.Tags, src.Tags, srcUsage.Tags),
Labels: mergeLabels(build.Labels, srcUsage.Labels, src.Labels),
}

pcb.SetDebug(cfg.debug)
Expand Down Expand Up @@ -776,6 +793,16 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
}
}

// -filter: applied after -only/-except, against the effective
// tags/labels set already assigned to pcb.
if len(filterExprs) > 0 {
filterCandidates++
if !buildfilter.Match(pcb, filterExprs) {
continue
}
filterMatches++
}

builder, moreDiags, generatedVars := cfg.startBuilder(srcUsage, cfg.EvalContext(BuildContext, nil))
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
Expand Down Expand Up @@ -862,9 +889,65 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
"These could also be matched with a glob pattern like: 'happycloud.*'", possibleBuildNames),
})
}
if len(filterExprs) > 0 && filterCandidates > 0 && filterMatches == 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "a -filter option was passed, but did not match any build.",
Detail: fmt.Sprintf("Possible build names: %v.\n"+
"Verify that your source and build blocks declare the `tags` and/or `labels` attributes you are filtering on.",
possibleBuildNames),
})
}
return res, diags
}

// mergeTags returns a deduplicated union of the tag slices, preserving the
// order in which values are first encountered (build tags first, then the
// source definition, then the inline source usage). The result is nil when
// every input is empty so CoreBuild.Tags reflects "no tags declared" as a
// nil rather than an empty non-nil slice.
func mergeTags(tagSets ...[]string) []string {
total := 0
for _, s := range tagSets {
total += len(s)
}
if total == 0 {
return nil
}
seen := make(map[string]struct{}, total)
out := make([]string, 0, total)
for _, s := range tagSets {
for _, t := range s {
if _, ok := seen[t]; ok {
continue
}
seen[t] = struct{}{}
out = append(out, t)
}
}
return out
}

// mergeLabels merges label maps. Earlier-listed maps provide defaults;
// later-listed maps override on key conflict. The caller passes them in
// precedence order (lowest to highest): typically build, usage, source.
func mergeLabels(labelSets ...map[string]string) map[string]string {
total := 0
for _, m := range labelSets {
total += len(m)
}
if total == 0 {
return nil
}
out := make(map[string]string, total)
for _, m := range labelSets {
for k, v := range m {
out[k] = v
}
}
return out
}

var PackerConsoleHelp = strings.TrimSpace(`
Packer console HCL2 Mode.
The Packer console allows you to experiment with Packer interpolations.
Expand Down
Loading