Skip to content

Commit

Permalink
fix(stdlib): tm_ternary() does not track expression scoped vars.
Browse files Browse the repository at this point in the history
Some variables are scoped by their expression declaration, that's the
case for `[for ...]`, `{for ...}` and `tm_dynamic`.
In this case, the `tm_ternary()` implementation must use the provided
closure evaluator variables instead of the outer Terramate evaluator.

Signed-off-by: Tiago Natel <[email protected]>
  • Loading branch information
i4ki committed Dec 5, 2023
1 parent 798e539 commit 0f92726
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 18 deletions.
20 changes: 20 additions & 0 deletions globals/globals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,26 @@ func TestLoadGlobals(t *testing.T) {
),
},
},
{
name: "global with tm_ternary with condition from for-loop",
layout: []string{"s:stack"},
configs: []hclconfig{
{
path: "/stack",
add: Globals(
Expr("val", `[for a in ["a", "b", "c"] : tm_ternary(
a == "a",
tm_upper(a),
"")]`),
),
},
},
want: map[string]*hclwrite.Block{
"/stack": Globals(
Expr("val", `["A", "", ""]`),
),
},
},
{
name: "global with tm_ternary with different branch types",
layout: []string{"s:stack"},
Expand Down
36 changes: 18 additions & 18 deletions hcl/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const (
type (
// Context is the variables evaluator.
Context struct {
scope project.Path
hclctx *hhcl.EvalContext
ns namespaces
scope project.Path
Internal *hhcl.EvalContext
ns namespaces

evaluators map[string]Resolver
}
Expand Down Expand Up @@ -76,7 +76,7 @@ func New(scope project.Path, evaluators ...Resolver) *Context {
}
evalctx := &Context{
scope: scope,
hclctx: hclctx,
Internal: hclctx,
evaluators: map[string]Resolver{},
ns: namespaces{},
}
Expand All @@ -86,7 +86,7 @@ func New(scope project.Path, evaluators ...Resolver) *Context {
}

unsetVal := cty.CapsuleVal(unset, &struct{}{})
evalctx.hclctx.Variables["unset"] = unsetVal
evalctx.Internal.Variables["unset"] = unsetVal

return evalctx
}
Expand All @@ -108,14 +108,14 @@ func (c *Context) SetResolver(ev Resolver) {
}
}
} else {
c.hclctx.Variables[ev.Name()] = prevalue
c.Internal.Variables[ev.Name()] = prevalue
}
}

// DeleteResolver removes the resolver.
func (c *Context) DeleteResolver(name string) {
delete(c.evaluators, name)
delete(c.hclctx.Variables, name)
delete(c.Internal.Variables, name)
}

// Eval the given expr and all of its dependency references (if needed)
Expand Down Expand Up @@ -207,7 +207,7 @@ func (c *Context) eval(expr hhcl.Expression, visited map[RefStr]hhcl.Expression)
}
}

val, diags := expr.Value(c.hclctx)
val, diags := expr.Value(c.Internal)
if diags.HasErrors() {
return cty.NilVal, errors.E(ErrEval, diags)
}
Expand Down Expand Up @@ -412,33 +412,33 @@ func (ns namespaces) Get(ref Ref) (value, bool) {
// SetNamespace will set the given values inside the given namespace on the
// evaluation context.
func (c *Context) SetNamespace(name string, vals map[string]cty.Value) {
c.hclctx.Variables[name] = cty.ObjectVal(vals)
c.Internal.Variables[name] = cty.ObjectVal(vals)
}

// SetFunction sets the function in the context.
func (c *Context) SetFunction(name string, fn function.Function) {
c.hclctx.Functions[name] = fn
c.Internal.Functions[name] = fn
}

// DeleteFunction deletes the given function from the context.
func (c *Context) DeleteFunction(name string) {
delete(c.hclctx.Functions, name)
delete(c.Internal.Functions, name)
}

// SetFunctions sets the functions of the context.
func (c *Context) SetFunctions(funcs map[string]function.Function) {
c.hclctx.Functions = funcs
c.Internal.Functions = funcs
}

// DeleteNamespace deletes the namespace name from the context.
// If name is not in the context, it's a no-op.
func (c *Context) DeleteNamespace(name string) {
delete(c.hclctx.Variables, name)
delete(c.Internal.Variables, name)
}

// HasNamespace returns true the evaluation context knows this namespace, false otherwise.
func (c *Context) HasNamespace(name string) bool {
_, has := c.hclctx.Variables[name]
_, has := c.Internal.Variables[name]
return has
}

Expand All @@ -459,22 +459,22 @@ func (c *Context) Copy() *Context {
newctx := &hhcl.EvalContext{
Variables: map[string]cty.Value{},
}
newctx.Functions = c.hclctx.Functions
for k, v := range c.hclctx.Variables {
newctx.Functions = c.Internal.Functions
for k, v := range c.Internal.Variables {
newctx.Variables[k] = v
}
return NewContextFrom(newctx)
}

// Unwrap returns the internal hhcl.EvalContext.
func (c *Context) Unwrap() *hhcl.EvalContext {
return c.hclctx
return c.Internal
}

// NewContextFrom creates a new evaluator from the hashicorp EvalContext.
func NewContextFrom(ctx *hhcl.EvalContext) *Context {
return &Context{
hclctx: ctx,
Internal: ctx,
}
}

Expand Down
6 changes: 6 additions & 0 deletions stdlib/ternary.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,15 @@ func ternary(evalctx *eval.Context, cond cty.Value, val1, val2 cty.Value) (cty.V

func evalTernaryBranch(evalctx *eval.Context, arg cty.Value) (cty.Value, error) {
closure := customdecode.ExpressionClosureFromVal(arg)

// some HCL language construct declare variables and pass them down the
// context tree, then we need to use the expression own underlying EvalContext when available.
bk := evalctx.Internal
evalctx.Internal = closure.EvalContext
newexpr, err := evalctx.PartialEval(&ast.CloneExpression{
Expression: closure.Expression.(hclsyntax.Expression),
})
evalctx.Internal = bk
if err != nil {
return cty.NilVal, errors.E(err, "evaluating tm_ternary branch")
}
Expand Down

0 comments on commit 0f92726

Please sign in to comment.