1// Package tryfunc contains some optional functions that can be exposed in 2// HCL-based languages to allow authors to test whether a particular expression 3// can succeed and take dynamic action based on that result. 4// 5// These functions are implemented in terms of the customdecode extension from 6// the sibling directory "customdecode", and so they are only useful when 7// used within an HCL EvalContext. Other systems using cty functions are 8// unlikely to support the HCL-specific "customdecode" extension. 9package tryfunc 10 11import ( 12 "errors" 13 "fmt" 14 "strings" 15 16 "github.com/hashicorp/hcl/v2" 17 "github.com/hashicorp/hcl/v2/ext/customdecode" 18 "github.com/zclconf/go-cty/cty" 19 "github.com/zclconf/go-cty/cty/function" 20) 21 22// TryFunc is a variadic function that tries to evaluate all of is arguments 23// in sequence until one succeeds, in which case it returns that result, or 24// returns an error if none of them succeed. 25var TryFunc function.Function 26 27// CanFunc tries to evaluate the expression given in its first argument. 28var CanFunc function.Function 29 30func init() { 31 TryFunc = function.New(&function.Spec{ 32 VarParam: &function.Parameter{ 33 Name: "expressions", 34 Type: customdecode.ExpressionClosureType, 35 }, 36 Type: func(args []cty.Value) (cty.Type, error) { 37 v, err := try(args) 38 if err != nil { 39 return cty.NilType, err 40 } 41 return v.Type(), nil 42 }, 43 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 44 return try(args) 45 }, 46 }) 47 CanFunc = function.New(&function.Spec{ 48 Params: []function.Parameter{ 49 { 50 Name: "expression", 51 Type: customdecode.ExpressionClosureType, 52 }, 53 }, 54 Type: function.StaticReturnType(cty.Bool), 55 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 56 return can(args[0]) 57 }, 58 }) 59} 60 61func try(args []cty.Value) (cty.Value, error) { 62 if len(args) == 0 { 63 return cty.NilVal, errors.New("at least one argument is required") 64 } 65 66 // We'll collect up all of the diagnostics we encounter along the way 67 // and report them all if none of the expressions succeed, so that the 68 // user might get some hints on how to make at least one succeed. 69 var diags hcl.Diagnostics 70 for _, arg := range args { 71 closure := customdecode.ExpressionClosureFromVal(arg) 72 if dependsOnUnknowns(closure.Expression, closure.EvalContext) { 73 // We can't safely decide if this expression will succeed yet, 74 // and so our entire result must be unknown until we have 75 // more information. 76 return cty.DynamicVal, nil 77 } 78 79 v, moreDiags := closure.Value() 80 diags = append(diags, moreDiags...) 81 if moreDiags.HasErrors() { 82 continue // try the next one, if there is one to try 83 } 84 return v, nil // ignore any accumulated diagnostics if one succeeds 85 } 86 87 // If we fall out here then none of the expressions succeeded, and so 88 // we must have at least one diagnostic and we'll return all of them 89 // so that the user can see the errors related to whichever one they 90 // were expecting to have succeeded in this case. 91 // 92 // Because our function must return a single error value rather than 93 // diagnostics, we'll construct a suitable error message string 94 // that will make sense in the context of the function call failure 95 // diagnostic HCL will eventually wrap this in. 96 var buf strings.Builder 97 buf.WriteString("no expression succeeded:\n") 98 for _, diag := range diags { 99 if diag.Subject != nil { 100 buf.WriteString(fmt.Sprintf("- %s (at %s)\n %s\n", diag.Summary, diag.Subject, diag.Detail)) 101 } else { 102 buf.WriteString(fmt.Sprintf("- %s\n %s\n", diag.Summary, diag.Detail)) 103 } 104 } 105 buf.WriteString("\nAt least one expression must produce a successful result") 106 return cty.NilVal, errors.New(buf.String()) 107} 108 109func can(arg cty.Value) (cty.Value, error) { 110 closure := customdecode.ExpressionClosureFromVal(arg) 111 if dependsOnUnknowns(closure.Expression, closure.EvalContext) { 112 // Can't decide yet, then. 113 return cty.UnknownVal(cty.Bool), nil 114 } 115 116 _, diags := closure.Value() 117 if diags.HasErrors() { 118 return cty.False, nil 119 } 120 return cty.True, nil 121} 122 123// dependsOnUnknowns returns true if any of the variables that the given 124// expression might access are unknown values or contain unknown values. 125// 126// This is a conservative result that prefers to return true if there's any 127// chance that the expression might derive from an unknown value during its 128// evaluation; it is likely to produce false-positives for more complex 129// expressions involving deep data structures. 130func dependsOnUnknowns(expr hcl.Expression, ctx *hcl.EvalContext) bool { 131 for _, traversal := range expr.Variables() { 132 val, diags := traversal.TraverseAbs(ctx) 133 if diags.HasErrors() { 134 // If the traversal returned a definitive error then it must 135 // not traverse through any unknowns. 136 continue 137 } 138 if !val.IsWhollyKnown() { 139 // The value will be unknown if either it refers directly to 140 // an unknown value or if the traversal moves through an unknown 141 // collection. We're using IsWhollyKnown, so this also catches 142 // situations where the traversal refers to a compound data 143 // structure that contains any unknown values. That's important, 144 // because during evaluation the expression might evaluate more 145 // deeply into this structure and encounter the unknowns. 146 return true 147 } 148 } 149 return false 150} 151