teach HCL mode builds to honor -only and -except options (#8947)

This commit is contained in:
Tom Dyas 2020-04-28 06:03:24 -07:00 committed by GitHub
parent e4df3b262b
commit c0a6623ea2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 203 additions and 6 deletions

View File

@ -104,7 +104,7 @@ func (c *BuildCommand) GetBuildsFromHCL(path string) ([]packer.Build, int) {
PostProcessorsSchemas: c.CoreConfig.Components.PostProcessorStore, 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. // write HCL errors/diagnostics if any.
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)

View File

@ -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) { func TestBuildWithNonExistingBuilder(t *testing.T) {
c := &BuildCommand{ c := &BuildCommand{
Meta: testMetaFile(t), Meta: testMetaFile(t),

View File

@ -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",
]
}

2
go.mod
View File

@ -51,7 +51,7 @@ require (
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-ini/ini v1.25.4 github.com/go-ini/ini v1.25.4
github.com/go-ole/go-ole v1.2.4 // indirect 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/gocolly/colly v1.2.0
github.com/gofrs/flock v0.7.1 github.com/gofrs/flock v0.7.1
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3

View File

@ -115,7 +115,7 @@ func testParse(t *testing.T, tests []parseTest) {
return return
} }
gotBuilds, gotDiags := tt.parser.getBuilds(gotCfg) gotBuilds, gotDiags := tt.parser.getBuilds(gotCfg, nil, nil)
if tt.getBuildsWantDiags == (gotDiags == nil) { if tt.getBuildsWantDiags == (gotDiags == nil) {
t.Fatalf("Parser.getBuilds() unexpected diagnostics. %s", gotDiags) t.Fatalf("Parser.getBuilds() unexpected diagnostics. %s", gotDiags)
} }

View File

@ -7,6 +7,8 @@ import (
"github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/gobwas/glob"
) )
// PackerConfig represents a loaded Packer HCL config. It will contain // 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 // 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 // blocks. All Builders, Provisioners and Post Processors will be started and
// configured. // 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{} res := []packer.Build{}
var diags hcl.Diagnostics var diags hcl.Diagnostics
@ -263,6 +265,38 @@ func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics)
}) })
continue 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)) builder, moreDiags, generatedVars := p.startBuilder(src, cfg.EvalContext(nil))
diags = append(diags, moreDiags...) diags = append(diags, moreDiags...)
if moreDiags.HasErrors() { if moreDiags.HasErrors() {
@ -317,6 +351,25 @@ func (p *Parser) getBuilds(cfg *PackerConfig) ([]packer.Build, hcl.Diagnostics)
return res, diags 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 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 // 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 // Parse then return a slice of packer.Builds; which are what packer core uses
// to run builds. // 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) cfg, diags := p.parse(path, varFiles, argVars)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags return nil, diags
} }
builds, moreDiags := p.getBuilds(cfg) builds, moreDiags := p.getBuilds(cfg, onlyGlobs, exceptGlobs)
return builds, append(diags, moreDiags...) return builds, append(diags, moreDiags...)
} }

View File

@ -262,3 +262,25 @@ func TestParser_complete(t *testing.T) {
} }
testParse(t, tests) 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)
}
})
}
}