Skip to content

Commit a9f8d65

Browse files
authored
Merge pull request #622 from hashicorp/jbardin/can-try-unknowns
Allow `can` and `try` functions to return known results with `unknown` values in the arguments
2 parents 527ec31 + 6af2870 commit a9f8d65

File tree

2 files changed

+62
-41
lines changed

2 files changed

+62
-41
lines changed

ext/tryfunc/tryfunc.go

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,35 @@ func try(args []cty.Value) (cty.Value, error) {
7272
var diags hcl.Diagnostics
7373
for _, arg := range args {
7474
closure := customdecode.ExpressionClosureFromVal(arg)
75-
if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
76-
// We can't safely decide if this expression will succeed yet,
77-
// and so our entire result must be unknown until we have
78-
// more information.
79-
return cty.DynamicVal, nil
80-
}
8175

8276
v, moreDiags := closure.Value()
8377
diags = append(diags, moreDiags...)
78+
8479
if moreDiags.HasErrors() {
85-
continue // try the next one, if there is one to try
80+
// If there's an error we know it will always fail and can
81+
// continue. A more refined value will not remove an error from
82+
// the expression.
83+
continue
8684
}
85+
86+
if !v.IsWhollyKnown() {
87+
// If there are any unknowns in the value at all, we cannot be
88+
// certain that the final value will be consistent or have the same
89+
// type, so wee need to be conservative and return a dynamic value.
90+
91+
// There are two different classes of failure that can happen when
92+
// an expression transitions from unknown to known; an operation on
93+
// a dynamic value becomes invalid for the type once the type is
94+
// known, or an index expression on a collection fails once the
95+
// collection value is known. These changes from a
96+
// valid-partially-unknown expression to an invalid-known
97+
// expression can produce inconsistent results by changing which
98+
// "try" argument is returned, which may be a collection with
99+
// different previously known values, or a different type entirely
100+
// ("try" does not require consistent argument types)
101+
return cty.DynamicVal, nil
102+
}
103+
87104
return v, nil // ignore any accumulated diagnostics if one succeeds
88105
}
89106

@@ -111,43 +128,17 @@ func try(args []cty.Value) (cty.Value, error) {
111128

112129
func can(arg cty.Value) (cty.Value, error) {
113130
closure := customdecode.ExpressionClosureFromVal(arg)
114-
if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
115-
// Can't decide yet, then.
116-
return cty.UnknownVal(cty.Bool), nil
117-
}
118-
119-
_, diags := closure.Value()
131+
v, diags := closure.Value()
120132
if diags.HasErrors() {
121133
return cty.False, nil
122134
}
123-
return cty.True, nil
124-
}
125135

126-
// dependsOnUnknowns returns true if any of the variables that the given
127-
// expression might access are unknown values or contain unknown values.
128-
//
129-
// This is a conservative result that prefers to return true if there's any
130-
// chance that the expression might derive from an unknown value during its
131-
// evaluation; it is likely to produce false-positives for more complex
132-
// expressions involving deep data structures.
133-
func dependsOnUnknowns(expr hcl.Expression, ctx *hcl.EvalContext) bool {
134-
for _, traversal := range expr.Variables() {
135-
val, diags := traversal.TraverseAbs(ctx)
136-
if diags.HasErrors() {
137-
// If the traversal returned a definitive error then it must
138-
// not traverse through any unknowns.
139-
continue
140-
}
141-
if !val.IsWhollyKnown() {
142-
// The value will be unknown if either it refers directly to
143-
// an unknown value or if the traversal moves through an unknown
144-
// collection. We're using IsWhollyKnown, so this also catches
145-
// situations where the traversal refers to a compound data
146-
// structure that contains any unknown values. That's important,
147-
// because during evaluation the expression might evaluate more
148-
// deeply into this structure and encounter the unknowns.
149-
return true
150-
}
136+
if !v.IsWhollyKnown() {
137+
// If the value is not wholly known, we still cannot be certain that
138+
// the expression was valid. There may be yet index expressions which
139+
// will fail once values are completely known.
140+
return cty.UnknownVal(cty.Bool), nil
151141
}
152-
return false
142+
143+
return cty.True, nil
153144
}

ext/tryfunc/tryfunc_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,36 @@ func TestTryFunc(t *testing.T) {
107107
cty.StringVal("list").Mark("secret"),
108108
``,
109109
},
110+
"nested known expression from unknown": {
111+
// this expression contains an unknown, but will always return in
112+
// "bar"
113+
`try({u: false ? unknown : "bar"}, other)`,
114+
map[string]cty.Value{
115+
"unknown": cty.UnknownVal(cty.String),
116+
"other": cty.MapVal(map[string]cty.Value{
117+
"v": cty.StringVal("oops"),
118+
}),
119+
},
120+
cty.ObjectVal(map[string]cty.Value{
121+
"u": cty.StringVal("bar"),
122+
}),
123+
``,
124+
},
125+
"nested index op on unknown": {
126+
// unknown and other have identical types, but we must return a
127+
// dynamic value since v could change within the final result value
128+
// after the first argument becomes known.
129+
`try({u: unknown["foo"], v: "orig"}, other)`,
130+
map[string]cty.Value{
131+
"unknown": cty.UnknownVal(cty.Map(cty.String)),
132+
"other": cty.MapVal(map[string]cty.Value{
133+
"u": cty.StringVal("oops"),
134+
"v": cty.StringVal("oops"),
135+
}),
136+
},
137+
cty.DynamicVal,
138+
``,
139+
},
110140
"three arguments, all fail": {
111141
`try(this, that, this_thing_in_particular)`,
112142
nil,

0 commit comments

Comments
 (0)