| // 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 |
| } |