add basic code for variable validation parsing
* hcl2template/addrs.ParseRef will parse a reference and tell for example if we are referring to a variable and its name, for now it can only do that and in the future it improved when we need to most of it is from the TF code. This is used to tell wether a variable vas referenced in a variable validation condition; for now. * Added Validations blocks to the hcl2 Variable struct and code to parse/validate that.
This commit is contained in:
parent
69312458c4
commit
91d7332471
|
@ -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,18 @@
|
|||
package addrs
|
||||
|
||||
// Referenceable is an interface implemented by all address types that can
|
||||
// appear as references in configuration language expressions.
|
||||
type Referenceable interface {
|
||||
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
|
||||
}
|
||||
|
||||
type referenceable struct {
|
||||
}
|
||||
|
||||
func (r referenceable) referenceableSigil() {
|
||||
}
|
|
@ -3,15 +3,20 @@ package hcl2template
|
|||
import (
|
||||
"fmt"
|
||||
"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.
|
||||
|
@ -47,6 +52,8 @@ type Variable struct {
|
|||
// the variable from the output stream. By replacing the text.
|
||||
Sensitive bool
|
||||
|
||||
Validations []*VariableValidation
|
||||
|
||||
Range hcl.Range
|
||||
}
|
||||
|
||||
|
@ -139,6 +146,28 @@ func (variables *Variables) decodeVariable(key string, attr *hcl.Attribute, ectx
|
|||
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 +184,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 +242,177 @@ func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.Eval
|
|||
}
|
||||
}
|
||||
|
||||
res.DefaultValue = defaultValue
|
||||
v.DefaultValue = defaultValue
|
||||
|
||||
// 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 = v.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 would need to 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 in
|
||||
// English, 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
|
||||
// Terraform 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 Terraform UI
|
||||
// writing style.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: errSummary,
|
||||
Detail: "Validation error message must be at least one full English sentence starting with an uppercase letter 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.
|
||||
// (This will only see the first rune in a multi-rune combining sequence,
|
||||
// but the first rune is generally the letter if any are, and if not then
|
||||
// we'll just ignore it because we're primarily expecting English messages
|
||||
// right now anyway, for consistency with all of Terraform's other output.)
|
||||
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_"
|
||||
|
|
|
@ -131,7 +131,7 @@ func TestParse_variables(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
true, false,
|
||||
true, true,
|
||||
[]packer.Build{},
|
||||
false,
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue