Merge pull request #10206 from hashicorp/azr_variable_validation
HCL2: add variable validation
This commit is contained in:
commit
61b9015415
|
@ -324,6 +324,52 @@ func TestBuild(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "hcl - valid validation rule for default value",
|
||||
args: []string{
|
||||
filepath.Join(testFixture("hcl", "validation", "map")),
|
||||
},
|
||||
expectedCode: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: "hcl - valid setting from varfile",
|
||||
args: []string{
|
||||
"-var-file", filepath.Join(testFixture("hcl", "validation", "map", "valid_value.pkrvars.hcl")),
|
||||
filepath.Join(testFixture("hcl", "validation", "map")),
|
||||
},
|
||||
expectedCode: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: "hcl - invalid setting from varfile",
|
||||
args: []string{
|
||||
"-var-file", filepath.Join(testFixture("hcl", "validation", "map", "invalid_value.pkrvars.hcl")),
|
||||
filepath.Join(testFixture("hcl", "validation", "map")),
|
||||
},
|
||||
expectedCode: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "hcl - valid cmd ( invalid varfile bypased )",
|
||||
args: []string{
|
||||
"-var-file", filepath.Join(testFixture("hcl", "validation", "map", "invalid_value.pkrvars.hcl")),
|
||||
"-var", `image_metadata={key = "new_value", something = { foo = "bar" }}`,
|
||||
filepath.Join(testFixture("hcl", "validation", "map")),
|
||||
},
|
||||
expectedCode: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: "hcl - invalid cmd ( valid varfile bypased )",
|
||||
args: []string{
|
||||
"-var-file", filepath.Join(testFixture("hcl", "validation", "map", "valid_value.pkrvars.hcl")),
|
||||
"-var", `image_metadata={key = "?", something = { foo = "wrong" }}`,
|
||||
filepath.Join(testFixture("hcl", "validation", "map")),
|
||||
},
|
||||
expectedCode: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
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\"."
|
||||
}
|
||||
}
|
||||
|
||||
build {}
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
image_metadata = {
|
||||
key: "value",
|
||||
something: {
|
||||
foo: "woo",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
image_metadata = {
|
||||
key: "value",
|
||||
something: {
|
||||
foo: "barwoo",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Package addrs contains types that represent "addresses", which are
|
||||
// references to specific objects within a Packer configuration.
|
||||
//
|
||||
// All addresses have string representations based on HCL traversal syntax
|
||||
// which should be used in the user-interface, and also in-memory
|
||||
// representations that can be used internally.
|
||||
//
|
||||
// All types within this package should be treated as immutable, even if this
|
||||
// is not enforced by the Go compiler. It is always an implementation error
|
||||
// to modify an address object in-place after it is initially constructed.
|
||||
package addrs
|
|
@ -0,0 +1,11 @@
|
|||
package addrs
|
||||
|
||||
// InputVariable is the address of an input variable.
|
||||
type InputVariable struct {
|
||||
referenceable
|
||||
Name string
|
||||
}
|
||||
|
||||
func (v InputVariable) String() string {
|
||||
return "var." + v.Name
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
// Reference describes a reference to an address with source location
|
||||
// information.
|
||||
type Reference struct {
|
||||
Subject Referenceable
|
||||
SourceRange hcl.Range
|
||||
Remaining hcl.Traversal
|
||||
}
|
||||
|
||||
// ParseRef attempts to extract a referencable address from the prefix of the
|
||||
// given traversal, which must be an absolute traversal or this function
|
||||
// will panic.
|
||||
//
|
||||
// If no error diagnostics are returned, the returned reference includes the
|
||||
// address that was extracted, the source range it was extracted from, and any
|
||||
// remaining relative traversal that was not consumed as part of the
|
||||
// reference.
|
||||
//
|
||||
// If error diagnostics are returned then the Reference value is invalid and
|
||||
// must not be used.
|
||||
func ParseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) {
|
||||
ref, diags := parseRef(traversal)
|
||||
|
||||
// Normalize a little to make life easier for callers.
|
||||
if ref != nil {
|
||||
if len(ref.Remaining) == 0 {
|
||||
ref.Remaining = nil
|
||||
}
|
||||
}
|
||||
|
||||
return ref, diags
|
||||
}
|
||||
|
||||
func parseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
root := traversal.RootName()
|
||||
rootRange := traversal[0].SourceRange()
|
||||
|
||||
switch root {
|
||||
|
||||
case "var":
|
||||
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
||||
return &Reference{
|
||||
Subject: InputVariable{Name: name},
|
||||
SourceRange: rng,
|
||||
Remaining: remain,
|
||||
}, diags
|
||||
|
||||
default:
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unhandled reference type",
|
||||
Detail: `Currently parseRef can only parse "var" references.`,
|
||||
Subject: &rootRange,
|
||||
})
|
||||
}
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
root := traversal.RootName()
|
||||
rootRange := traversal[0].SourceRange()
|
||||
|
||||
if len(traversal) < 2 {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference",
|
||||
Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
|
||||
Subject: &rootRange,
|
||||
})
|
||||
return "", hcl.Range{}, nil, diags
|
||||
}
|
||||
if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
|
||||
return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
|
||||
}
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference",
|
||||
Detail: fmt.Sprintf("The %q object does not support this operation.", root),
|
||||
Subject: traversal[1].SourceRange().Ptr(),
|
||||
})
|
||||
return "", hcl.Range{}, nil, diags
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package addrs
|
||||
|
||||
// Referenceable is an interface implemented by all address types that can
|
||||
// appear as references in configuration language expressions.
|
||||
type Referenceable interface {
|
||||
// referenceableSigil is private to ensure that all Referenceables are
|
||||
// implentented in this current package. For now this does nothing.
|
||||
referenceableSigil()
|
||||
|
||||
// String produces a string representation of the address that could be
|
||||
// parsed as a HCL traversal and passed to ParseRef to produce an identical
|
||||
// result.
|
||||
String() string
|
||||
}
|
||||
|
||||
// referenceable is an empty struct that implements Referenceable, add it to
|
||||
// your Referenceable struct so that it can be recognized as such.
|
||||
type referenceable struct {
|
||||
}
|
||||
|
||||
func (r referenceable) referenceableSigil() {
|
||||
}
|
|
@ -69,53 +69,17 @@ 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 }{}),
|
||||
); diff != "" {
|
||||
if diff := cmp.Diff(tt.parseWantCfg, gotCfg, cmpOpts...); diff != "" {
|
||||
t.Fatalf("Parser.parse() wrong packer config. %s", diff)
|
||||
}
|
||||
|
||||
if gotCfg != nil && !tt.parseWantDiagHasErrors {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("Parser.parse() missing input variable. %s", name)
|
||||
}
|
||||
if diff := cmp.Diff(tt.parseWantCfg.InputVariables, gotCfg.InputVariables, cmpOpts...); diff != "" {
|
||||
t.Fatalf("Parser.parse() unexpected input vars. %s", diff)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("Parser.parse() missing local variable. %s", name)
|
||||
}
|
||||
if diff := cmp.Diff(tt.parseWantCfg.LocalVariables, gotCfg.LocalVariables, cmpOpts...); diff != "" {
|
||||
t.Fatalf("Parser.parse() unexpected local vars. %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,18 +91,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 +203,48 @@ var (
|
|||
},
|
||||
}
|
||||
)
|
||||
|
||||
var ctyValueComparer = cmp.Comparer(func(x, y cty.Value) bool {
|
||||
return x.RawEquals(y)
|
||||
})
|
||||
|
||||
var ctyTypeComparer = cmp.Comparer(func(x, y cty.Type) bool {
|
||||
if x == cty.NilType && y == cty.NilType {
|
||||
return true
|
||||
}
|
||||
if x == cty.NilType || y == cty.NilType {
|
||||
return false
|
||||
}
|
||||
return x.Equals(y)
|
||||
})
|
||||
|
||||
var cmpOpts = []cmp.Option{
|
||||
ctyValueComparer,
|
||||
ctyTypeComparer,
|
||||
cmpopts.IgnoreUnexported(
|
||||
PackerConfig{},
|
||||
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 }{}),
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||||
)
|
||||
|
||||
var LengthFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "value",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowDynamicType: true,
|
||||
AllowUnknown: true,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
collTy := args[0].Type()
|
||||
switch {
|
||||
case collTy == cty.String || collTy.IsTupleType() || collTy.IsObjectType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType:
|
||||
return cty.Number, nil
|
||||
default:
|
||||
return cty.Number, errors.New("argument must be a string, a collection type, or a structural type")
|
||||
}
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
coll := args[0]
|
||||
collTy := args[0].Type()
|
||||
switch {
|
||||
case collTy == cty.DynamicPseudoType:
|
||||
return cty.UnknownVal(cty.Number), nil
|
||||
case collTy.IsTupleType():
|
||||
l := len(collTy.TupleElementTypes())
|
||||
return cty.NumberIntVal(int64(l)), nil
|
||||
case collTy.IsObjectType():
|
||||
l := len(collTy.AttributeTypes())
|
||||
return cty.NumberIntVal(int64(l)), nil
|
||||
case collTy == cty.String:
|
||||
// We'll delegate to the cty stdlib strlen function here, because
|
||||
// it deals with all of the complexities of tokenizing unicode
|
||||
// grapheme clusters.
|
||||
return stdlib.Strlen(coll)
|
||||
case collTy.IsListType() || collTy.IsSetType() || collTy.IsMapType():
|
||||
return coll.Length(), nil
|
||||
default:
|
||||
// Should never happen, because of the checks in our Type func above
|
||||
return cty.UnknownVal(cty.Number), errors.New("impossible value type for length(...)")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Length returns the number of elements in the given collection or number of
|
||||
// Unicode characters in the given string.
|
||||
func Length(collection cty.Value) (cty.Value, error) {
|
||||
return LengthFunc.Call([]cty.Value{collection})
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
Value cty.Value
|
||||
Want cty.Value
|
||||
}{
|
||||
{
|
||||
cty.ListValEmpty(cty.Number),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.True}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.SetValEmpty(cty.Number),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{cty.True}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.MapValEmpty(cty.Bool),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.MapVal(map[string]cty.Value{"hello": cty.True}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal,
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.EmptyTuple),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.True}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.EmptyObjectVal,
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.EmptyObject),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{"true": cty.True}),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.List(cty.Bool)),
|
||||
cty.UnknownVal(cty.Number),
|
||||
},
|
||||
{
|
||||
cty.DynamicVal,
|
||||
cty.UnknownVal(cty.Number),
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello"),
|
||||
cty.NumberIntVal(5),
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.NumberIntVal(0),
|
||||
},
|
||||
{
|
||||
cty.StringVal("1"),
|
||||
cty.NumberIntVal(1),
|
||||
},
|
||||
{
|
||||
cty.StringVal("Живой Журнал"),
|
||||
cty.NumberIntVal(12),
|
||||
},
|
||||
{
|
||||
// note that the dieresis here is intentionally a combining
|
||||
// ligature.
|
||||
cty.StringVal("noël"),
|
||||
cty.NumberIntVal(4),
|
||||
},
|
||||
{
|
||||
// The Es in this string has three combining acute accents.
|
||||
// This tests something that NFC-normalization cannot collapse
|
||||
// into a single precombined codepoint, since otherwise we might
|
||||
// be cheating and relying on the single-codepoint forms.
|
||||
cty.StringVal("wé́́é́́é́́!"),
|
||||
cty.NumberIntVal(5),
|
||||
},
|
||||
{
|
||||
// Go's normalization forms don't handle this ligature, so we
|
||||
// will produce the wrong result but this is now a compatibility
|
||||
// constraint and so we'll test it.
|
||||
cty.StringVal("baffle"),
|
||||
cty.NumberIntVal(4),
|
||||
},
|
||||
{
|
||||
cty.StringVal("😸😾"),
|
||||
cty.NumberIntVal(2),
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.UnknownVal(cty.Number),
|
||||
},
|
||||
{
|
||||
cty.DynamicVal,
|
||||
cty.UnknownVal(cty.Number),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("Length(%#v)", test.Value), func(t *testing.T) {
|
||||
got, err := Length(test.Value)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ func Functions(basedir string) map[string]function.Function {
|
|||
"jsondecode": stdlib.JSONDecodeFunc,
|
||||
"jsonencode": stdlib.JSONEncodeFunc,
|
||||
"keys": stdlib.KeysFunc,
|
||||
"length": stdlib.LengthFunc,
|
||||
"length": pkrfunction.LengthFunc,
|
||||
"log": stdlib.LogFunc,
|
||||
"lookup": stdlib.LookupFunc,
|
||||
"lower": stdlib.LowerFunc,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
variable "image_id" {
|
||||
type = string
|
||||
default = "potato"
|
||||
validation {
|
||||
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
|
||||
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
variable "image_id" {
|
||||
type = string
|
||||
default = "ami-something-something"
|
||||
validation {
|
||||
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
|
||||
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
|
||||
}
|
||||
}
|
|
@ -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\"."
|
||||
}
|
||||
}
|
7
hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl
vendored
Normal file
7
hcl2template/testdata/variables/validation/valid_map/invalid_value.auto.pkrvars.hcl
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
image_metadata = {
|
||||
key: "value",
|
||||
something: {
|
||||
foo: "woo",
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -25,49 +25,76 @@ 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")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"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")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"port": &Variable{
|
||||
Name: "port",
|
||||
DefaultValue: cty.NumberIntVal(42),
|
||||
Name: "port",
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.NumberIntVal(42)}},
|
||||
Type: cty.Number,
|
||||
},
|
||||
"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"),
|
||||
}),
|
||||
}},
|
||||
Type: cty.List(cty.String),
|
||||
},
|
||||
},
|
||||
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")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"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"),
|
||||
}),
|
||||
}},
|
||||
Type: cty.Object(map[string]cty.Type{
|
||||
"Component": cty.String,
|
||||
"Environment": cty.String,
|
||||
}),
|
||||
},
|
||||
"abc_map": &Variable{
|
||||
Name: "abc_map",
|
||||
DefaultValue: cty.TupleVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("a"),
|
||||
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"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("b"),
|
||||
}},
|
||||
Type: cty.Tuple([]cty.Type{
|
||||
cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("c"),
|
||||
cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
}),
|
||||
cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -2,16 +2,22 @@ package hcl2template
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/packer/hcl2template/addrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
// A consistent detail message for all "not a valid identifier" diagnostics.
|
||||
const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes."
|
||||
|
||||
// Local represents a single entry from a "locals" block in a file.
|
||||
// The "locals" block itself is not represented, because it serves only to
|
||||
// provide context for us to interpret its contents.
|
||||
|
@ -20,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
|
||||
|
@ -51,35 +68,101 @@ type Variable struct {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (v *Variable) Value() (cty.Value, *hcl.Diagnostic) {
|
||||
for _, value := range []cty.Value{
|
||||
v.CmdValue,
|
||||
v.VarfileValue,
|
||||
v.EnvValue,
|
||||
v.DefaultValue,
|
||||
} {
|
||||
if value != cty.NilVal {
|
||||
return value, nil
|
||||
// validateValue ensures that all of the configured custom validations for a
|
||||
// variable value are passing.
|
||||
//
|
||||
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
|
||||
}
|
||||
|
||||
hclCtx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"var": cty.ObjectVal(map[string]cty.Value{
|
||||
v.Name: val.Value,
|
||||
}),
|
||||
},
|
||||
Functions: Functions(""),
|
||||
}
|
||||
|
||||
for _, validation := range v.Validations {
|
||||
const errInvalidCondition = "Invalid variable validation result"
|
||||
|
||||
result, moreDiags := validation.Condition.Value(hclCtx)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition expression failed: %s", v.Name, validation.DeclRange, moreDiags.Error())
|
||||
}
|
||||
if !result.IsKnown() {
|
||||
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", v.Name, validation.DeclRange)
|
||||
continue // We'll wait until we've learned more, then.
|
||||
}
|
||||
if result.IsNull() {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: errInvalidCondition,
|
||||
Detail: "Validation condition expression must return either true or false, not null.",
|
||||
Subject: validation.Condition.Range().Ptr(),
|
||||
Expression: validation.Condition,
|
||||
EvalContext: hclCtx,
|
||||
})
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
result, err = convert.Convert(result, cty.Bool)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: errInvalidCondition,
|
||||
Detail: fmt.Sprintf("Invalid validation condition result value: %s.", err),
|
||||
Subject: validation.Condition.Range().Ptr(),
|
||||
Expression: validation.Condition,
|
||||
EvalContext: hclCtx,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
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: 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: subj,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return cty.UnknownVal(v.Type), &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 diags
|
||||
}
|
||||
|
||||
// Value returns the last found value from the list of variable settings.
|
||||
func (v *Variable) Value() (cty.Value, hcl.Diagnostics) {
|
||||
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(),
|
||||
}}
|
||||
}
|
||||
val := v.Values[len(v.Values)-1]
|
||||
return val.Value, v.validateValue(v.Values[len(v.Values)-1])
|
||||
}
|
||||
|
||||
type Variables map[string]*Variable
|
||||
|
@ -96,10 +179,8 @@ func (variables Variables) Values() (map[string]cty.Value, hcl.Diagnostics) {
|
|||
res := map[string]cty.Value{}
|
||||
var diags hcl.Diagnostics
|
||||
for k, v := range variables {
|
||||
value, diag := v.Value()
|
||||
if diag != nil {
|
||||
diags = append(diags, diag)
|
||||
}
|
||||
value, moreDiags := v.Value()
|
||||
diags = append(diags, moreDiags...)
|
||||
res[k] = value
|
||||
}
|
||||
return res, diags
|
||||
|
@ -130,15 +211,41 @@ 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
|
||||
}
|
||||
|
||||
var variableBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "description",
|
||||
},
|
||||
{
|
||||
Name: "default",
|
||||
},
|
||||
{
|
||||
Name: "type",
|
||||
},
|
||||
{
|
||||
Name: "sensitive",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "validation",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// decodeVariableBlock decodes a "variables" section the way packer 1 used to
|
||||
func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.EvalContext) hcl.Diagnostics {
|
||||
if (*variables) == nil {
|
||||
|
@ -155,51 +262,53 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
|
|||
}}
|
||||
}
|
||||
|
||||
var b struct {
|
||||
Description string `hcl:"description,optional"`
|
||||
Sensitive bool `hcl:"sensitive,optional"`
|
||||
Rest hcl.Body `hcl:",remain"`
|
||||
}
|
||||
diags := gohcl.DecodeBody(block.Body, nil, &b)
|
||||
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
name := block.Labels[0]
|
||||
|
||||
res := &Variable{
|
||||
Name: name,
|
||||
Description: b.Description,
|
||||
Sensitive: b.Sensitive,
|
||||
Range: block.DefRange,
|
||||
content, diags := block.Body.Content(variableBlockSchema)
|
||||
if !hclsyntax.ValidIdentifier(name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid variable name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &block.LabelRanges[0],
|
||||
})
|
||||
}
|
||||
|
||||
attrs, moreDiags := b.Rest.JustAttributes()
|
||||
diags = append(diags, moreDiags...)
|
||||
v := &Variable{
|
||||
Name: name,
|
||||
Range: block.DefRange,
|
||||
}
|
||||
|
||||
if t, ok := attrs["type"]; ok {
|
||||
delete(attrs, "type")
|
||||
if attr, exists := content.Attributes["description"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
if t, ok := content.Attributes["type"]; ok {
|
||||
tp, moreDiags := typeexpr.Type(t.Expr)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
res.Type = tp
|
||||
v.Type = tp
|
||||
}
|
||||
|
||||
if def, ok := attrs["default"]; ok {
|
||||
delete(attrs, "default")
|
||||
if attr, exists := content.Attributes["sensitive"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
if def, ok := content.Attributes["default"]; ok {
|
||||
defaultValue, moreDiags := def.Expr.Value(ectx)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
if res.Type != cty.NilType {
|
||||
if v.Type != cty.NilType {
|
||||
var err error
|
||||
defaultValue, err = convert.Convert(defaultValue, res.Type)
|
||||
defaultValue, err = convert.Convert(defaultValue, v.Type)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
|
@ -211,32 +320,178 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
|
|||
}
|
||||
}
|
||||
|
||||
res.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 res.Type == cty.NilType {
|
||||
res.Type = res.DefaultValue.Type()
|
||||
if v.Type == cty.NilType {
|
||||
v.Type = defaultValue.Type()
|
||||
}
|
||||
}
|
||||
if len(attrs) > 0 {
|
||||
keys := []string{}
|
||||
for k := range attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "Unknown keys",
|
||||
Detail: fmt.Sprintf("unknown variable setting(s): %s", keys),
|
||||
Context: block.DefRange.Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
(*variables)[name] = res
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
case "validation":
|
||||
vv, moreDiags := decodeVariableValidationBlock(v.Name, block)
|
||||
diags = append(diags, moreDiags...)
|
||||
v.Validations = append(v.Validations, vv)
|
||||
}
|
||||
}
|
||||
|
||||
(*variables)[name] = v
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
var variableValidationBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "condition",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "error_message",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// VariableValidation represents a configuration-defined validation rule
|
||||
// for a particular input variable, given as a "validation" block inside
|
||||
// a "variable" block.
|
||||
type VariableValidation struct {
|
||||
// Condition is an expression that refers to the variable being tested and
|
||||
// contains no other references. The expression must return true to
|
||||
// indicate that the value is valid or false to indicate that it is
|
||||
// invalid. If the expression produces an error, that's considered a bug in
|
||||
// the block defining the validation rule, not an error in the caller.
|
||||
Condition hcl.Expression
|
||||
|
||||
// ErrorMessage is one or more full sentences, which _should_ be in English
|
||||
// for consistency with the rest of the error message output but can in
|
||||
// practice be in any language as long as it ends with a period. The
|
||||
// message should describe what is required for the condition to return
|
||||
// true in a way that would make sense to a caller of the module.
|
||||
ErrorMessage string
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeVariableValidationBlock(varName string, block *hcl.Block) (*VariableValidation, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
vv := &VariableValidation{
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
content, moreDiags := block.Body.Content(variableValidationBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
if attr, exists := content.Attributes["condition"]; exists {
|
||||
vv.Condition = attr.Expr
|
||||
|
||||
// The validation condition must refer to the variable itself and
|
||||
// nothing else; to ensure that the variable declaration can't create
|
||||
// additional edges in the dependency graph.
|
||||
goodRefs := 0
|
||||
for _, traversal := range vv.Condition.Variables() {
|
||||
|
||||
ref, moreDiags := addrs.ParseRef(traversal)
|
||||
if !moreDiags.HasErrors() {
|
||||
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
|
||||
if addr.Name == varName {
|
||||
goodRefs++
|
||||
continue // Reference is valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall out here then the reference is invalid.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference in variable validation",
|
||||
Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
|
||||
Subject: traversal.SourceRange().Ptr(),
|
||||
})
|
||||
}
|
||||
if goodRefs < 1 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid variable validation condition",
|
||||
Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName),
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["error_message"]; exists {
|
||||
moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &vv.ErrorMessage)
|
||||
diags = append(diags, moreDiags...)
|
||||
if !moreDiags.HasErrors() {
|
||||
const errSummary = "Invalid validation error message"
|
||||
switch {
|
||||
case vv.ErrorMessage == "":
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: errSummary,
|
||||
Detail: "An empty string is not a valid nor useful error message.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
case !looksLikeSentences(vv.ErrorMessage):
|
||||
// Because we're going to include this string verbatim as part
|
||||
// of a bigger error message written in our usual style, we'll
|
||||
// require the given error message to conform to that. We might
|
||||
// relax this in future if e.g. we start presenting these error
|
||||
// messages in a different way, or if Packer starts supporting
|
||||
// producing error messages in other human languages, etc. For
|
||||
// pragmatism we also allow sentences ending with exclamation
|
||||
// points, but we don't mention it explicitly here because
|
||||
// that's not really consistent with the Packer UI writing
|
||||
// style.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: errSummary,
|
||||
Detail: "Validation error message must be at least one full sentence starting with an uppercase letter ( if the alphabet permits it ) and ending with a period or question mark.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vv, diags
|
||||
}
|
||||
|
||||
// looksLikeSentence is a simple heuristic that encourages writing error
|
||||
// messages that will be presentable when included as part of a larger error
|
||||
// diagnostic whose other text is written in the UI writing style.
|
||||
//
|
||||
// This is intentionally not a very strong validation since we're assuming that
|
||||
// authors want to write good messages and might just need a nudge about
|
||||
// Packer's specific style, rather than that they are going to try to work
|
||||
// around these rules to write a lower-quality message.
|
||||
func looksLikeSentences(s string) bool {
|
||||
if len(s) < 1 {
|
||||
return false
|
||||
}
|
||||
runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
|
||||
first := runes[0]
|
||||
last := runes[len(runes)-1]
|
||||
|
||||
// If the first rune is a letter then it must be an uppercase letter. To
|
||||
// sorts of nudge people into writting sentences. For alphabets that don't
|
||||
// have the notion of 'upper', this does nothing.
|
||||
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
|
||||
return false
|
||||
}
|
||||
|
||||
// The string must be at least one full sentence, which implies having
|
||||
// sentence-ending punctuation.
|
||||
return last == '.' || last == '?' || last == '!'
|
||||
}
|
||||
|
||||
// Prefix your environment variables with VarEnvPrefix so that Packer can see
|
||||
// them.
|
||||
const VarEnvPrefix = "PKR_VAR_"
|
||||
|
@ -288,7 +543,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
|
||||
|
@ -310,7 +569,7 @@ func (cfg *PackerConfig) collectInputVariableValues(env []string, files []*hcl.F
|
|||
})
|
||||
for _, block := range content.Blocks {
|
||||
name := block.Labels[0]
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Variable declaration in a .pkrvar file",
|
||||
Detail: fmt.Sprintf("A .pkrvar file is used to assign "+
|
||||
|
@ -372,7 +631,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,7 +679,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
|
||||
|
|
|
@ -25,47 +25,68 @@ 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",
|
||||
Type: cty.String,
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo-image-{{user `my_secret`}}")}},
|
||||
},
|
||||
"key": &Variable{
|
||||
Name: "key",
|
||||
DefaultValue: cty.StringVal("value"),
|
||||
Name: "key",
|
||||
Type: cty.String,
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("value")}},
|
||||
},
|
||||
"my_secret": &Variable{
|
||||
Name: "my_secret",
|
||||
DefaultValue: cty.StringVal("foo"),
|
||||
Name: "my_secret",
|
||||
Type: cty.String,
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}},
|
||||
},
|
||||
"image_id": &Variable{
|
||||
Name: "image_id",
|
||||
DefaultValue: cty.StringVal("image-id-default"),
|
||||
Name: "image_id",
|
||||
Type: cty.String,
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("image-id-default")}},
|
||||
},
|
||||
"port": &Variable{
|
||||
Name: "port",
|
||||
DefaultValue: cty.NumberIntVal(42),
|
||||
Name: "port",
|
||||
Type: cty.Number,
|
||||
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"),
|
||||
}),
|
||||
}},
|
||||
Type: cty.List(cty.String),
|
||||
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),
|
||||
}},
|
||||
Type: 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"),
|
||||
}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"service_name": &Variable{
|
||||
Name: "service_name",
|
||||
DefaultValue: cty.StringVal("forum"),
|
||||
Name: "service_name",
|
||||
Values: []VariableAssignment{{
|
||||
From: "default",
|
||||
Value: cty.StringVal("forum"),
|
||||
}},
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -81,6 +102,11 @@ func TestParse_variables(t *testing.T) {
|
|||
InputVariables: Variables{
|
||||
"boolean_value": &Variable{
|
||||
Name: "boolean_value",
|
||||
Values: []VariableAssignment{{
|
||||
From: "default",
|
||||
Value: cty.BoolVal(false),
|
||||
}},
|
||||
Type: cty.Bool,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -96,6 +122,11 @@ func TestParse_variables(t *testing.T) {
|
|||
InputVariables: Variables{
|
||||
"boolean_value": &Variable{
|
||||
Name: "boolean_value",
|
||||
Values: []VariableAssignment{{
|
||||
From: "default",
|
||||
Value: cty.BoolVal(false),
|
||||
}},
|
||||
Type: cty.Bool,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -111,6 +142,11 @@ func TestParse_variables(t *testing.T) {
|
|||
InputVariables: Variables{
|
||||
"broken_type": &Variable{
|
||||
Name: "broken_type",
|
||||
Values: []VariableAssignment{{
|
||||
From: "default",
|
||||
Value: cty.UnknownVal(cty.DynamicPseudoType),
|
||||
}},
|
||||
Type: cty.List(cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -126,12 +162,13 @@ 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)}},
|
||||
Type: cty.Bool,
|
||||
},
|
||||
},
|
||||
},
|
||||
true, false,
|
||||
true, true,
|
||||
[]packer.Build{},
|
||||
false,
|
||||
},
|
||||
|
@ -144,6 +181,7 @@ func TestParse_variables(t *testing.T) {
|
|||
InputVariables: Variables{
|
||||
"foo": &Variable{
|
||||
Name: "foo",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -160,6 +198,7 @@ func TestParse_variables(t *testing.T) {
|
|||
InputVariables: Variables{
|
||||
"foo": &Variable{
|
||||
Name: "foo",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
Sources: map[SourceRef]SourceBlock{
|
||||
|
@ -196,33 +235,46 @@ 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")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
LocalVariables: Variables{
|
||||
"name_prefix": &Variable{
|
||||
Name: "name_prefix",
|
||||
DefaultValue: cty.StringVal("foo"),
|
||||
Name: "name_prefix",
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"foo": &Variable{
|
||||
Name: "foo",
|
||||
DefaultValue: cty.StringVal("foo"),
|
||||
Name: "foo",
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"bar": &Variable{
|
||||
Name: "bar",
|
||||
DefaultValue: cty.StringVal("foo"),
|
||||
Name: "bar",
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"for_var": &Variable{
|
||||
Name: "for_var",
|
||||
DefaultValue: cty.StringVal("foo"),
|
||||
Name: "for_var",
|
||||
Values: []VariableAssignment{{From: "default", Value: cty.StringVal("foo")}},
|
||||
Type: cty.String,
|
||||
},
|
||||
"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"),
|
||||
}),
|
||||
}},
|
||||
Type: cty.Tuple([]cty.Type{
|
||||
cty.String,
|
||||
cty.String,
|
||||
cty.String,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
@ -250,9 +302,12 @@ 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},
|
||||
},
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -279,14 +334,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{
|
||||
|
@ -357,6 +412,54 @@ func TestParse_variables(t *testing.T) {
|
|||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{"valid validation block",
|
||||
defaultParser,
|
||||
parseTestArgs{"testdata/variables/validation/valid.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",
|
||||
Type: cty.String,
|
||||
Validations: []*VariableValidation{
|
||||
&VariableValidation{
|
||||
ErrorMessage: `The image_id value must be a valid AMI id, starting with "ami-".`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false, false,
|
||||
[]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("potato"), nil}},
|
||||
Name: "image_id",
|
||||
Type: cty.String,
|
||||
Validations: []*VariableValidation{
|
||||
&VariableValidation{
|
||||
ErrorMessage: `The image_id value must be a valid AMI id, starting with "ami-".`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true, true,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
}
|
||||
testParse(t, tests)
|
||||
}
|
||||
|
@ -380,8 +483,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`},
|
||||
|
@ -398,11 +503,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{
|
||||
|
@ -412,8 +520,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"`},
|
||||
|
@ -430,11 +540,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{
|
||||
|
@ -444,8 +557,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"]`},
|
||||
|
@ -462,11 +577,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{
|
||||
|
@ -476,8 +594,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`},
|
||||
|
@ -493,11 +611,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{
|
||||
|
@ -507,8 +627,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`},
|
||||
|
@ -518,8 +638,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{
|
||||
|
@ -598,8 +718,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{
|
||||
|
@ -622,8 +742,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{
|
||||
|
@ -648,8 +768,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{
|
||||
|
@ -692,7 +812,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{}
|
||||
|
|
|
@ -24,6 +24,8 @@ If a default value is set, the variable is optional. Otherwise, the variable
|
|||
|
||||
`@include 'from-1.5/variables/assignment.mdx'`
|
||||
|
||||
`@include 'from-1.5/variables/custom-validation.mdx'`
|
||||
|
||||
Example of a variable assignment from a file:
|
||||
|
||||
`@include 'from-1.5/variables/foo-pkrvar.mdx'`
|
||||
|
|
|
@ -162,6 +162,8 @@ maintainers, use comments.
|
|||
|
||||
The following sections describe these options in more detail.
|
||||
|
||||
`@include 'from-1.5/variables/custom-validation.mdx'`
|
||||
|
||||
### Variables on the Command Line
|
||||
|
||||
To specify individual variables on the command line, use the `-var` option when
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
## Custom Validation Rules
|
||||
|
||||
In addition to Type Constraints, you can specify arbitrary custom validation
|
||||
rules for a particular variable using one or more `validation` block nested
|
||||
within the corresponding `variable` block:
|
||||
|
||||
```hcl
|
||||
variable "image_id" {
|
||||
type = string
|
||||
description = "The id of the machine image (AMI) to use for the server."
|
||||
|
||||
validation {
|
||||
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
|
||||
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `condition` argument is an expression that must use the value of the
|
||||
variable to return `true` if the value is valid or `false` if it is invalid.
|
||||
The expression can refer only to the variable that the condition applies to,
|
||||
and _must not_ produce errors.
|
||||
|
||||
If the failure of an expression is the basis of the validation decision, use
|
||||
[the `can` function](/docs/from-1.5/functions/conversion/can) to detect such errors. For example:
|
||||
|
||||
```hcl
|
||||
variable "image_id" {
|
||||
type = string
|
||||
description = "The id of the machine image (AMI) to use for the server."
|
||||
|
||||
validation {
|
||||
# regex(...) fails if it cannot find a match
|
||||
condition = can(regex("^ami-", var.image_id))
|
||||
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `condition` evaluates to `false`, an error message including the sentences
|
||||
given in `error_message` will be produced. The error message string should be
|
||||
at least one full sentence explaining the constraint that failed, using a
|
||||
sentence structure similar to the above examples.
|
||||
|
||||
Validation also works with more complex cases:
|
||||
|
||||
```hcl
|
||||
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 = can(var.image_metadata.something.foo)
|
||||
error_message = "The image_metadata.something.foo field must exist."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = substr(var.image_metadata.something.foo, 0, 3) == "bar"
|
||||
error_message = "The image_metadata.something.foo field must start with \"bar\"."
|
||||
}
|
||||
|
||||
}
|
||||
```
|
Loading…
Reference in New Issue