diff --git a/docs/functions.md b/docs/functions.md index 7f1795dd4..6bad658a9 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -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 { @@ -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 diff --git a/generate/genfile/genfile.go b/generate/genfile/genfile.go index 3412cb2f0..da144a5ca 100644 --- a/generate/genfile/genfile.go +++ b/generate/genfile/genfile.go @@ -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 { diff --git a/generate/genhcl/genhcl.go b/generate/genhcl/genhcl.go index 560e1f785..2014d6def 100644 --- a/generate/genhcl/genhcl.go +++ b/generate/genhcl/genhcl.go @@ -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 { @@ -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, diff --git a/generate/hcl_expr_func_test.go b/generate/hcl_expr_func_test.go new file mode 100644 index 000000000..e1d2e7bbe --- /dev/null +++ b/generate/hcl_expr_func_test.go @@ -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), + }, + }, + }, + }, + }) +} diff --git a/hcl/eval/eval.go b/hcl/eval/eval.go index c9d1bd5c8..38cf9a65b 100644 --- a/hcl/eval/eval.go +++ b/hcl/eval/eval.go @@ -57,7 +57,7 @@ func NewContext(basedir string) (*Context, error) { } hclctx := &hhcl.EvalContext{ - Functions: newTmFunctions(basedir), + Functions: newDefaultFunctions(basedir), Variables: map[string]cty.Value{}, } return &Context{ @@ -65,13 +65,13 @@ func NewContext(basedir string) (*Context, error) { }, 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, @@ -79,6 +79,11 @@ func (c *Context) SetTmVendor( 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) { diff --git a/hcl/eval/functions.go b/hcl/eval/functions.go index 5c173054d..f34d6b710 100644 --- a/hcl/eval/functions.go +++ b/hcl/eval/functions.go @@ -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() @@ -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 } diff --git a/hcl/eval/functions_test.go b/hcl/eval/functions_test.go index 5edfad3a1..cc770ca09 100644 --- a/hcl/eval/functions_test.go +++ b/hcl/eval/functions_test.go @@ -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{}) @@ -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) diff --git a/hcl/hcl_stack_test.go b/hcl/hcl_stack_test.go index 280c557a7..46599041d 100644 --- a/hcl/hcl_stack_test.go +++ b/hcl/hcl_stack_test.go @@ -21,6 +21,7 @@ import ( "github.com/madlambda/spells/assert" "github.com/mineiros-io/terramate/errors" "github.com/mineiros-io/terramate/hcl" + "github.com/mineiros-io/terramate/hcl/eval" . "github.com/mineiros-io/terramate/test/hclutils" ) @@ -164,6 +165,42 @@ func TestHCLParserStack(t *testing.T) { }, }, }, + { + name: "tm_vendor is not available on stack block", + input: []cfgfile{ + { + filename: "stack.tm", + body: ` + stack{ + name = tm_vendor("github.com/mineiros-io/terramate?ref=v2") + } + `, + }, + }, + want: want{ + errs: []error{ + errors.E(eval.ErrEval), + }, + }, + }, + { + name: "tm_hcl_expression is not available on stack block", + input: []cfgfile{ + { + filename: "stack.tm", + body: ` + stack{ + name = tm_hcl_expression("ref") + } + `, + }, + }, + want: want{ + errs: []error{ + errors.E(eval.ErrEval), + }, + }, + }, { name: "multiple stack blocks", input: []cfgfile{ diff --git a/stack/globals_test.go b/stack/globals_test.go index 7afc49ca1..014829a12 100644 --- a/stack/globals_test.go +++ b/stack/globals_test.go @@ -54,6 +54,8 @@ func TestLoadGlobals(t *testing.T) { } ) + t.Parallel() + tcases := []testcase{ { name: "no stacks no globals", @@ -1444,6 +1446,32 @@ func TestLoadGlobals(t *testing.T) { ), }, }, + { + name: "tm_hcl_expression is not available on globals", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("data", `tm_hcl_expression("test")`), + ), + }, + }, + wantErr: errors.E(globals.ErrEval), + }, + { + name: "tm_vendor is not available on globals", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("data", `tm_vendor("github.com/mineiros-io/terramate?ref=test")`), + ), + }, + }, + wantErr: errors.E(globals.ErrEval), + }, { name: "global interpolating multiple lists fails", layout: []string{"s:stack"}, @@ -2422,8 +2450,11 @@ func TestLoadGlobalsErrors(t *testing.T) { }, } - for _, tcase := range tcases { + for _, tc := range tcases { + tcase := tc t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + s := sandbox.New(t) s.BuildTree(tcase.layout)