151 lines
5.2 KiB
Go
151 lines
5.2 KiB
Go
|
// Package tryfunc contains some optional functions that can be exposed in
|
||
|
// HCL-based languages to allow authors to test whether a particular expression
|
||
|
// can succeed and take dynamic action based on that result.
|
||
|
//
|
||
|
// These functions are implemented in terms of the customdecode extension from
|
||
|
// the sibling directory "customdecode", and so they are only useful when
|
||
|
// used within an HCL EvalContext. Other systems using cty functions are
|
||
|
// unlikely to support the HCL-specific "customdecode" extension.
|
||
|
package tryfunc
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/hashicorp/hcl/v2"
|
||
|
"github.com/hashicorp/hcl/v2/ext/customdecode"
|
||
|
"github.com/zclconf/go-cty/cty"
|
||
|
"github.com/zclconf/go-cty/cty/function"
|
||
|
)
|
||
|
|
||
|
// TryFunc is a variadic function that tries to evaluate all of is arguments
|
||
|
// in sequence until one succeeds, in which case it returns that result, or
|
||
|
// returns an error if none of them succeed.
|
||
|
var TryFunc function.Function
|
||
|
|
||
|
// CanFunc tries to evaluate the expression given in its first argument.
|
||
|
var CanFunc function.Function
|
||
|
|
||
|
func init() {
|
||
|
TryFunc = function.New(&function.Spec{
|
||
|
VarParam: &function.Parameter{
|
||
|
Name: "expressions",
|
||
|
Type: customdecode.ExpressionClosureType,
|
||
|
},
|
||
|
Type: func(args []cty.Value) (cty.Type, error) {
|
||
|
v, err := try(args)
|
||
|
if err != nil {
|
||
|
return cty.NilType, err
|
||
|
}
|
||
|
return v.Type(), nil
|
||
|
},
|
||
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||
|
return try(args)
|
||
|
},
|
||
|
})
|
||
|
CanFunc = function.New(&function.Spec{
|
||
|
Params: []function.Parameter{
|
||
|
{
|
||
|
Name: "expression",
|
||
|
Type: customdecode.ExpressionClosureType,
|
||
|
},
|
||
|
},
|
||
|
Type: function.StaticReturnType(cty.Bool),
|
||
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||
|
return can(args[0])
|
||
|
},
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func try(args []cty.Value) (cty.Value, error) {
|
||
|
if len(args) == 0 {
|
||
|
return cty.NilVal, errors.New("at least one argument is required")
|
||
|
}
|
||
|
|
||
|
// We'll collect up all of the diagnostics we encounter along the way
|
||
|
// and report them all if none of the expressions succeed, so that the
|
||
|
// user might get some hints on how to make at least one succeed.
|
||
|
var diags hcl.Diagnostics
|
||
|
for _, arg := range args {
|
||
|
closure := customdecode.ExpressionClosureFromVal(arg)
|
||
|
if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
|
||
|
// We can't safely decide if this expression will succeed yet,
|
||
|
// and so our entire result must be unknown until we have
|
||
|
// more information.
|
||
|
return cty.DynamicVal, nil
|
||
|
}
|
||
|
|
||
|
v, moreDiags := closure.Value()
|
||
|
diags = append(diags, moreDiags...)
|
||
|
if moreDiags.HasErrors() {
|
||
|
continue // try the next one, if there is one to try
|
||
|
}
|
||
|
return v, nil // ignore any accumulated diagnostics if one succeeds
|
||
|
}
|
||
|
|
||
|
// If we fall out here then none of the expressions succeeded, and so
|
||
|
// we must have at least one diagnostic and we'll return all of them
|
||
|
// so that the user can see the errors related to whichever one they
|
||
|
// were expecting to have succeeded in this case.
|
||
|
//
|
||
|
// Because our function must return a single error value rather than
|
||
|
// diagnostics, we'll construct a suitable error message string
|
||
|
// that will make sense in the context of the function call failure
|
||
|
// diagnostic HCL will eventually wrap this in.
|
||
|
var buf strings.Builder
|
||
|
buf.WriteString("no expression succeeded:\n")
|
||
|
for _, diag := range diags {
|
||
|
if diag.Subject != nil {
|
||
|
buf.WriteString(fmt.Sprintf("- %s (at %s)\n %s\n", diag.Summary, diag.Subject, diag.Detail))
|
||
|
} else {
|
||
|
buf.WriteString(fmt.Sprintf("- %s\n %s\n", diag.Summary, diag.Detail))
|
||
|
}
|
||
|
}
|
||
|
buf.WriteString("\nAt least one expression must produce a successful result")
|
||
|
return cty.NilVal, errors.New(buf.String())
|
||
|
}
|
||
|
|
||
|
func can(arg cty.Value) (cty.Value, error) {
|
||
|
closure := customdecode.ExpressionClosureFromVal(arg)
|
||
|
if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
|
||
|
// Can't decide yet, then.
|
||
|
return cty.UnknownVal(cty.Bool), nil
|
||
|
}
|
||
|
|
||
|
_, diags := closure.Value()
|
||
|
if diags.HasErrors() {
|
||
|
return cty.False, nil
|
||
|
}
|
||
|
return cty.True, nil
|
||
|
}
|
||
|
|
||
|
// dependsOnUnknowns returns true if any of the variables that the given
|
||
|
// expression might access are unknown values or contain unknown values.
|
||
|
//
|
||
|
// This is a conservative result that prefers to return true if there's any
|
||
|
// chance that the expression might derive from an unknown value during its
|
||
|
// evaluation; it is likely to produce false-positives for more complex
|
||
|
// expressions involving deep data structures.
|
||
|
func dependsOnUnknowns(expr hcl.Expression, ctx *hcl.EvalContext) bool {
|
||
|
for _, traversal := range expr.Variables() {
|
||
|
val, diags := traversal.TraverseAbs(ctx)
|
||
|
if diags.HasErrors() {
|
||
|
// If the traversal returned a definitive error then it must
|
||
|
// not traverse through any unknowns.
|
||
|
continue
|
||
|
}
|
||
|
if !val.IsWhollyKnown() {
|
||
|
// The value will be unknown if either it refers directly to
|
||
|
// an unknown value or if the traversal moves through an unknown
|
||
|
// collection. We're using IsWhollyKnown, so this also catches
|
||
|
// situations where the traversal refers to a compound data
|
||
|
// structure that contains any unknown values. That's important,
|
||
|
// because during evaluation the expression might evaluate more
|
||
|
// deeply into this structure and encounter the unknowns.
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|