Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: panic when using tm_hcl_expression() on invalid contexts #731

Merged
merged 8 commits into from
Nov 25, 2022
Merged
16 changes: 12 additions & 4 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ contents as an expression. It is particularly useful to circumvent some
limitations on HCL and Terraform when building complex expressions from
dynamic data.

For example, given a global named data defined like this:
Since this function produces an expression, not a final evaluated value,
it is only allowed to be used on contexts where partial evaluation is
allowed, which currently is only the `generate_hcl.content` block.

To use `tm_hcl_expression`, lets say we have a global named data defined like this:

```
globals {
Expand All @@ -67,13 +71,17 @@ You can use this global to build a complex expression when generation code,
like this:

```hcl
tm_hcl_expression("data.google_active_folder._parent_id.id.${global.data}")
generate_hcl "test.hcl" {
content {
expr = tm_hcl_expression("data.google_active_folder._parent_id.id.${global.data}")
}
}
```

Which will produce the expression:
Which will generate:

```hcl
data.google_active_folder._parent_id.id.data
expr = data.google_active_folder._parent_id.id.data
```

## Experimental Functions
Expand Down
2 changes: 1 addition & 1 deletion generate/genfile/genfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func Load(
vendorTargetDir := project.NewPath(path.Join(
sm.Path().String(),
path.Dir(name)))
evalctx.SetTmVendor(vendorTargetDir, vendorDir, vendorRequests)
evalctx.AddTmVendor(vendorTargetDir, vendorDir, vendorRequests)

file, err := Eval(genFileBlock, evalctx.Context)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion generate/genhcl/genhcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func Load(
vendorTargetDir := project.NewPath(path.Join(
sm.Path().String(),
path.Dir(name)))
evalctx.SetTmVendor(vendorTargetDir, vendorDir, vendorRequests)
evalctx.AddTmVendor(vendorTargetDir, vendorDir, vendorRequests)

err := lets.Load(hclBlock.Lets, evalctx.Context)
if err != nil {
Expand Down Expand Up @@ -233,6 +233,8 @@ func Load(
continue
}

evalctx.AddTmHCLExpression()

gen := hclwrite.NewEmptyFile()
if err := copyBody(gen.Body(), hclBlock.Content.Body, evalctx); err != nil {
return nil, errors.E(ErrContentEval, sm, err,
Expand Down
238 changes: 238 additions & 0 deletions generate/hcl_expr_func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Copyright 2022 Mineiros GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package generate_test

import (
"testing"

"github.com/mineiros-io/terramate/errors"
"github.com/mineiros-io/terramate/generate"
"github.com/mineiros-io/terramate/hcl/eval"

. "github.com/mineiros-io/terramate/test/hclwrite/hclutils"
)

func TestHCLExpressionFunc(t *testing.T) {
// TODO(KATCIPIS): currently most behavior is tested on the genhcl pkg.
// In the future tests could be moved here.
testCodeGeneration(t, []testcase{
{
name: "not available on generate_hcl lets block",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateHCL(
Labels("test.hcl"),
Lets(
Expr("expr", `tm_hcl_expression("test")`),
),
Content(
Expr("value", `let.expr`),
),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
name: "not available on generate_hcl condition",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateHCL(
Labels("test.hcl"),
Expr("condition", `tm_hcl_expression("test")`),
Content(),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
name: "not available on generate_hcl assert",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateHCL(
Labels("test.hcl"),
Assert(
Expr("assertion", `tm_hcl_expression("true")`),
Str("message", "msg"),
),
Content(),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
name: "not available on generate_file condition",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateFile(
Labels("test.txt"),
Expr("condition", `tm_hcl_expression("test")`),
Str("content", "content"),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
name: "not available on generate_file assert",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateFile(
Labels("test.txt"),
Assert(
Expr("assertion", `tm_hcl_expression("true")`),
Str("message", "msg"),
),
Str("content", "content"),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
name: "not available on generate_file lets block",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: Doc(
GenerateFile(
Labels("test.txt"),
Lets(
Expr("content", `tm_hcl_expression("test")`),
),
Expr("content", "let.content"),
),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
{
// There is no way to interpolate the expression on a string template
name: "not available on generate_file content",
layout: []string{
"s:stack",
},
configs: []hclconfig{
{
path: "/stack",
add: GenerateFile(
Labels("expr.txt"),
Str("content", `generated: ${tm_hcl_expression("data")}`),
),
},
},
wantReport: generate.Report{
Failures: []generate.FailureResult{
{
Result: generate.Result{
Dir: "/stack",
},
Error: errors.E(eval.ErrEval),
},
},
},
},
})
}
11 changes: 8 additions & 3 deletions hcl/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,33 @@ func NewContext(basedir string) (*Context, error) {
}

hclctx := &hhcl.EvalContext{
Functions: newTmFunctions(basedir),
Functions: newDefaultFunctions(basedir),
Variables: map[string]cty.Value{},
}
return &Context{
hclctx: hclctx,
}, nil
}

// SetTmVendor sets the tm_vendor function on this evaluation context.
// AddTmVendor adds the tm_vendor function on this evaluation context.
// The targetdir defines what tm_vendor will use to define the relative paths
// of vendored dependencies.
// The vendordir defines where modules are vendored inside the project.
// The stream defines the event stream for tm_vendor, one event is produced
// per successful function call.
func (c *Context) SetTmVendor(
func (c *Context) AddTmVendor(
targetdir project.Path,
vendordir project.Path,
stream chan<- event.VendorRequest,
) {
c.hclctx.Functions["tm_vendor"] = tmVendor(targetdir, vendordir, stream)
}

// AddTmHCLExpression adds the tm_hcl_expression function on this evaluation context.
func (c *Context) AddTmHCLExpression() {
c.hclctx.Functions["tm_hcl_expression"] = tmHCLExpression()
}

// 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) {
Expand Down
3 changes: 1 addition & 2 deletions hcl/eval/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
"github.com/zclconf/go-cty/cty/function"
)

func newTmFunctions(basedir string) map[string]function.Function {
func newDefaultFunctions(basedir string) map[string]function.Function {
scope := &tflang.Scope{BaseDir: basedir}
tffuncs := scope.Functions()

Expand All @@ -44,7 +44,6 @@ func newTmFunctions(basedir string) map[string]function.Function {

// sane ternary
tmfuncs["tm_ternary"] = tmTernary()
tmfuncs["tm_hcl_expression"] = tmHCLExpression()
return tmfuncs
}

Expand Down
4 changes: 2 additions & 2 deletions hcl/eval/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestTmVendor(t *testing.T) {
ctx, err := eval.NewContext(rootdir)
assert.NoError(t, err)

ctx.SetTmVendor(targetdir, vendordir, events)
ctx.AddTmVendor(targetdir, vendordir, events)

gotEvents := []event.VendorRequest{}
done := make(chan struct{})
Expand Down Expand Up @@ -170,7 +170,7 @@ func TestTmVendor(t *testing.T) {
ctx, err := eval.NewContext(rootdir)
assert.NoError(t, err)

ctx.SetTmVendor(targetdir, vendordir, nil)
ctx.AddTmVendor(targetdir, vendordir, nil)

val, err := ctx.Eval(test.NewExpr(t, tcase.expr))
assert.NoError(t, err)
Expand Down
Loading