Merge pull request #8837 from hashicorp/fix_8730

Fix crash when an unset variable is used
This commit is contained in:
Megan Marsh 2020-03-10 10:40:48 -07:00 committed by GitHub
commit 8a1caaa804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 703 additions and 136 deletions

5
go.mod
View File

@ -20,7 +20,6 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f
github.com/antchfx/htmlquery v1.0.0 // indirect github.com/antchfx/htmlquery v1.0.0 // indirect
github.com/antchfx/xmlquery v1.0.0 // indirect github.com/antchfx/xmlquery v1.0.0 // indirect
github.com/antchfx/xpath v0.0.0-20170728053731-b5c552e1acbd // indirect github.com/antchfx/xpath v0.0.0-20170728053731-b5c552e1acbd // indirect
github.com/antchfx/xquery v0.0.0-20170730121040-eb8c3c172607 // indirect github.com/antchfx/xquery v0.0.0-20170730121040-eb8c3c172607 // indirect
github.com/approvals/go-approval-tests v0.0.0-20160714161514-ad96e53bea43 github.com/approvals/go-approval-tests v0.0.0-20160714161514-ad96e53bea43
@ -153,7 +152,7 @@ require (
github.com/xanzy/go-cloudstack v0.0.0-20190526095453-42f262b63ed0 github.com/xanzy/go-cloudstack v0.0.0-20190526095453-42f262b63ed0
github.com/yandex-cloud/go-genproto v0.0.0-20190916101622-7617782d381e github.com/yandex-cloud/go-genproto v0.0.0-20190916101622-7617782d381e
github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829 github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829
github.com/zclconf/go-cty v1.2.1 github.com/zclconf/go-cty v1.3.1
github.com/zclconf/go-cty-yaml v1.0.1 github.com/zclconf/go-cty-yaml v1.0.1
go.opencensus.io v0.22.2 // indirect go.opencensus.io v0.22.2 // indirect
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
@ -180,6 +179,4 @@ replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110
replace github.com/gofrs/flock => github.com/azr/flock v0.0.0-20190823144736-958d66434653 replace github.com/gofrs/flock => github.com/azr/flock v0.0.0-20190823144736-958d66434653
replace github.com/zclconf/go-cty => github.com/azr/go-cty v1.1.1-0.20200203143058-28fcda2fe0cc
go 1.13 go 1.13

6
go.sum
View File

@ -488,6 +488,11 @@ github.com/yandex-cloud/go-genproto v0.0.0-20190916101622-7617782d381e h1:hzwq5G
github.com/yandex-cloud/go-genproto v0.0.0-20190916101622-7617782d381e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-genproto v0.0.0-20190916101622-7617782d381e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829 h1:2FGwbx03GpP1Ulzg/L46tSoKh9t4yg8BhMKQl/Ff1x8= github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829 h1:2FGwbx03GpP1Ulzg/L46tSoKh9t4yg8BhMKQl/Ff1x8=
github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829/go.mod h1:Eml0jFLU4VVHgIN8zPHMuNwZXVzUMILyO6lQZSfz854= github.com/yandex-cloud/go-sdk v0.0.0-20190916101744-c781afa45829/go.mod h1:Eml0jFLU4VVHgIN8zPHMuNwZXVzUMILyO6lQZSfz854=
github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.3.1 h1:QIOZl+CKKdkv4l2w3lG23nNzXgLoxsWLSEdg1MlX4p0=
github.com/zclconf/go-cty v1.3.1/go.mod h1:YO23e2L18AG+ZYQfSobnY4G65nvwvprPCxBHkufUH1k=
github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8= github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8=
github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
@ -541,6 +546,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zH
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/packer/builder/null"
. "github.com/hashicorp/packer/hcl2template/internal" . "github.com/hashicorp/packer/hcl2template/internal"
"github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
@ -21,6 +22,7 @@ func getBasicParser() *Parser {
BuilderSchemas: packer.MapOfBuilder{ BuilderSchemas: packer.MapOfBuilder{
"amazon-ebs": func() (packer.Builder, error) { return &MockBuilder{}, nil }, "amazon-ebs": func() (packer.Builder, error) { return &MockBuilder{}, nil },
"virtualbox-iso": func() (packer.Builder, error) { return &MockBuilder{}, nil }, "virtualbox-iso": func() (packer.Builder, error) { return &MockBuilder{}, nil },
"null": func() (packer.Builder, error) { return &null.Builder{}, nil },
}, },
ProvisionersSchemas: packer.MapOfProvisioner{ ProvisionersSchemas: packer.MapOfProvisioner{
"shell": func() (packer.Provisioner, error) { return &MockProvisioner{}, nil }, "shell": func() (packer.Provisioner, error) { return &MockProvisioner{}, nil },
@ -58,7 +60,7 @@ func testParse(t *testing.T, tests []parseTest) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
gotCfg, gotDiags := tt.parser.parse(tt.args.filename, tt.args.vars) gotCfg, gotDiags := tt.parser.parse(tt.args.filename, tt.args.vars)
if tt.parseWantDiags == (gotDiags == nil) { if tt.parseWantDiags == (gotDiags == nil) {
t.Fatalf("Parser.parse() unexpected diagnostics. %s", gotDiags) t.Fatalf("Parser.parse() unexpected %q diagnostics.", gotDiags)
} }
if tt.parseWantDiagHasErrors != gotDiags.HasErrors() { if tt.parseWantDiagHasErrors != gotDiags.HasErrors() {
t.Fatalf("Parser.parse() unexpected diagnostics HasErrors. %s", gotDiags) t.Fatalf("Parser.parse() unexpected diagnostics HasErrors. %s", gotDiags)
@ -120,6 +122,7 @@ func testParse(t *testing.T, tests []parseTest) {
packer.CoreBuild{}, packer.CoreBuild{},
packer.CoreBuildProvisioner{}, packer.CoreBuildProvisioner{},
packer.CoreBuildPostProcessor{}, packer.CoreBuildPostProcessor{},
null.Builder{},
), ),
); diff != "" { ); diff != "" {
t.Fatalf("Parser.getBuilds() wrong packer builds. %s", diff) t.Fatalf("Parser.getBuilds() wrong packer builds. %s", diff)

View File

@ -9,6 +9,8 @@ import (
"github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/json"
) )
type NestedMockConfig struct { type NestedMockConfig struct {
@ -38,6 +40,27 @@ type MockConfig struct {
NestedSlice []NestedMockConfig `mapstructure:"nested_slice"` NestedSlice []NestedMockConfig `mapstructure:"nested_slice"`
} }
func (b *MockConfig) Prepare(raws ...interface{}) error {
for i, raw := range raws {
cval, ok := raw.(cty.Value)
if !ok {
continue
}
b, err := json.Marshal(cval, cty.DynamicPseudoType)
if err != nil {
return err
}
ccval, err := json.Unmarshal(b, cty.DynamicPseudoType)
if err != nil {
return err
}
raws[i] = ccval
}
return config.Decode(b, &config.DecodeOpts{
Interpolate: true,
}, raws...)
}
////// //////
// MockBuilder // MockBuilder
////// //////
@ -51,9 +74,7 @@ var _ packer.Builder = new(MockBuilder)
func (b *MockBuilder) ConfigSpec() hcldec.ObjectSpec { return b.Config.FlatMapstructure().HCL2Spec() } func (b *MockBuilder) ConfigSpec() hcldec.ObjectSpec { return b.Config.FlatMapstructure().HCL2Spec() }
func (b *MockBuilder) Prepare(raws ...interface{}) ([]string, []string, error) { func (b *MockBuilder) Prepare(raws ...interface{}) ([]string, []string, error) {
return nil, nil, config.Decode(&b.Config, &config.DecodeOpts{ return nil, nil, b.Config.Prepare(raws...)
Interpolate: true,
}, raws...)
} }
func (b *MockBuilder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { func (b *MockBuilder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
@ -75,9 +96,7 @@ func (b *MockProvisioner) ConfigSpec() hcldec.ObjectSpec {
} }
func (b *MockProvisioner) Prepare(raws ...interface{}) error { func (b *MockProvisioner) Prepare(raws ...interface{}) error {
return config.Decode(&b.Config, &config.DecodeOpts{ return b.Config.Prepare(raws...)
Interpolate: true,
}, raws...)
} }
func (b *MockProvisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.Communicator, _ map[string]interface{}) error { func (b *MockProvisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.Communicator, _ map[string]interface{}) error {
@ -99,9 +118,7 @@ func (b *MockPostProcessor) ConfigSpec() hcldec.ObjectSpec {
} }
func (b *MockPostProcessor) Configure(raws ...interface{}) error { func (b *MockPostProcessor) Configure(raws ...interface{}) error {
return config.Decode(&b.Config, &config.DecodeOpts{ return b.Config.Prepare(raws...)
Interpolate: true,
}, raws...)
} }
func (b *MockPostProcessor) PostProcess(ctx context.Context, ui packer.Ui, a packer.Artifact) (packer.Artifact, bool, bool, error) { func (b *MockPostProcessor) PostProcess(ctx context.Context, ui packer.Ui, a packer.Artifact) (packer.Artifact, bool, bool, error) {
@ -124,9 +141,7 @@ func (b *MockCommunicator) ConfigSpec() hcldec.ObjectSpec {
} }
func (b *MockCommunicator) Configure(raws ...interface{}) ([]string, error) { func (b *MockCommunicator) Configure(raws ...interface{}) ([]string, error) {
return nil, config.Decode(&b.Config, &config.DecodeOpts{ return nil, b.Config.Prepare(raws...)
Interpolate: true,
}, raws...)
} }
////// //////

View File

@ -123,9 +123,14 @@ func (p *Parser) parse(filename string, vars map[string]string) (*PackerConfig,
varFiles = append(varFiles, f) varFiles = append(varFiles, f)
} }
diags = append(diags, cfg.InputVariables.collectVariableValues(os.Environ(), varFiles, vars)...) diags = append(diags, cfg.collectInputVariableValues(os.Environ(), varFiles, vars)...)
} }
_, moreDiags := cfg.InputVariables.Values()
diags = append(diags, moreDiags...)
_, moreDiags = cfg.LocalVariables.Values()
diags = append(diags, moreDiags...)
// decode the actual content // decode the actual content
for _, file := range files { for _, file := range files {
diags = append(diags, p.decodeConfig(file, cfg)...) diags = append(diags, p.decodeConfig(file, cfg)...)

View File

@ -29,6 +29,7 @@ variable "super_secret_password" {
description = <<IMSENSIBLE description = <<IMSENSIBLE
Handle with care plz Handle with care plz
IMSENSIBLE IMSENSIBLE
default = null
} }
locals { locals {

View File

View File

@ -1,4 +1,5 @@
variable "broken_type" { variable "broken_variable" {
invalid = true invalid = true
default = true
} }

View File

@ -0,0 +1,15 @@
variable "foo" {
type = string
}
build {
sources = [
"source.null.null-builder",
]
}
source "null" "null-builder" {
communicator = "none"
}

View File

@ -0,0 +1,10 @@
variable "foo" {
type = string
}
build {
sources = [
"source.null.null-builder${var.foo}",
]
}

View File

@ -60,6 +60,9 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block) (*BuildBlock, hcl.Diagnosti
Config hcl.Body `hcl:",remain"` Config hcl.Body `hcl:",remain"`
} }
diags := gohcl.DecodeBody(block.Body, nil, &b) diags := gohcl.DecodeBody(block.Body, nil, &b)
if diags.HasErrors() {
return nil, diags
}
for _, buildFrom := range b.FromSources { for _, buildFrom := range b.FromSources {
ref := sourceRefFromString(buildFrom) ref := sourceRefFromString(buildFrom)
@ -84,6 +87,9 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block) (*BuildBlock, hcl.Diagnosti
content, moreDiags := b.Config.Content(buildSchema) content, moreDiags := b.Config.Content(buildSchema)
diags = append(diags, moreDiags...) diags = append(diags, moreDiags...)
if diags.HasErrors() {
return nil, diags
}
for _, block := range content.Blocks { for _, block := range content.Blocks {
switch block.Type { switch block.Type {
case buildProvisionerLabel: case buildProvisionerLabel:

View File

@ -2,6 +2,7 @@ package hcl2template
import ( import (
"fmt" "fmt"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
@ -24,19 +25,27 @@ type PackerConfig struct {
InputVariables Variables InputVariables Variables
LocalVariables Variables LocalVariables Variables
ValidationOptions
// Builds is the list of Build blocks defined in the config files. // Builds is the list of Build blocks defined in the config files.
Builds Builds Builds Builds
} }
type ValidationOptions struct {
Strict bool
}
// EvalContext returns the *hcl.EvalContext that will be passed to an hcl // EvalContext returns the *hcl.EvalContext that will be passed to an hcl
// decoder in order to tell what is the actual value of a var or a local and // decoder in order to tell what is the actual value of a var or a local and
// the list of defined functions. // the list of defined functions.
func (cfg *PackerConfig) EvalContext() *hcl.EvalContext { func (cfg *PackerConfig) EvalContext() *hcl.EvalContext {
inputVariables, _ := cfg.InputVariables.Values()
localVariables, _ := cfg.LocalVariables.Values()
ectx := &hcl.EvalContext{ ectx := &hcl.EvalContext{
Functions: Functions(cfg.Basedir), Functions: Functions(cfg.Basedir),
Variables: map[string]cty.Value{ Variables: map[string]cty.Value{
"var": cty.ObjectVal(cfg.InputVariables.Values()), "var": cty.ObjectVal(inputVariables),
"local": cty.ObjectVal(cfg.LocalVariables.Values()), "local": cty.ObjectVal(localVariables),
}, },
} }
return ectx return ectx
@ -76,20 +85,19 @@ func (c *PackerConfig) parseLocalVariables(f *hcl.File) ([]*Local, hcl.Diagnosti
content, moreDiags := f.Body.Content(configSchema) content, moreDiags := f.Body.Content(configSchema)
diags = append(diags, moreDiags...) diags = append(diags, moreDiags...)
var allLocals []*Local var locals []*Local
for _, block := range content.Blocks { for _, block := range content.Blocks {
switch block.Type { switch block.Type {
case localsLabel: case localsLabel:
attrs, moreDiags := block.Body.JustAttributes() attrs, moreDiags := block.Body.JustAttributes()
diags = append(diags, moreDiags...) diags = append(diags, moreDiags...)
locals := make([]*Local, 0, len(attrs))
for name, attr := range attrs { for name, attr := range attrs {
if _, found := c.LocalVariables[name]; found { if _, found := c.LocalVariables[name]; found {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Duplicate variable", Summary: "Duplicate value in " + localsLabel,
Detail: "Duplicate " + name + " variable definition found.", Detail: "Duplicate " + name + " definition found.",
Subject: attr.NameRange.Ptr(), Subject: attr.NameRange.Ptr(),
Context: block.DefRange.Ptr(), Context: block.DefRange.Ptr(),
}) })
@ -100,11 +108,10 @@ func (c *PackerConfig) parseLocalVariables(f *hcl.File) ([]*Local, hcl.Diagnosti
Expr: attr.Expr, Expr: attr.Expr,
}) })
} }
allLocals = append(allLocals, locals...)
} }
} }
return allLocals, diags return locals, diags
} }
func (c *PackerConfig) evaluateLocalVariables(locals []*Local) hcl.Diagnostics { func (c *PackerConfig) evaluateLocalVariables(locals []*Local) hcl.Diagnostics {
@ -156,6 +163,7 @@ func (c *PackerConfig) evaluateLocalVariable(local *Local) hcl.Diagnostics {
return diags return diags
} }
c.LocalVariables[local.Name] = &Variable{ c.LocalVariables[local.Name] = &Variable{
Name: local.Name,
DefaultValue: value, DefaultValue: value,
Type: value.Type(), Type: value.Type(),
} }

View File

@ -23,15 +23,19 @@ func TestParser_complete(t *testing.T) {
Basedir: "testdata/complete", Basedir: "testdata/complete",
InputVariables: Variables{ InputVariables: Variables{
"foo": &Variable{ "foo": &Variable{
Name: "foo",
DefaultValue: cty.StringVal("value"), DefaultValue: cty.StringVal("value"),
}, },
"image_id": &Variable{ "image_id": &Variable{
Name: "image_id",
DefaultValue: cty.StringVal("image-id-default"), DefaultValue: cty.StringVal("image-id-default"),
}, },
"port": &Variable{ "port": &Variable{
Name: "port",
DefaultValue: cty.NumberIntVal(42), DefaultValue: cty.NumberIntVal(42),
}, },
"availability_zone_names": &Variable{ "availability_zone_names": &Variable{
Name: "availability_zone_names",
DefaultValue: cty.ListVal([]cty.Value{ DefaultValue: cty.ListVal([]cty.Value{
cty.StringVal("A"), cty.StringVal("A"),
cty.StringVal("B"), cty.StringVal("B"),
@ -41,15 +45,18 @@ func TestParser_complete(t *testing.T) {
}, },
LocalVariables: Variables{ LocalVariables: Variables{
"feefoo": &Variable{ "feefoo": &Variable{
Name: "feefoo",
DefaultValue: cty.StringVal("value_image-id-default"), DefaultValue: cty.StringVal("value_image-id-default"),
}, },
"standard_tags": &Variable{ "standard_tags": &Variable{
Name: "standard_tags",
DefaultValue: cty.ObjectVal(map[string]cty.Value{ DefaultValue: cty.ObjectVal(map[string]cty.Value{
"Component": cty.StringVal("user-service"), "Component": cty.StringVal("user-service"),
"Environment": cty.StringVal("production"), "Environment": cty.StringVal("production"),
}), }),
}, },
"abc_map": &Variable{ "abc_map": &Variable{
Name: "abc_map",
DefaultValue: cty.TupleVal([]cty.Value{ DefaultValue: cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{ cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"), "id": cty.StringVal("a"),

View File

@ -39,13 +39,15 @@ type Variable struct {
// declaration, the type of the default variable will be used. This will // declaration, the type of the default variable will be used. This will
// allow to ensure that users set this variable correctly. // allow to ensure that users set this variable correctly.
Type cty.Type Type cty.Type
// Common name of the variable
Name string
// Description of the variable // Description of the variable
Description string Description string
// When Sensitive is set to true Packer will try it best to hide/obfuscate // When Sensitive is set to true Packer will try it best to hide/obfuscate
// the variable from the output stream. By replacing the text. // the variable from the output stream. By replacing the text.
Sensitive bool Sensitive bool
block *hcl.Block Range hcl.Range
} }
func (v *Variable) GoString() string { func (v *Variable) GoString() string {
@ -60,27 +62,37 @@ func (v *Variable) Value() (cty.Value, *hcl.Diagnostic) {
v.EnvValue, v.EnvValue,
v.DefaultValue, v.DefaultValue,
} { } {
if !value.IsNull() { if value != cty.NilVal {
return value, nil return value, nil
} }
} }
return cty.NilVal, &hcl.Diagnostic{
value := cty.NullVal(cty.DynamicPseudoType)
return value, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Unset variable", Summary: fmt.Sprintf("Unset variable %q", v.Name),
Detail: "A used variable must be set; see " + Detail: "A used variable must be set or have a default value; see " +
"https://packer.io/docs/configuration/from-1.5/syntax.html for details.", "https://packer.io/docs/configuration/from-1.5/syntax.html for " +
Context: v.block.DefRange.Ptr(), "details.",
Context: v.Range.Ptr(),
} }
} }
type Variables map[string]*Variable type Variables map[string]*Variable
func (variables Variables) Values() map[string]cty.Value { func (variables Variables) Values() (map[string]cty.Value, hcl.Diagnostics) {
res := map[string]cty.Value{} res := map[string]cty.Value{}
var diags hcl.Diagnostics
for k, v := range variables { for k, v := range variables {
res[k], _ = v.Value() value, diag := v.Value()
if diag != nil {
diags = append(diags, diag)
continue
} }
return res res[k] = value
}
return res, diags
} }
// decodeVariable decodes a variable key and value into Variables // decodeVariable decodes a variable key and value into Variables
@ -108,8 +120,10 @@ func (variables *Variables) decodeVariable(key string, attr *hcl.Attribute, ectx
} }
(*variables)[key] = &Variable{ (*variables)[key] = &Variable{
Name: key,
DefaultValue: value, DefaultValue: value,
Type: value.Type(), Type: value.Type(),
Range: attr.Range,
} }
return diags return diags
@ -142,10 +156,13 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
return diags return diags
} }
name := block.Labels[0]
res := &Variable{ res := &Variable{
Name: name,
Description: b.Description, Description: b.Description,
Sensitive: b.Sensitive, Sensitive: b.Sensitive,
block: block, Range: block.DefRange,
} }
attrs, moreDiags := b.Rest.JustAttributes() attrs, moreDiags := b.Rest.JustAttributes()
@ -206,7 +223,7 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
}) })
} }
(*variables)[block.Labels[0]] = res (*variables)[name] = res
return diags return diags
} }
@ -215,8 +232,9 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
// them. // them.
const VarEnvPrefix = "PKR_VAR_" const VarEnvPrefix = "PKR_VAR_"
func (variables Variables) collectVariableValues(env []string, files []*hcl.File, argv map[string]string) hcl.Diagnostics { func (cfg *PackerConfig) collectInputVariableValues(env []string, files []*hcl.File, argv map[string]string) hcl.Diagnostics {
var diags hcl.Diagnostics var diags hcl.Diagnostics
variables := cfg.InputVariables
for _, raw := range env { for _, raw := range env {
if !strings.HasPrefix(raw, VarEnvPrefix) { if !strings.HasPrefix(raw, VarEnvPrefix) {
@ -245,6 +263,7 @@ func (variables Variables) collectVariableValues(env []string, files []*hcl.File
if moreDiags.HasErrors() { if moreDiags.HasErrors() {
continue continue
} }
val, valDiags := expr.Value(nil) val, valDiags := expr.Value(nil)
diags = append(diags, valDiags...) diags = append(diags, valDiags...)
if variable.Type != cty.NilType { if variable.Type != cty.NilType {
@ -310,7 +329,20 @@ func (variables Variables) collectVariableValues(env []string, files []*hcl.File
for name, attr := range attrs { for name, attr := range attrs {
variable, found := variables[name] variable, found := variables[name]
if !found { if !found {
// No file defines this variable; let's skip it sev := hcl.DiagWarning
if cfg.ValidationOptions.Strict {
sev = hcl.DiagError
}
diags = append(diags, &hcl.Diagnostic{
Severity: sev,
Summary: "Undefined variable",
Detail: fmt.Sprintf("A %q variable was set but was "+
"not found in known variables. To declare "+
"variable %q, place this block in one of your"+
".pkr files, such as variables.pkr.hcl",
name, name),
Context: attr.Range.Ptr(),
})
continue continue
} }
@ -340,8 +372,8 @@ func (variables Variables) collectVariableValues(env []string, files []*hcl.File
variable, found := variables[name] variable, found := variables[name]
if !found { if !found {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning, Severity: hcl.DiagError,
Summary: "Unknown -var variable", Summary: "Undefined -var variable",
Detail: fmt.Sprintf("A %q variable was passed in the command "+ Detail: fmt.Sprintf("A %q variable was passed in the command "+
"line but was not found in known variables."+ "line but was not found in known variables."+
"To declare variable %q, place this block in one of your"+ "To declare variable %q, place this block in one of your"+

View File

@ -10,6 +10,7 @@ import (
"github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/builder/null"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
@ -24,36 +25,46 @@ func TestParse_variables(t *testing.T) {
Basedir: filepath.Join("testdata", "variables"), Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{ InputVariables: Variables{
"image_name": &Variable{ "image_name": &Variable{
Name: "image_name",
DefaultValue: cty.StringVal("foo-image-{{user `my_secret`}}"), DefaultValue: cty.StringVal("foo-image-{{user `my_secret`}}"),
}, },
"key": &Variable{ "key": &Variable{
Name: "key",
DefaultValue: cty.StringVal("value"), DefaultValue: cty.StringVal("value"),
}, },
"my_secret": &Variable{ "my_secret": &Variable{
Name: "my_secret",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
"image_id": &Variable{ "image_id": &Variable{
Name: "image_id",
DefaultValue: cty.StringVal("image-id-default"), DefaultValue: cty.StringVal("image-id-default"),
}, },
"port": &Variable{ "port": &Variable{
Name: "port",
DefaultValue: cty.NumberIntVal(42), DefaultValue: cty.NumberIntVal(42),
}, },
"availability_zone_names": &Variable{ "availability_zone_names": &Variable{
Name: "availability_zone_names",
DefaultValue: cty.ListVal([]cty.Value{ DefaultValue: cty.ListVal([]cty.Value{
cty.StringVal("us-west-1a"), cty.StringVal("us-west-1a"),
}), }),
Description: fmt.Sprintln("Describing is awesome ;D"), Description: fmt.Sprintln("Describing is awesome ;D"),
}, },
"super_secret_password": &Variable{ "super_secret_password": &Variable{
Name: "super_secret_password",
Sensitive: true, Sensitive: true,
DefaultValue: cty.NullVal(cty.String),
Description: fmt.Sprintln("Handle with care plz"), Description: fmt.Sprintln("Handle with care plz"),
}, },
}, },
LocalVariables: Variables{ LocalVariables: Variables{
"owner": &Variable{ "owner": &Variable{
Name: "owner",
DefaultValue: cty.StringVal("Community Team"), DefaultValue: cty.StringVal("Community Team"),
}, },
"service_name": &Variable{ "service_name": &Variable{
Name: "service_name",
DefaultValue: cty.StringVal("forum"), DefaultValue: cty.StringVal("forum"),
}, },
}, },
@ -68,7 +79,9 @@ func TestParse_variables(t *testing.T) {
&PackerConfig{ &PackerConfig{
Basedir: filepath.Join("testdata", "variables"), Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{ InputVariables: Variables{
"boolean_value": &Variable{}, "boolean_value": &Variable{
Name: "boolean_value",
},
}, },
}, },
true, true, true, true,
@ -81,7 +94,9 @@ func TestParse_variables(t *testing.T) {
&PackerConfig{ &PackerConfig{
Basedir: filepath.Join("testdata", "variables"), Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{ InputVariables: Variables{
"boolean_value": &Variable{}, "boolean_value": &Variable{
Name: "boolean_value",
},
}, },
}, },
true, true, true, true,
@ -94,26 +109,84 @@ func TestParse_variables(t *testing.T) {
&PackerConfig{ &PackerConfig{
Basedir: filepath.Join("testdata", "variables"), Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{ InputVariables: Variables{
"broken_type": &Variable{}, "broken_type": &Variable{
Name: "broken_type",
},
}, },
}, },
true, true, true, true,
[]packer.Build{}, []packer.Build{},
false, false,
}, },
{"invalid default type",
{"unknown key",
defaultParser, defaultParser,
parseTestArgs{"testdata/variables/unknown_key.pkr.hcl", nil}, parseTestArgs{"testdata/variables/unknown_key.pkr.hcl", nil},
&PackerConfig{ &PackerConfig{
Basedir: filepath.Join("testdata", "variables"), Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{ InputVariables: Variables{
"broken_type": &Variable{}, "broken_variable": &Variable{
Name: "broken_variable",
DefaultValue: cty.BoolVal(true),
},
}, },
}, },
true, false, true, false,
[]packer.Build{}, []packer.Build{},
false, false,
}, },
{"unset used variable",
defaultParser,
parseTestArgs{"testdata/variables/unset_used_string_variable.pkr.hcl", nil},
&PackerConfig{
Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{
"foo": &Variable{
Name: "foo",
},
},
},
true, true,
[]packer.Build{},
true,
},
{"unset unused variable",
defaultParser,
parseTestArgs{"testdata/variables/unset_unused_string_variable.pkr.hcl", nil},
&PackerConfig{
Basedir: filepath.Join("testdata", "variables"),
InputVariables: Variables{
"foo": &Variable{
Name: "foo",
},
},
Sources: map[SourceRef]*SourceBlock{
SourceRef{"null", "null-builder"}: &SourceBlock{
Name: "null-builder",
Type: "null",
},
},
Builds: Builds{
&BuildBlock{
Sources: []SourceRef{SourceRef{"null", "null-builder"}},
},
},
},
true, true,
[]packer.Build{
&packer.CoreBuild{
Type: "null",
Builder: &null.Builder{},
Provisioners: []packer.CoreBuildProvisioner{},
PostProcessors: [][]packer.CoreBuildPostProcessor{},
Prepared: true,
},
},
false,
},
{"locals within another locals usage in different files", {"locals within another locals usage in different files",
defaultParser, defaultParser,
parseTestArgs{"testdata/variables/complicated", nil}, parseTestArgs{"testdata/variables/complicated", nil},
@ -121,23 +194,29 @@ func TestParse_variables(t *testing.T) {
Basedir: "testdata/variables/complicated", Basedir: "testdata/variables/complicated",
InputVariables: Variables{ InputVariables: Variables{
"name_prefix": &Variable{ "name_prefix": &Variable{
Name: "name_prefix",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
}, },
LocalVariables: Variables{ LocalVariables: Variables{
"name_prefix": &Variable{ "name_prefix": &Variable{
Name: "name_prefix",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
"foo": &Variable{ "foo": &Variable{
Name: "foo",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
"bar": &Variable{ "bar": &Variable{
Name: "bar",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
"for_var": &Variable{ "for_var": &Variable{
Name: "for_var",
DefaultValue: cty.StringVal("foo"), DefaultValue: cty.StringVal("foo"),
}, },
"bar_var": &Variable{ "bar_var": &Variable{
Name: "bar_var",
DefaultValue: cty.TupleVal([]cty.Value{ DefaultValue: cty.TupleVal([]cty.Value{
cty.StringVal("foo"), cty.StringVal("foo"),
cty.StringVal("foo"), cty.StringVal("foo"),
@ -174,8 +253,10 @@ func TestVariables_collectVariableValues(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
variables Variables variables Variables
validationOptions ValidationOptions
args args args args
wantDiags bool wantDiags bool
wantDiagsHasError bool
wantVariables Variables wantVariables Variables
wantValues map[string]cty.Value wantValues map[string]cty.Value
}{ }{
@ -329,11 +410,39 @@ func TestVariables_collectVariableValues(t *testing.T) {
}, },
}, },
{name: "undefined but set value", {name: "undefined but set value - pkrvar file - normal mode",
variables: Variables{}, variables: Variables{},
args: args{ args: args{
env: []string{`PKR_VAR_unused_string=value`}, hclFiles: []string{`undefined_string="value"`},
hclFiles: []string{`unused_string="value"`}, },
// output
wantDiags: true,
wantDiagsHasError: false,
wantVariables: Variables{},
wantValues: map[string]cty.Value{},
},
{name: "undefined but set value - pkrvar file - strict mode",
variables: Variables{},
validationOptions: ValidationOptions{
Strict: true,
},
args: args{
hclFiles: []string{`undefined_string="value"`},
},
// output
wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{},
wantValues: map[string]cty.Value{},
},
{name: "undefined but set value - env",
variables: Variables{},
args: args{
env: []string{`PKR_VAR_undefined_string=value`},
}, },
// output // output
@ -342,16 +451,17 @@ func TestVariables_collectVariableValues(t *testing.T) {
wantValues: map[string]cty.Value{}, wantValues: map[string]cty.Value{},
}, },
{name: "undefined but set value - args", {name: "undefined but set value - argv",
variables: Variables{}, variables: Variables{},
args: args{ args: args{
argv: map[string]string{ argv: map[string]string{
"unused_string": "value", "undefined_string": "value",
}, },
}, },
// output // output
wantDiags: true, wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{}, wantVariables: Variables{},
wantValues: map[string]cty.Value{}, wantValues: map[string]cty.Value{},
}, },
@ -368,6 +478,7 @@ func TestVariables_collectVariableValues(t *testing.T) {
// output // output
wantDiags: true, wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{ wantVariables: Variables{
"used_string": &Variable{ "used_string": &Variable{
Type: cty.List(cty.String), Type: cty.List(cty.String),
@ -391,6 +502,7 @@ func TestVariables_collectVariableValues(t *testing.T) {
// output // output
wantDiags: true, wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{ wantVariables: Variables{
"used_string": &Variable{ "used_string": &Variable{
Type: cty.Bool, Type: cty.Bool,
@ -416,6 +528,7 @@ func TestVariables_collectVariableValues(t *testing.T) {
// output // output
wantDiags: true, wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{ wantVariables: Variables{
"used_string": &Variable{ "used_string": &Variable{
Type: cty.Bool, Type: cty.Bool,
@ -435,6 +548,7 @@ func TestVariables_collectVariableValues(t *testing.T) {
// output // output
wantDiags: true, wantDiags: true,
wantDiagsHasError: true,
wantVariables: Variables{}, wantVariables: Variables{},
wantValues: map[string]cty.Value{}, wantValues: map[string]cty.Value{},
}, },
@ -450,9 +564,17 @@ func TestVariables_collectVariableValues(t *testing.T) {
} }
files = append(files, file) files = append(files, file)
} }
if gotDiags := tt.variables.collectVariableValues(tt.args.env, files, tt.args.argv); (gotDiags == nil) == tt.wantDiags { cfg := &PackerConfig{
InputVariables: tt.variables,
ValidationOptions: tt.validationOptions,
}
gotDiags := cfg.collectInputVariableValues(tt.args.env, files, tt.args.argv)
if (gotDiags == nil) == tt.wantDiags {
t.Fatalf("Variables.collectVariableValues() = %v, want %v", gotDiags, tt.wantDiags) t.Fatalf("Variables.collectVariableValues() = %v, want %v", gotDiags, tt.wantDiags)
} }
if tt.wantDiagsHasError != gotDiags.HasErrors() {
t.Fatalf("Variables.collectVariableValues() unexpected diagnostics HasErrors. %s", gotDiags)
}
if diff := cmp.Diff(fmt.Sprintf("%#v", tt.wantVariables), fmt.Sprintf("%#v", tt.variables)); diff != "" { if diff := cmp.Diff(fmt.Sprintf("%#v", tt.wantVariables), fmt.Sprintf("%#v", tt.variables)); diff != "" {
t.Fatalf("didn't get expected variables: %s", diff) t.Fatalf("didn't get expected variables: %s", diff)
} }

View File

@ -138,6 +138,15 @@ func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion {
outEty := out.ElementType() outEty := out.ElementType()
return conversionObjectToMap(in, outEty, unsafe) return conversionObjectToMap(in, outEty, unsafe)
case out.IsObjectType() && in.IsMapType():
if !unsafe {
// Converting a map to an object is an "unsafe" conversion,
// because we don't know if all the map keys will correspond to
// object attributes.
return nil
}
return conversionMapToObject(in, out, unsafe)
case in.IsCapsuleType() || out.IsCapsuleType(): case in.IsCapsuleType() || out.IsCapsuleType():
if !unsafe { if !unsafe {
// Capsule types can only participate in "unsafe" conversions, // Capsule types can only participate in "unsafe" conversions,

View File

@ -15,18 +15,18 @@ func conversionCollectionToList(ety cty.Type, conv conversion) conversion {
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, val.LengthInt()) elems := make([]cty.Value, 0, val.LengthInt())
i := int64(0) i := int64(0)
path = append(path, nil) elemPath := append(path.Copy(), nil)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
_, val := it.Element() _, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(i), Key: cty.NumberIntVal(i),
} }
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -37,6 +37,9 @@ func conversionCollectionToList(ety cty.Type, conv conversion) conversion {
} }
if len(elems) == 0 { if len(elems) == 0 {
if ety == cty.DynamicPseudoType {
ety = val.Type().ElementType()
}
return cty.ListValEmpty(ety), nil return cty.ListValEmpty(ety), nil
} }
@ -55,18 +58,18 @@ func conversionCollectionToSet(ety cty.Type, conv conversion) conversion {
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, val.LengthInt()) elems := make([]cty.Value, 0, val.LengthInt())
i := int64(0) i := int64(0)
path = append(path, nil) elemPath := append(path.Copy(), nil)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
_, val := it.Element() _, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(i), Key: cty.NumberIntVal(i),
} }
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -77,6 +80,11 @@ func conversionCollectionToSet(ety cty.Type, conv conversion) conversion {
} }
if len(elems) == 0 { if len(elems) == 0 {
// Prefer a concrete type over a dynamic type when returning an
// empty set
if ety == cty.DynamicPseudoType {
ety = val.Type().ElementType()
}
return cty.SetValEmpty(ety), nil return cty.SetValEmpty(ety), nil
} }
@ -93,13 +101,13 @@ func conversionCollectionToSet(ety cty.Type, conv conversion) conversion {
func conversionCollectionToMap(ety cty.Type, conv conversion) conversion { func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make(map[string]cty.Value, 0) elems := make(map[string]cty.Value, 0)
path = append(path, nil) elemPath := append(path.Copy(), nil)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
key, val := it.Element() key, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: key, Key: key,
} }
@ -107,11 +115,11 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
if err != nil { if err != nil {
// Should never happen, because keys can only be numbers or // Should never happen, because keys can only be numbers or
// strings and both can convert to string. // strings and both can convert to string.
return cty.DynamicVal, path.NewErrorf("cannot convert key type %s to string for map", key.Type().FriendlyName()) return cty.DynamicVal, elemPath.NewErrorf("cannot convert key type %s to string for map", key.Type().FriendlyName())
} }
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -121,9 +129,25 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
} }
if len(elems) == 0 { if len(elems) == 0 {
// Prefer a concrete type over a dynamic type when returning an
// empty map
if ety == cty.DynamicPseudoType {
ety = val.Type().ElementType()
}
return cty.MapValEmpty(ety), nil return cty.MapValEmpty(ety), nil
} }
if ety.IsCollectionType() || ety.IsObjectType() {
var err error
if elems, err = conversionUnifyCollectionElements(elems, path, false); err != nil {
return cty.NilVal, err
}
}
if err := conversionCheckMapElementTypes(elems, path); err != nil {
return cty.NilVal, err
}
return cty.MapVal(elems), nil return cty.MapVal(elems), nil
} }
} }
@ -171,20 +195,20 @@ func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) con
// element conversions in elemConvs // element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, len(elemConvs)) elems := make([]cty.Value, 0, len(elemConvs))
path = append(path, nil) elemPath := append(path.Copy(), nil)
i := int64(0) i := int64(0)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
_, val := it.Element() _, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(i), Key: cty.NumberIntVal(i),
} }
conv := elemConvs[i] conv := elemConvs[i]
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -241,20 +265,20 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
// element conversions in elemConvs // element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, len(elemConvs)) elems := make([]cty.Value, 0, len(elemConvs))
path = append(path, nil) elemPath := append(path.Copy(), nil)
i := int64(0) i := int64(0)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
_, val := it.Element() _, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(i), Key: cty.NumberIntVal(i),
} }
conv := elemConvs[i] conv := elemConvs[i]
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -315,19 +339,19 @@ func conversionObjectToMap(objectType cty.Type, mapEty cty.Type, unsafe bool) co
// element conversions in elemConvs // element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) { return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make(map[string]cty.Value, len(elemConvs)) elems := make(map[string]cty.Value, len(elemConvs))
path = append(path, nil) elemPath := append(path.Copy(), nil)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
name, val := it.Element() name, val := it.Element()
var err error var err error
path[len(path)-1] = cty.IndexStep{ elemPath[len(elemPath)-1] = cty.IndexStep{
Key: name, Key: name,
} }
conv := elemConvs[name.AsString()] conv := elemConvs[name.AsString()]
if conv != nil { if conv != nil {
val, err = conv(val, path) val, err = conv(val, elemPath)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -335,6 +359,130 @@ func conversionObjectToMap(objectType cty.Type, mapEty cty.Type, unsafe bool) co
elems[name.AsString()] = val elems[name.AsString()] = val
} }
if mapEty.IsCollectionType() || mapEty.IsObjectType() {
var err error
if elems, err = conversionUnifyCollectionElements(elems, path, unsafe); err != nil {
return cty.NilVal, err
}
}
if err := conversionCheckMapElementTypes(elems, path); err != nil {
return cty.NilVal, err
}
return cty.MapVal(elems), nil return cty.MapVal(elems), nil
} }
} }
// conversionMapToObject returns a conversion that will take a value of the
// given map type and return an object of the given type. The object attribute
// types must all be compatible with the map element type.
//
// Will panic if the given mapType and objType are not maps and objects
// respectively.
func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conversion {
objectAtys := objType.AttributeTypes()
mapEty := mapType.ElementType()
elemConvs := make(map[string]conversion, len(objectAtys))
for name, objectAty := range objectAtys {
if objectAty.Equals(mapEty) {
// no conversion required
continue
}
elemConvs[name] = getConversion(mapEty, objectAty, unsafe)
if elemConvs[name] == nil {
// If any of our element conversions are impossible, then the our
// whole conversion is impossible.
return nil
}
}
// If we fall out here then a conversion is possible, using the
// element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make(map[string]cty.Value, len(elemConvs))
elemPath := append(path.Copy(), nil)
it := val.ElementIterator()
for it.Next() {
name, val := it.Element()
// if there is no corresponding attribute, we skip this key
if _, ok := objectAtys[name.AsString()]; !ok {
continue
}
var err error
elemPath[len(elemPath)-1] = cty.IndexStep{
Key: name,
}
conv := elemConvs[name.AsString()]
if conv != nil {
val, err = conv(val, elemPath)
if err != nil {
return cty.NilVal, err
}
}
elems[name.AsString()] = val
}
return cty.ObjectVal(elems), nil
}
}
func conversionUnifyCollectionElements(elems map[string]cty.Value, path cty.Path, unsafe bool) (map[string]cty.Value, error) {
elemTypes := make([]cty.Type, 0, len(elems))
for _, elem := range elems {
elemTypes = append(elemTypes, elem.Type())
}
unifiedType, _ := unify(elemTypes, unsafe)
if unifiedType == cty.NilType {
}
unifiedElems := make(map[string]cty.Value)
elemPath := append(path.Copy(), nil)
for name, elem := range elems {
if elem.Type().Equals(unifiedType) {
unifiedElems[name] = elem
continue
}
conv := getConversion(elem.Type(), unifiedType, unsafe)
if conv == nil {
}
elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.StringVal(name),
}
val, err := conv(elem, elemPath)
if err != nil {
return nil, err
}
unifiedElems[name] = val
}
return unifiedElems, nil
}
func conversionCheckMapElementTypes(elems map[string]cty.Value, path cty.Path) error {
elementType := cty.NilType
elemPath := append(path.Copy(), nil)
for name, elem := range elems {
if elementType == cty.NilType {
elementType = elem.Type()
continue
}
if !elementType.Equals(elem.Type()) {
elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.StringVal(name),
}
return elemPath.NewErrorf("%s is required", elementType.FriendlyName())
}
}
return nil
}

View File

@ -28,11 +28,14 @@ func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
// a subset of that type, which would be a much less useful conversion for // a subset of that type, which would be a much less useful conversion for
// unification purposes. // unification purposes.
{ {
mapCt := 0
objectCt := 0 objectCt := 0
tupleCt := 0 tupleCt := 0
dynamicCt := 0 dynamicCt := 0
for _, ty := range types { for _, ty := range types {
switch { switch {
case ty.IsMapType():
mapCt++
case ty.IsObjectType(): case ty.IsObjectType():
objectCt++ objectCt++
case ty.IsTupleType(): case ty.IsTupleType():
@ -44,6 +47,8 @@ func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
} }
} }
switch { switch {
case mapCt > 0 && (mapCt+dynamicCt) == len(types):
return unifyMapTypes(types, unsafe, dynamicCt > 0)
case objectCt > 0 && (objectCt+dynamicCt) == len(types): case objectCt > 0 && (objectCt+dynamicCt) == len(types):
return unifyObjectTypes(types, unsafe, dynamicCt > 0) return unifyObjectTypes(types, unsafe, dynamicCt > 0)
case tupleCt > 0 && (tupleCt+dynamicCt) == len(types): case tupleCt > 0 && (tupleCt+dynamicCt) == len(types):
@ -95,6 +100,44 @@ Preferences:
return cty.NilType, nil return cty.NilType, nil
} }
func unifyMapTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
// If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so
// we'll conservatively produce DynamicVal for these.
if hasDynamic {
return unifyAllAsDynamic(types)
}
elemTypes := make([]cty.Type, 0, len(types))
for _, ty := range types {
elemTypes = append(elemTypes, ty.ElementType())
}
retElemType, _ := unify(elemTypes, unsafe)
if retElemType == cty.NilType {
return cty.NilType, nil
}
retTy := cty.Map(retElemType)
conversions := make([]Conversion, len(types))
for i, ty := range types {
if ty.Equals(retTy) {
continue
}
if unsafe {
conversions[i] = GetConversionUnsafe(ty, retTy)
} else {
conversions[i] = GetConversion(ty, retTy)
}
if conversions[i] == nil {
// Shouldn't be reachable, since we were able to unify
return cty.NilType, nil
}
}
return retTy, conversions
}
func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
// If we had any dynamic types in the input here then we can't predict // If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so // what path we'll take through here once these become known types, so

View File

@ -299,7 +299,7 @@ var ContainsFunc = function.New(&function.Spec{
}, },
}, },
Type: function.StaticReturnType(cty.Bool), Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
arg := args[0] arg := args[0]
ty := arg.Type() ty := arg.Type()
@ -307,12 +307,39 @@ var ContainsFunc = function.New(&function.Spec{
return cty.NilVal, errors.New("argument must be list, tuple, or set") return cty.NilVal, errors.New("argument must be list, tuple, or set")
} }
_, err = Index(cty.TupleVal(arg.AsValueSlice()), args[1]) if args[0].IsNull() {
if err != nil { return cty.NilVal, errors.New("cannot search a nil list or set")
}
if args[0].LengthInt() == 0 {
return cty.False, nil return cty.False, nil
} }
if !args[0].IsKnown() || !args[1].IsKnown() {
return cty.UnknownVal(cty.Bool), nil
}
containsUnknown := false
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
eq := args[1].Equals(v)
if !eq.IsKnown() {
// We may have an unknown value which could match later, but we
// first need to continue checking all values for an exact
// match.
containsUnknown = true
continue
}
if eq.True() {
return cty.True, nil return cty.True, nil
}
}
if containsUnknown {
return cty.UnknownVal(cty.Bool), nil
}
return cty.False, nil
}, },
}) })
@ -566,19 +593,12 @@ var LookupFunc = function.New(&function.Spec{
Name: "key", Name: "key",
Type: cty.String, Type: cty.String,
}, },
}, {
VarParam: &function.Parameter{
Name: "default", Name: "default",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowUnknown: true, },
AllowDynamicType: true,
AllowNull: true,
}, },
Type: func(args []cty.Value) (ret cty.Type, err error) { Type: func(args []cty.Value) (ret cty.Type, err error) {
if len(args) < 1 || len(args) > 3 {
return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args))
}
ty := args[0].Type() ty := args[0].Type()
switch { switch {
@ -609,13 +629,7 @@ var LookupFunc = function.New(&function.Spec{
} }
}, },
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var defaultVal cty.Value defaultVal := args[2]
defaultValueSet := false
if len(args) == 3 {
defaultVal = args[2]
defaultValueSet = true
}
mapVar := args[0] mapVar := args[0]
lookupKey := args[1].AsString() lookupKey := args[1].AsString()
@ -632,48 +646,128 @@ var LookupFunc = function.New(&function.Spec{
return mapVar.Index(cty.StringVal(lookupKey)), nil return mapVar.Index(cty.StringVal(lookupKey)), nil
} }
if defaultValueSet {
defaultVal, err = convert.Convert(defaultVal, retType) defaultVal, err = convert.Convert(defaultVal, retType)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
return defaultVal, nil return defaultVal, nil
}
return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf(
"lookup failed to find '%s'", lookupKey)
}, },
}) })
// MergeFunc is a function that takes an arbitrary number of maps and // MergeFunc constructs a function that takes an arbitrary number of maps or
// returns a single map that contains a merged set of elements from all of the maps. // objects, and returns a single value that contains a merged set of keys and
// values from all of the inputs.
// //
// If more than one given map defines the same key then the one that is later in // If more than one given map or object defines the same key then the one that
// the argument sequence takes precedence. // is later in the argument sequence takes precedence.
var MergeFunc = function.New(&function.Spec{ var MergeFunc = function.New(&function.Spec{
Params: []function.Parameter{}, Params: []function.Parameter{},
VarParam: &function.Parameter{ VarParam: &function.Parameter{
Name: "maps", Name: "maps",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowDynamicType: true, AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (cty.Type, error) {
// empty args is accepted, so assume an empty object since we have no
// key-value types.
if len(args) == 0 {
return cty.EmptyObject, nil
}
// collect the possible object attrs
attrs := map[string]cty.Type{}
first := cty.NilType
matching := true
attrsKnown := true
for i, arg := range args {
ty := arg.Type()
// any dynamic args mean we can't compute a type
if ty.Equals(cty.DynamicPseudoType) {
return cty.DynamicPseudoType, nil
}
// check for invalid arguments
if !ty.IsMapType() && !ty.IsObjectType() {
return cty.NilType, fmt.Errorf("arguments must be maps or objects, got %#v", ty.FriendlyName())
}
switch {
case ty.IsObjectType() && !arg.IsNull():
for attr, aty := range ty.AttributeTypes() {
attrs[attr] = aty
}
case ty.IsMapType():
switch {
case arg.IsNull():
// pass, nothing to add
case arg.IsKnown():
ety := arg.Type().ElementType()
for it := arg.ElementIterator(); it.Next(); {
attr, _ := it.Element()
attrs[attr.AsString()] = ety
}
default:
// any unknown maps means we don't know all possible attrs
// for the return type
attrsKnown = false
}
}
// record the first argument type for comparison
if i == 0 {
first = arg.Type()
continue
}
if !ty.Equals(first) && matching {
matching = false
}
}
// the types all match, so use the first argument type
if matching {
return first, nil
}
// We had a mix of unknown maps and objects, so we can't predict the
// attributes
if !attrsKnown {
return cty.DynamicPseudoType, nil
}
return cty.Object(attrs), nil
}, },
Type: function.StaticReturnType(cty.DynamicPseudoType),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := make(map[string]cty.Value) outputMap := make(map[string]cty.Value)
// if all inputs are null, return a null value rather than an object
// with null attributes
allNull := true
for _, arg := range args { for _, arg := range args {
if !arg.IsWhollyKnown() { if arg.IsNull() {
return cty.UnknownVal(retType), nil continue
} } else {
if !arg.Type().IsObjectType() && !arg.Type().IsMapType() { allNull = false
return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type().FriendlyName())
} }
for it := arg.ElementIterator(); it.Next(); { for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element() k, v := it.Element()
outputMap[k.AsString()] = v outputMap[k.AsString()] = v
} }
} }
switch {
case allNull:
return cty.NullVal(retType), nil
case retType.IsMapType():
return cty.MapVal(outputMap), nil
case retType.IsObjectType(), retType.Equals(cty.DynamicPseudoType):
return cty.ObjectVal(outputMap), nil return cty.ObjectVal(outputMap), nil
default:
panic(fmt.Sprintf("unexpected return type: %#v", retType))
}
}, },
}) })
@ -1184,8 +1278,8 @@ func Keys(inputMap cty.Value) (cty.Value, error) {
// Lookup performs a dynamic lookup into a map. // Lookup performs a dynamic lookup into a map.
// There are two required arguments, map and key, plus an optional default, // There are two required arguments, map and key, plus an optional default,
// which is a value to return if no key is found in map. // which is a value to return if no key is found in map.
func Lookup(args ...cty.Value) (cty.Value, error) { func Lookup(inputMap, key, defaultValue cty.Value) (cty.Value, error) {
return LookupFunc.Call(args) return LookupFunc.Call([]cty.Value{inputMap, key, defaultValue})
} }
// Merge takes an arbitrary number of maps and returns a single map that contains // Merge takes an arbitrary number of maps and returns a single map that contains

View File

@ -217,7 +217,7 @@ var TimeAddFunc = function.New(&function.Spec{
}, },
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ts, err := time.Parse(time.RFC3339, args[0].AsString()) ts, err := parseTimestamp(args[0].AsString())
if err != nil { if err != nil {
return cty.UnknownVal(cty.String), err return cty.UnknownVal(cty.String), err
} }

2
vendor/modules.txt vendored
View File

@ -638,7 +638,7 @@ github.com/yandex-cloud/go-sdk/pkg/retry
github.com/yandex-cloud/go-sdk/pkg/sdkerrors github.com/yandex-cloud/go-sdk/pkg/sdkerrors
github.com/yandex-cloud/go-sdk/pkg/singleflight github.com/yandex-cloud/go-sdk/pkg/singleflight
github.com/yandex-cloud/go-sdk/sdkresolvers github.com/yandex-cloud/go-sdk/sdkresolvers
# github.com/zclconf/go-cty v1.2.1 => github.com/azr/go-cty v1.1.1-0.20200203143058-28fcda2fe0cc # github.com/zclconf/go-cty v1.3.1
github.com/zclconf/go-cty/cty github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/convert github.com/zclconf/go-cty/cty/convert
github.com/zclconf/go-cty/cty/function github.com/zclconf/go-cty/cty/function

View File

@ -278,3 +278,48 @@ precedence over earlier ones:
~> **Important:** Variables with map and object values behave the same way as ~> **Important:** Variables with map and object values behave the same way as
other variables: the last value found overrides the previous values. other variables: the last value found overrides the previous values.
### A variable value must be known :
Take the following variable for example:
``` hcl
variable "foo" {
type = string
```
Here `foo` must have a known value but you can default it to `null` to make
this behavior optional :
| | no default | `default = null` | `default = "xy"` |
|:---------------------------:|:----------------------------:|:----------------:|:----------------:|
| foo unused | error, "foo needs to be set" | - | - |
| var.foo | error, "foo needs to be set" | null¹ | xy |
| `PKR_VAR_foo=yz`<br>var.foo | yz | yz | yz |
| `-var foo=yz`<br>var.foo | yz | yz | yz |
1: Null is a valid value. Packer will only error when the receiving field needs
a value, example:
``` hcl
variable "example" {
type = string
default = null
}
source "example" "foo" {
arg = var.example
}
```
In the above case, as long as "arg" is optional for an "example" source, there is no error and arg wont be set.
### Setting an unknown variable will not always fail :
| Usage | packer validate | any other packer command |
|:------------------------------:|:-----------------------:|:-------------------------:|
| `bar=yz` in .pkrvars.hcl file. | error, "bar undeclared" | warning, "bar undeclared" |
| `var.bar` in .pkr.hcl file | error, "bar undeclared" | error, "bar undeclared" |
| `-var bar=yz` argument | error, "bar undeclared" | error, "bar undeclared" |
| `export PKR_VAR_bar=yz` | - | - |