From 6911495fc486f1bc582eceeffc7c8b80ce775520 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Mon, 2 Nov 2020 11:49:40 +0100 Subject: [PATCH] add VariableAssignment struct that help describe an input var assignment --- hcl2template/common_test.go | 75 +++--- .../validation/valid_map/definition.pkr.hcl | 17 ++ .../valid_map/invalid_value.auto.pkrvars.hcl | 7 + hcl2template/types.packer_config.go | 10 +- hcl2template/types.packer_config_test.go | 61 +++-- hcl2template/types.variables.go | 124 +++++---- hcl2template/types.variables_test.go | 246 +++++++++++------- 7 files changed, 335 insertions(+), 205 deletions(-) create mode 100644 hcl2template/testdata/variables/validation/valid_map/definition.pkr.hcl create mode 100644 hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl diff --git a/hcl2template/common_test.go b/hcl2template/common_test.go index 48eec28fa..9c77cbd7a 100644 --- a/hcl2template/common_test.go +++ b/hcl2template/common_test.go @@ -69,25 +69,7 @@ func testParse(t *testing.T, tests []parseTest) { if tt.parseWantDiagHasErrors != gotDiags.HasErrors() { t.Fatalf("Parser.parse() unexpected diagnostics HasErrors. %s", gotDiags) } - if diff := cmp.Diff(tt.parseWantCfg, gotCfg, - cmpopts.IgnoreUnexported( - PackerConfig{}, - cty.Value{}, - cty.Type{}, - Variable{}, - SourceBlock{}, - ProvisionerBlock{}, - PostProcessorBlock{}, - ), - cmpopts.IgnoreFields(PackerConfig{}, - "Cwd", // Cwd will change for every computer - ), - cmpopts.IgnoreTypes(HCL2Ref{}), - cmpopts.IgnoreTypes([]*LocalBlock{}), - cmpopts.IgnoreTypes([]hcl.Range{}), - cmpopts.IgnoreTypes(hcl.Range{}), - cmpopts.IgnoreInterfaces(struct{ hcl.Expression }{}), - cmpopts.IgnoreInterfaces(struct{ hcl.Body }{}), + if diff := cmp.Diff(tt.parseWantCfg, gotCfg, cmpOpts..., ); diff != "" { t.Fatalf("Parser.parse() wrong packer config. %s", diff) } @@ -96,11 +78,8 @@ func testParse(t *testing.T, tests []parseTest) { gotInputVar := gotCfg.InputVariables for name, value := range tt.parseWantCfg.InputVariables { if variable, ok := gotInputVar[name]; ok { - if diff := cmp.Diff(variable.DefaultValue.GoString(), value.DefaultValue.GoString()); diff != "" { - t.Fatalf("Parser.parse(): unexpected default value for %s: %s", name, diff) - } - if diff := cmp.Diff(variable.VarfileValue.GoString(), value.VarfileValue.GoString()); diff != "" { - t.Fatalf("Parser.parse(): varfile value differs for %s: %s", name, diff) + if diff := cmp.Diff(variable, value, cmpOpts...); diff != "" { + t.Fatalf("Parser.parse(): unexpected variable values %s: %s", name, diff) } } else { t.Fatalf("Parser.parse() missing input variable. %s", name) @@ -110,8 +89,8 @@ func testParse(t *testing.T, tests []parseTest) { gotLocalVar := gotCfg.LocalVariables for name, value := range tt.parseWantCfg.LocalVariables { if variable, ok := gotLocalVar[name]; ok { - if variable.DefaultValue.GoString() != value.DefaultValue.GoString() { - t.Fatalf("Parser.parse() local variable %s expected '%s' but was '%s'", name, value.DefaultValue.GoString(), variable.DefaultValue.GoString()) + if diff := cmp.Diff(variable, value, cmpOpts...); diff != "" { + t.Fatalf("Parser.parse(): unexpected variable values %s: %s", name, diff) } } else { t.Fatalf("Parser.parse() missing local variable. %s", name) @@ -127,18 +106,7 @@ func testParse(t *testing.T, tests []parseTest) { if tt.getBuildsWantDiags == (gotDiags == nil) { t.Fatalf("Parser.getBuilds() unexpected diagnostics. %s", gotDiags) } - if diff := cmp.Diff(tt.getBuildsWantBuilds, gotBuilds, - cmpopts.IgnoreUnexported( - cty.Value{}, - cty.Type{}, - packer.CoreBuild{}, - packer.CoreBuildProvisioner{}, - packer.CoreBuildPostProcessor{}, - null.Builder{}, - HCL2Provisioner{}, - HCL2PostProcessor{}, - ), - ); diff != "" { + if diff := cmp.Diff(tt.getBuildsWantBuilds, gotBuilds, cmpOpts...); diff != "" { t.Fatalf("Parser.getBuilds() wrong packer builds. %s", diff) } }) @@ -250,3 +218,34 @@ var ( }, } ) + +var cmpOpts = []cmp.Option{ + cmpopts.IgnoreUnexported( + PackerConfig{}, + cty.Value{}, + cty.Type{}, + Variable{}, + SourceBlock{}, + ProvisionerBlock{}, + PostProcessorBlock{}, + packer.CoreBuild{}, + HCL2Provisioner{}, + HCL2PostProcessor{}, + packer.CoreBuildPostProcessor{}, + packer.CoreBuildProvisioner{}, + packer.CoreBuildPostProcessor{}, + null.Builder{}, + ), + cmpopts.IgnoreFields(PackerConfig{}, + "Cwd", // Cwd will change for every os type + ), + cmpopts.IgnoreFields(VariableAssignment{}, + "Expr", // its an interface + ), + cmpopts.IgnoreTypes(HCL2Ref{}), + cmpopts.IgnoreTypes([]*LocalBlock{}), + cmpopts.IgnoreTypes([]hcl.Range{}), + cmpopts.IgnoreTypes(hcl.Range{}), + cmpopts.IgnoreInterfaces(struct{ hcl.Expression }{}), + cmpopts.IgnoreInterfaces(struct{ hcl.Body }{}), +} diff --git a/hcl2template/testdata/variables/validation/valid_map/definition.pkr.hcl b/hcl2template/testdata/variables/validation/valid_map/definition.pkr.hcl new file mode 100644 index 000000000..f095e9062 --- /dev/null +++ b/hcl2template/testdata/variables/validation/valid_map/definition.pkr.hcl @@ -0,0 +1,17 @@ + +variable "image_metadata" { + default = { + key: "value", + something: { + foo: "bar", + } + } + validation { + condition = length(var.image_metadata.key) > 4 + error_message = "The image_metadata.key field must be more than 4 runes." + } + validation { + condition = substr(var.image_metadata.something.foo, 0, 3) == "bar" + error_message = "The image_metadata.something.foo field must start with \"bar\"." + } +} diff --git a/hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl b/hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl new file mode 100644 index 000000000..9734f36f6 --- /dev/null +++ b/hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl @@ -0,0 +1,7 @@ + +image_metadata = { + key: "value", + something: { + foo: "woo", + } +} diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index c62436725..daf80a209 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -210,9 +210,13 @@ func (c *PackerConfig) evaluateLocalVariable(local *LocalBlock) hcl.Diagnostics return diags } c.LocalVariables[local.Name] = &Variable{ - Name: local.Name, - DefaultValue: value, - Type: value.Type(), + Name: local.Name, + Values: []VariableAssignment{{ + Value: value, + Expr: local.Expr, + From: "default", + }}, + Type: value.Type(), } return diags diff --git a/hcl2template/types.packer_config_test.go b/hcl2template/types.packer_config_test.go index 90498c172..992e6e80b 100644 --- a/hcl2template/types.packer_config_test.go +++ b/hcl2template/types.packer_config_test.go @@ -25,51 +25,58 @@ func TestParser_complete(t *testing.T) { Basedir: "testdata/complete", InputVariables: Variables{ "foo": &Variable{ - Name: "foo", - DefaultValue: cty.StringVal("value"), + Name: "foo", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("value")}}, }, "image_id": &Variable{ - Name: "image_id", - DefaultValue: cty.StringVal("image-id-default"), + Name: "image_id", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("image-id-default")}}, }, "port": &Variable{ - Name: "port", - DefaultValue: cty.NumberIntVal(42), + Name: "port", + Values: []VariableAssignment{{From: "default", Value: cty.NumberIntVal(42)}}, }, "availability_zone_names": &Variable{ Name: "availability_zone_names", - DefaultValue: cty.ListVal([]cty.Value{ - cty.StringVal("A"), - cty.StringVal("B"), - cty.StringVal("C"), - }), + Values: []VariableAssignment{{ + From: "default", + Value: cty.ListVal([]cty.Value{ + cty.StringVal("A"), + cty.StringVal("B"), + cty.StringVal("C"), + }), + }}, }, }, LocalVariables: Variables{ "feefoo": &Variable{ - Name: "feefoo", - DefaultValue: cty.StringVal("value_image-id-default"), + Name: "feefoo", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("value_image-id-default")}}, }, "standard_tags": &Variable{ Name: "standard_tags", - DefaultValue: cty.ObjectVal(map[string]cty.Value{ - "Component": cty.StringVal("user-service"), - "Environment": cty.StringVal("production"), - }), + Values: []VariableAssignment{{From: "default", + Value: cty.ObjectVal(map[string]cty.Value{ + "Component": cty.StringVal("user-service"), + "Environment": cty.StringVal("production"), + }), + }}, }, "abc_map": &Variable{ Name: "abc_map", - DefaultValue: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("a"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("b"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("c"), + Values: []VariableAssignment{{From: "default", + Value: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("b"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("c"), + }), }), - }), + }}, }, }, Sources: map[SourceRef]SourceBlock{ diff --git a/hcl2template/types.variables.go b/hcl2template/types.variables.go index a5de7ca47..5ffe57cdf 100644 --- a/hcl2template/types.variables.go +++ b/hcl2template/types.variables.go @@ -26,15 +26,26 @@ type LocalBlock struct { Expr hcl.Expression } +// VariableAssignment represents a way a variable was set: the expression +// setting it and the value of that expression. It helps pinpoint were +// something was set in diagnostics. +type VariableAssignment struct { + // From tells were it was taken from, command/varfile/env/default + From string + Value cty.Value + Expr hcl.Expression +} + type Variable struct { - // CmdValue, VarfileValue, EnvValue, DefaultValue are possible values of - // the variable; The first value set from these will be the one used. If - // none is set; an error will be returned if a user tries to use the - // Variable. - CmdValue cty.Value - VarfileValue cty.Value - EnvValue cty.Value - DefaultValue cty.Value + // Values contains possible values for the variable; The last value set + // from these will be the one used. If none is set; an error will be + // returned by Value(). + Values []VariableAssignment + + // Validations contains all variables validation rules to be applied to the + // used value. Only the used value - the last value from Values - is + // validated. + Validations []*VariableValidation // Cty Type of the variable. If the default value or a collected value is // not of this type nor can be converted to this type an error diagnostic @@ -53,25 +64,23 @@ type Variable struct { // the variable from the output stream. By replacing the text. Sensitive bool - Validations []*VariableValidation - Range hcl.Range } func (v *Variable) GoString() string { - return fmt.Sprintf("{Type:%s,CmdValue:%s,VarfileValue:%s,EnvValue:%s,DefaultValue:%s}", - v.Type.GoString(), - PrintableCtyValue(v.CmdValue), - PrintableCtyValue(v.VarfileValue), - PrintableCtyValue(v.EnvValue), - PrintableCtyValue(v.DefaultValue), - ) + b := &strings.Builder{} + fmt.Fprintf(b, "{type:%s", v.Type.GoString()) + for _, vv := range v.Values { + fmt.Fprintf(b, ",%s:%s", vv.From, vv.Value) + } + fmt.Fprintf(b, "}") + return b.String() } // validateValue ensures that all of the configured custom validations for a // variable value are passing. // -func (v *Variable) validateValue(val cty.Value) (diags hcl.Diagnostics) { +func (v *Variable) validateValue(val VariableAssignment) (diags hcl.Diagnostics) { if len(v.Validations) == 0 { log.Printf("[TRACE] validateValue: not active for %s, so skipping", v.Name) return nil @@ -80,7 +89,7 @@ func (v *Variable) validateValue(val cty.Value) (diags hcl.Diagnostics) { hclCtx := &hcl.EvalContext{ Variables: map[string]cty.Value{ "var": cty.ObjectVal(map[string]cty.Value{ - v.Name: val, + v.Name: val.Value, }), }, Functions: Functions(""), @@ -88,7 +97,6 @@ func (v *Variable) validateValue(val cty.Value) (diags hcl.Diagnostics) { for _, validation := range v.Validations { const errInvalidCondition = "Invalid variable validation result" - const errInvalidValue = "Invalid value for variable" result, moreDiags := validation.Condition.Value(hclCtx) diags = append(diags, moreDiags...) @@ -125,11 +133,15 @@ func (v *Variable) validateValue(val cty.Value) (diags hcl.Diagnostics) { } if result.False() { + subj := validation.DeclRange.Ptr() + if val.Expr != nil { + subj = val.Expr.Range().Ptr() + } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: errInvalidValue, + Summary: fmt.Sprintf("Invalid value for %s variable", val.From), Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()), - Subject: validation.DeclRange.Ptr(), + Subject: subj, }) } } @@ -137,26 +149,20 @@ func (v *Variable) validateValue(val cty.Value) (diags hcl.Diagnostics) { return diags } +// Value returns the last found value from the list of variable settings. func (v *Variable) Value() (cty.Value, hcl.Diagnostics) { - for _, value := range []cty.Value{ - v.CmdValue, - v.VarfileValue, - v.EnvValue, - v.DefaultValue, - } { - if value != cty.NilVal { - return value, v.validateValue(value) - } + if len(v.Values) == 0 { + return cty.UnknownVal(v.Type), hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Unset variable %q", v.Name), + Detail: "A used variable must be set or have a default value; see " + + "https://packer.io/docs/configuration/from-1.5/syntax for " + + "details.", + Context: v.Range.Ptr(), + }} } - - return cty.UnknownVal(v.Type), hcl.Diagnostics{&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Unset variable %q", v.Name), - Detail: "A used variable must be set or have a default value; see " + - "https://packer.io/docs/configuration/from-1.5/syntax for " + - "details.", - Context: v.Range.Ptr(), - }} + val := v.Values[len(v.Values)-1] + return val.Value, v.validateValue(v.Values[len(v.Values)-1]) } type Variables map[string]*Variable @@ -205,10 +211,14 @@ func (variables *Variables) decodeVariable(key string, attr *hcl.Attribute, ectx } (*variables)[key] = &Variable{ - Name: key, - DefaultValue: value, - Type: value.Type(), - Range: attr.Range, + Name: key, + Values: []VariableAssignment{{ + From: "default", + Value: value, + Expr: attr.Expr, + }}, + Type: value.Type(), + Range: attr.Range, } return diags @@ -310,12 +320,16 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval } } - v.DefaultValue = defaultValue + v.Values = append(v.Values, VariableAssignment{ + From: "default", + Value: defaultValue, + Expr: def.Expr, + }) // It's possible no type attribute was assigned so lets make sure we // have a valid type otherwise there could be issues parsing the value. if v.Type == cty.NilType { - v.Type = v.DefaultValue.Type() + v.Type = defaultValue.Type() } } @@ -532,7 +546,11 @@ func (cfg *PackerConfig) collectInputVariableValues(env []string, files []*hcl.F val = cty.DynamicVal } } - variable.EnvValue = val + variable.Values = append(variable.Values, VariableAssignment{ + From: "env", + Value: val, + Expr: expr, + }) } // files will contain files found in the folder then files passed as @@ -616,7 +634,11 @@ func (cfg *PackerConfig) collectInputVariableValues(env []string, files []*hcl.F } } - variable.VarfileValue = val + variable.Values = append(variable.Values, VariableAssignment{ + From: "varfile", + Value: val, + Expr: attr.Expr, + }) } } @@ -660,7 +682,11 @@ func (cfg *PackerConfig) collectInputVariableValues(env []string, files []*hcl.F } } - variable.CmdValue = val + variable.Values = append(variable.Values, VariableAssignment{ + From: "cmd", + Value: val, + Expr: expr, + }) } return diags diff --git a/hcl2template/types.variables_test.go b/hcl2template/types.variables_test.go index 4a8f697c5..a6a71d99e 100644 --- a/hcl2template/types.variables_test.go +++ b/hcl2template/types.variables_test.go @@ -25,47 +25,59 @@ func TestParse_variables(t *testing.T) { Basedir: filepath.Join("testdata", "variables"), InputVariables: Variables{ "image_name": &Variable{ - Name: "image_name", - DefaultValue: cty.StringVal("foo-image-{{user `my_secret`}}"), + Name: "image_name", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo-image-{{user `my_secret`}}")}}, }, "key": &Variable{ - Name: "key", - DefaultValue: cty.StringVal("value"), + Name: "key", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("value")}}, }, "my_secret": &Variable{ - Name: "my_secret", - DefaultValue: cty.StringVal("foo"), + Name: "my_secret", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, "image_id": &Variable{ - Name: "image_id", - DefaultValue: cty.StringVal("image-id-default"), + Name: "image_id", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("image-id-default")}}, }, "port": &Variable{ - Name: "port", - DefaultValue: cty.NumberIntVal(42), + Name: "port", + Values: []VariableAssignment{{From: "default", Value: cty.NumberIntVal(42)}}, }, "availability_zone_names": &Variable{ Name: "availability_zone_names", - DefaultValue: cty.ListVal([]cty.Value{ - cty.StringVal("us-west-1a"), - }), + Values: []VariableAssignment{{ + From: "default", + Value: cty.ListVal([]cty.Value{ + cty.StringVal("us-west-1a"), + }), + }}, Description: fmt.Sprintln("Describing is awesome ;D"), }, "super_secret_password": &Variable{ - Name: "super_secret_password", - Sensitive: true, - DefaultValue: cty.NullVal(cty.String), - Description: fmt.Sprintln("Handle with care plz"), + Name: "super_secret_password", + Sensitive: true, + Values: []VariableAssignment{{ + From: "default", + Value: cty.NullVal(cty.String), + }}, + Description: fmt.Sprintln("Handle with care plz"), }, }, LocalVariables: Variables{ "owner": &Variable{ - Name: "owner", - DefaultValue: cty.StringVal("Community Team"), + Name: "owner", + Values: []VariableAssignment{{ + From: "default", + Value: cty.StringVal("Community Team"), + }}, }, "service_name": &Variable{ - Name: "service_name", - DefaultValue: cty.StringVal("forum"), + Name: "service_name", + Values: []VariableAssignment{{ + From: "default", + Value: cty.StringVal("forum"), + }}, }, }, }, @@ -81,6 +93,10 @@ func TestParse_variables(t *testing.T) { InputVariables: Variables{ "boolean_value": &Variable{ Name: "boolean_value", + Values: []VariableAssignment{{ + From: "default", + Value: cty.BoolVal(false), + }}, }, }, }, @@ -96,6 +112,10 @@ func TestParse_variables(t *testing.T) { InputVariables: Variables{ "boolean_value": &Variable{ Name: "boolean_value", + Values: []VariableAssignment{{ + From: "default", + Value: cty.BoolVal(false), + }}, }, }, }, @@ -111,6 +131,10 @@ func TestParse_variables(t *testing.T) { InputVariables: Variables{ "broken_type": &Variable{ Name: "broken_type", + Values: []VariableAssignment{{ + From: "default", + Value: cty.UnknownVal(cty.DynamicPseudoType), + }}, }, }, }, @@ -126,8 +150,8 @@ func TestParse_variables(t *testing.T) { Basedir: filepath.Join("testdata", "variables"), InputVariables: Variables{ "broken_variable": &Variable{ - Name: "broken_variable", - DefaultValue: cty.BoolVal(true), + Name: "broken_variable", + Values: []VariableAssignment{{From: "default", Value: cty.BoolVal(true)}}, }, }, }, @@ -196,34 +220,37 @@ func TestParse_variables(t *testing.T) { Basedir: "testdata/variables/complicated", InputVariables: Variables{ "name_prefix": &Variable{ - Name: "name_prefix", - DefaultValue: cty.StringVal("foo"), + Name: "name_prefix", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, }, LocalVariables: Variables{ "name_prefix": &Variable{ - Name: "name_prefix", - DefaultValue: cty.StringVal("foo"), + Name: "name_prefix", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, "foo": &Variable{ - Name: "foo", - DefaultValue: cty.StringVal("foo"), + Name: "foo", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, "bar": &Variable{ - Name: "bar", - DefaultValue: cty.StringVal("foo"), + Name: "bar", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, "for_var": &Variable{ - Name: "for_var", - DefaultValue: cty.StringVal("foo"), + Name: "for_var", + Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}}, }, "bar_var": &Variable{ Name: "bar_var", - DefaultValue: cty.TupleVal([]cty.Value{ - cty.StringVal("foo"), - cty.StringVal("foo"), - cty.StringVal("foo"), - }), + Values: []VariableAssignment{{ + From: "default", + Value: cty.TupleVal([]cty.Value{ + cty.StringVal("foo"), + cty.StringVal("foo"), + cty.StringVal("foo"), + }), + }}, }, }, }, @@ -250,9 +277,11 @@ func TestParse_variables(t *testing.T) { Basedir: filepath.Join("testdata", "variables"), InputVariables: Variables{ "foo": &Variable{ - DefaultValue: cty.StringVal("bar"), - Name: "foo", - VarfileValue: cty.StringVal("wee"), + Name: "foo", + Values: []VariableAssignment{ + VariableAssignment{"default", cty.StringVal("bar"), nil}, + VariableAssignment{"varfile", cty.StringVal("wee"), nil}, + }, }, }, }, @@ -279,14 +308,14 @@ func TestParse_variables(t *testing.T) { Basedir: filepath.Join("testdata", "variables"), InputVariables: Variables{ "max_retries": &Variable{ - Name: "max_retries", - DefaultValue: cty.StringVal("1"), - Type: cty.String, + Name: "max_retries", + Values: []VariableAssignment{{"default", cty.StringVal("1"), nil}}, + Type: cty.String, }, "max_retries_int": &Variable{ - Name: "max_retries_int", - DefaultValue: cty.NumberIntVal(1), - Type: cty.Number, + Name: "max_retries_int", + Values: []VariableAssignment{{"default", cty.NumberIntVal(1), nil}}, + Type: cty.Number, }, }, Sources: map[SourceRef]SourceBlock{ @@ -365,8 +394,10 @@ func TestParse_variables(t *testing.T) { Basedir: filepath.Join("testdata", "variables", "validation"), InputVariables: Variables{ "image_id": &Variable{ - DefaultValue: cty.StringVal("ami-something-something"), - Name: "image_id", + Values: []VariableAssignment{ + {"default", cty.StringVal("ami-something-something"), nil}, + }, + Name: "image_id", Validations: []*VariableValidation{ &VariableValidation{ ErrorMessage: `The image_id value must be a valid AMI id, starting with "ami-".`, @@ -379,6 +410,28 @@ func TestParse_variables(t *testing.T) { []packer.Build{}, false, }, + + {"valid validation block - invalid default", + defaultParser, + parseTestArgs{"testdata/variables/validation/invalid_default.pkr.hcl", nil, nil}, + &PackerConfig{ + Basedir: filepath.Join("testdata", "variables", "validation"), + InputVariables: Variables{ + "image_id": &Variable{ + Values: []VariableAssignment{{"default", cty.StringVal("ami-something-something"), nil}}, + Name: "image_id", + Validations: []*VariableValidation{ + &VariableValidation{ + ErrorMessage: `The image_id value must be a valid AMI id, starting with "ami-".`, + }, + }, + }, + }, + }, + true, true, + nil, + false, + }, } testParse(t, tests) } @@ -402,8 +455,10 @@ func TestVariables_collectVariableValues(t *testing.T) { {name: "string", variables: Variables{"used_string": &Variable{ - DefaultValue: cty.StringVal("default_value"), - Type: cty.String, + Values: []VariableAssignment{ + {"default", cty.StringVal("default_value"), nil}, + }, + Type: cty.String, }}, args: args{ env: []string{`PKR_VAR_used_string=env_value`}, @@ -420,11 +475,14 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiags: false, wantVariables: Variables{ "used_string": &Variable{ - Type: cty.String, - CmdValue: cty.StringVal("cmd_value"), - VarfileValue: cty.StringVal("varfile_value"), - EnvValue: cty.StringVal("env_value"), - DefaultValue: cty.StringVal("default_value"), + Type: cty.String, + Values: []VariableAssignment{ + {"default", cty.StringVal(`"default_value"`), nil}, + {"env", cty.StringVal(`"env_value"`), nil}, + {"varfile", cty.StringVal(`"xy"`), nil}, + {"varfile", cty.StringVal(`"varfile_value"`), nil}, + {"cmd", cty.StringVal(`"cmd_value"`), nil}, + }, }, }, wantValues: map[string]cty.Value{ @@ -434,8 +492,10 @@ func TestVariables_collectVariableValues(t *testing.T) { {name: "quoted string", variables: Variables{"quoted_string": &Variable{ - DefaultValue: cty.StringVal(`"default_value"`), - Type: cty.String, + Values: []VariableAssignment{ + {"default", cty.StringVal(`"default_value"`), nil}, + }, + Type: cty.String, }}, args: args{ env: []string{`PKR_VAR_quoted_string="env_value"`}, @@ -452,11 +512,14 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiags: false, wantVariables: Variables{ "quoted_string": &Variable{ - Type: cty.String, - CmdValue: cty.StringVal(`"cmd_value"`), - VarfileValue: cty.StringVal(`"varfile_value"`), - EnvValue: cty.StringVal(`"env_value"`), - DefaultValue: cty.StringVal(`"default_value"`), + Type: cty.String, + Values: []VariableAssignment{ + {"default", cty.StringVal(`"default_value"`), nil}, + {"env", cty.StringVal(`"env_value"`), nil}, + {"varfile", cty.StringVal(`"xy"`), nil}, + {"varfile", cty.StringVal(`"varfile_value"`), nil}, + {"cmd", cty.StringVal(`"cmd_value"`), nil}, + }, }, }, wantValues: map[string]cty.Value{ @@ -466,8 +529,10 @@ func TestVariables_collectVariableValues(t *testing.T) { {name: "array of strings", variables: Variables{"used_strings": &Variable{ - DefaultValue: stringListVal("default_value_1"), - Type: cty.List(cty.String), + Values: []VariableAssignment{ + {"default", stringListVal("default_value_1"), nil}, + }, + Type: cty.List(cty.String), }}, args: args{ env: []string{`PKR_VAR_used_strings=["env_value_1", "env_value_2"]`}, @@ -484,11 +549,14 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiags: false, wantVariables: Variables{ "used_strings": &Variable{ - Type: cty.List(cty.String), - CmdValue: stringListVal("cmd_value_1"), - VarfileValue: stringListVal("varfile_value_1"), - EnvValue: stringListVal("env_value_1", "env_value_2"), - DefaultValue: stringListVal("default_value_1"), + Type: cty.List(cty.String), + Values: []VariableAssignment{ + {"default", stringListVal("default_value_1"), nil}, + {"env", stringListVal("env_value_1", "env_value_2"), nil}, + {"varfile", stringListVal("xy"), nil}, + {"varfile", stringListVal("varfile_value_1"), nil}, + {"cmd", stringListVal("cmd_value_1"), nil}, + }, }, }, wantValues: map[string]cty.Value{ @@ -498,8 +566,8 @@ func TestVariables_collectVariableValues(t *testing.T) { {name: "bool", variables: Variables{"enabled": &Variable{ - DefaultValue: cty.False, - Type: cty.Bool, + Values: []VariableAssignment{{"default", cty.False, nil}}, + Type: cty.Bool, }}, args: args{ env: []string{`PKR_VAR_enabled=true`}, @@ -515,11 +583,13 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiags: false, wantVariables: Variables{ "enabled": &Variable{ - Type: cty.Bool, - CmdValue: cty.True, - VarfileValue: cty.False, - EnvValue: cty.True, - DefaultValue: cty.False, + Type: cty.Bool, + Values: []VariableAssignment{ + {"default", cty.False, nil}, + {"env", cty.True, nil}, + {"varfile", cty.False, nil}, + {"cmd", cty.True, nil}, + }, }, }, wantValues: map[string]cty.Value{ @@ -529,8 +599,8 @@ func TestVariables_collectVariableValues(t *testing.T) { {name: "invalid env var", variables: Variables{"used_string": &Variable{ - DefaultValue: cty.StringVal("default_value"), - Type: cty.String, + Values: []VariableAssignment{{"default", cty.StringVal("default_value"), nil}}, + Type: cty.String, }}, args: args{ env: []string{`PKR_VAR_used_string`}, @@ -540,8 +610,8 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiags: false, wantVariables: Variables{ "used_string": &Variable{ - Type: cty.String, - DefaultValue: cty.StringVal("default_value"), + Type: cty.String, + Values: []VariableAssignment{{"default", cty.StringVal("default_value"), nil}}, }, }, wantValues: map[string]cty.Value{ @@ -620,8 +690,8 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiagsHasError: true, wantVariables: Variables{ "used_string": &Variable{ - Type: cty.List(cty.String), - EnvValue: cty.DynamicVal, + Type: cty.List(cty.String), + Values: []VariableAssignment{{"env", cty.DynamicVal, nil}}, }, }, wantValues: map[string]cty.Value{ @@ -644,8 +714,8 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiagsHasError: true, wantVariables: Variables{ "used_string": &Variable{ - Type: cty.Bool, - VarfileValue: cty.DynamicVal, + Type: cty.Bool, + Values: []VariableAssignment{{"varfile", cty.DynamicVal, nil}}, }, }, wantValues: map[string]cty.Value{ @@ -670,8 +740,8 @@ func TestVariables_collectVariableValues(t *testing.T) { wantDiagsHasError: true, wantVariables: Variables{ "used_string": &Variable{ - Type: cty.Bool, - CmdValue: cty.DynamicVal, + Type: cty.Bool, + Values: []VariableAssignment{{"cmd", cty.DynamicVal, nil}}, }, }, wantValues: map[string]cty.Value{ @@ -714,7 +784,7 @@ func TestVariables_collectVariableValues(t *testing.T) { 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(tt.wantVariables, tt.variables, cmpOpts...); diff != "" { t.Fatalf("didn't get expected variables: %s", diff) } values := map[string]cty.Value{}