diff --git a/command/build.go b/command/build.go index 64a352678..05392c063 100644 --- a/command/build.go +++ b/command/build.go @@ -104,7 +104,7 @@ func (c *BuildCommand) GetBuildsFromHCL(path string) ([]packer.Build, int) { PostProcessorsSchemas: c.CoreConfig.Components.PostProcessorStore, } - builds, diags := parser.Parse(path, c.varFiles, c.flagVars) + builds, diags := parser.Parse(path, c.varFiles, c.flagVars, c.CoreConfig.Only, c.CoreConfig.Except) { // write HCL errors/diagnostics if any. b := bytes.NewBuffer(nil) diff --git a/command/build_test.go b/command/build_test.go index 697bcd20e..63eb53ff7 100644 --- a/command/build_test.go +++ b/command/build_test.go @@ -394,6 +394,88 @@ func TestBuildExceptFileCommaFlags(t *testing.T) { } } +func testHCLOnlyExceptFlags(t *testing.T, args, present, notPresent []string) { + c := &BuildCommand{ + Meta: testMetaFile(t), + } + + defer cleanup() + + finalArgs := []string{"-parallel=false"} + finalArgs = append(finalArgs, args...) + finalArgs = append(finalArgs, testFixture("hcl-only-except")) + + if code := c.Run(finalArgs); code != 0 { + fatalCommand(t, c.Meta) + } + + for _, f := range notPresent { + if fileExists(f) { + t.Errorf("Expected NOT to find %s", f) + } + } + for _, f := range present { + if !fileExists(f) { + t.Errorf("Expected to find %s", f) + } + } +} + +func TestBuildCommand_HCLOnlyExceptOptions(t *testing.T) { + tests := []struct { + args []string + present []string + notPresent []string + }{ + { + []string{"-only=chocolate"}, + []string{}, + []string{"chocolate.txt", "vanilla.txt", "cherry.txt"}, + }, + { + []string{"-only=*chocolate*"}, + []string{"chocolate.txt"}, + []string{"vanilla.txt", "cherry.txt"}, + }, + { + []string{"-except=*chocolate*"}, + []string{"vanilla.txt", "cherry.txt"}, + []string{"chocolate.txt"}, + }, + { + []string{"-except=*ch*"}, + []string{"vanilla.txt"}, + []string{"chocolate.txt", "cherry.txt"}, + }, + { + []string{"-only=*chocolate*", "-only=*vanilla*"}, + []string{"chocolate.txt", "vanilla.txt"}, + []string{"cherry.txt"}, + }, + { + []string{"-except=*chocolate*", "-except=*vanilla*"}, + []string{"cherry.txt"}, + []string{"chocolate.txt", "vanilla.txt"}, + }, + { + []string{"-only=file.chocolate"}, + []string{"chocolate.txt"}, + []string{"vanilla.txt", "cherry.txt"}, + }, + { + []string{"-except=file.chocolate"}, + []string{"vanilla.txt", "cherry.txt"}, + []string{"chocolate.txt"}, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("%s", tt.args), func(t *testing.T) { + testHCLOnlyExceptFlags(t, tt.args, tt.present, tt.notPresent) + }) + } +} + func TestBuildWithNonExistingBuilder(t *testing.T) { c := &BuildCommand{ Meta: testMetaFile(t), diff --git a/command/test-fixtures/hcl-only-except/build.pkr.hcl b/command/test-fixtures/hcl-only-except/build.pkr.hcl new file mode 100644 index 000000000..af9f3fff9 --- /dev/null +++ b/command/test-fixtures/hcl-only-except/build.pkr.hcl @@ -0,0 +1,22 @@ +source "file" "chocolate" { + content = "chocolate" + target = "chocolate.txt" +} + +source "file" "vanilla" { + content = "vanilla" + target = "vanilla.txt" +} + +source "file" "cherry" { + content = "cherry" + target = "cherry.txt" +} + +build { + sources = [ + "file.chocolate", + "file.vanilla", + "file.cherry", + ] +} diff --git a/go.mod b/go.mod index ab4e1c120..1dfaa3e69 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.25.4 github.com/go-ole/go-ole v1.2.4 // indirect - github.com/gobwas/glob v0.2.3 // indirect + github.com/gobwas/glob v0.2.3 github.com/gocolly/colly v1.2.0 github.com/gofrs/flock v0.7.1 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 diff --git a/hcl2template/common_test.go b/hcl2template/common_test.go index d6b9462aa..752055fc4 100644 --- a/hcl2template/common_test.go +++ b/hcl2template/common_test.go @@ -115,7 +115,7 @@ func testParse(t *testing.T, tests []parseTest) { return } - gotBuilds, gotDiags := tt.parser.getBuilds(gotCfg) + gotBuilds, gotDiags := tt.parser.getBuilds(gotCfg, nil, nil) if tt.getBuildsWantDiags == (gotDiags == nil) { t.Fatalf("Parser.getBuilds() unexpected diagnostics. %s", gotDiags) } diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index fe8df0732..ead2d66b1 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/packer" "github.com/zclconf/go-cty/cty" + + "github.com/gobwas/glob" ) // PackerConfig represents a loaded Packer HCL config. It will contain @@ -248,7 +250,7 @@ func (p *Parser) getCoreBuildPostProcessors(source *SourceBlock, blocks []*PostP // getBuilds will return a list of packer Build based on the HCL2 parsed build // blocks. All Builders, Provisioners and Post Processors will be started and // configured. -func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics) { +func (p *Parser) getBuilds(cfg *PackerConfig, onlyGlobs []glob.Glob, exceptGlobs []glob.Glob) ([]packer.Build, hcl.Diagnostics) { res := []packer.Build{} var diags hcl.Diagnostics @@ -263,6 +265,38 @@ func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics) }) continue } + + // Apply the -only and -except command-line options to exclude matching builds. + buildName := fmt.Sprintf("%s.%s", src.Type, src.Name) + + // -only + if len(onlyGlobs) > 0 { + include := false + for _, onlyGlob := range onlyGlobs { + if onlyGlob.Match(buildName) { + include = true + break + } + } + if !include { + continue + } + } + + // -except + if len(exceptGlobs) > 0 { + exclude := false + for _, exceptGlob := range exceptGlobs { + if exceptGlob.Match(buildName) { + exclude = true + break + } + } + if exclude { + continue + } + } + builder, moreDiags, generatedVars := p.startBuilder(src, cfg.EvalContext(nil)) diags = append(diags, moreDiags...) if moreDiags.HasErrors() { @@ -317,6 +351,25 @@ func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics) return res, diags } +// Convert -only and -except globs to glob.Glob instances. +func convertFilterOption(patterns []string, optionName string) ([]glob.Glob, hcl.Diagnostics) { + var globs []glob.Glob + var diags hcl.Diagnostics + + for _, pattern := range patterns { + g, err := glob.Compile(pattern) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Summary: fmt.Sprintf("Invalid -%s pattern %s: %s", optionName, pattern, err), + Severity: hcl.DiagError, + }) + } + globs = append(globs, g) + } + + return globs, diags +} + // Parse will parse HCL file(s) in path. Path can be a folder or a file. // // Parse will first parse variables and then the rest; so that interpolation @@ -327,12 +380,30 @@ func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics) // // Parse then return a slice of packer.Builds; which are what packer core uses // to run builds. -func (p *Parser) Parse(path string, varFiles []string, argVars map[string]string) ([]packer.Build, hcl.Diagnostics) { +func (p *Parser) Parse(path string, varFiles []string, argVars map[string]string, onlyBuilds []string, exceptBuilds []string) ([]packer.Build, hcl.Diagnostics) { + var onlyGlobs []glob.Glob + if len(onlyBuilds) > 0 { + og, diags := convertFilterOption(onlyBuilds, "only") + if diags.HasErrors() { + return nil, diags + } + onlyGlobs = og + } + + var exceptGlobs []glob.Glob + if len(exceptBuilds) > 0 { + eg, diags := convertFilterOption(exceptBuilds, "except") + if diags.HasErrors() { + return nil, diags + } + exceptGlobs = eg + } + cfg, diags := p.parse(path, varFiles, argVars) if diags.HasErrors() { return nil, diags } - builds, moreDiags := p.getBuilds(cfg) + builds, moreDiags := p.getBuilds(cfg, onlyGlobs, exceptGlobs) return builds, append(diags, moreDiags...) } diff --git a/hcl2template/types.packer_config_test.go b/hcl2template/types.packer_config_test.go index d3f1afce9..45d9c43c5 100644 --- a/hcl2template/types.packer_config_test.go +++ b/hcl2template/types.packer_config_test.go @@ -262,3 +262,25 @@ func TestParser_complete(t *testing.T) { } testParse(t, tests) } + +func TestParser_ValidateFilterOption(t *testing.T) { + tests := []struct { + pattern string + expectError bool + }{ + {"*foo*", false}, + {"foo[]bar", true}, + } + + for _, test := range tests { + t.Run(test.pattern, func(t *testing.T) { + _, diags := convertFilterOption([]string{test.pattern}, "") + if diags.HasErrors() && !test.expectError { + t.Fatalf("Expected %s to parse as glob", test.pattern) + } + if !diags.HasErrors() && test.expectError { + t.Fatalf("Expected %s to fail to parse as glob", test.pattern) + } + }) + } +}