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

Added templatestring function similar to templatefile #1223

Merged
merged 22 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d5785e8
added templatestring function
sanskruti-shahu Feb 1, 2024
ede3b19
updated changelog
sanskruti-shahu Feb 1, 2024
4fb1950
implemented requested changes
sanskruti-shahu Feb 4, 2024
a7f9c63
Merge branch 'opentofu:main' into main
sanskruti-shahu Feb 4, 2024
0c54a9f
updated testnames in string_test.go file
sanskruti-shahu Feb 5, 2024
ab81b2d
Merge branch 'main' of https://github.com/sanskruti-shahu/opentofu
sanskruti-shahu Feb 5, 2024
51b1b90
implemented requested changes
sanskruti-shahu Feb 6, 2024
821727d
implemented requested changes: handled sensitive string case
sanskruti-shahu Feb 9, 2024
8554fb8
Merge branch 'opentofu:main' into main
sanskruti-shahu Feb 9, 2024
eb1183e
formatted files
sanskruti-shahu Feb 9, 2024
1cb00e7
Merge branch 'main' into main
sanskruti-shahu Feb 14, 2024
0dc049d
implemented requested changes: removed sensitive string error
sanskruti-shahu Feb 20, 2024
258fadb
Merge branch 'opentofu:main' into main
sanskruti-shahu Feb 20, 2024
58bbdb4
Update internal/lang/funcs/string.go
sanskruti-shahu Feb 21, 2024
de40bab
implemented requested changes: added 'RenderTemplate' func and its tests
sanskruti-shahu Feb 22, 2024
6a9b46b
Merge branch 'opentofu:main' into main
sanskruti-shahu Feb 22, 2024
c7bb8b9
did some minor fixes
sanskruti-shahu Feb 22, 2024
e566425
did some very minor fix
sanskruti-shahu Feb 22, 2024
40da8a5
added comment for TemplateStringFileName
sanskruti-shahu Feb 23, 2024
61c0469
updated CHANGELOG.md
sanskruti-shahu Feb 23, 2024
9295c43
implemented requested changes: some minor changes
sanskruti-shahu Feb 23, 2024
d7dbe91
Merge branch 'opentofu:main' into main
sanskruti-shahu Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ UPGRADE NOTES:
NEW FEATURES:

ENHANCEMENTS:
* Added `templatestring` function. ([#1223](https://github.com/opentofu/opentofu/pull/1223))
* `nonsensitive` function no longer returns error when applied to values that are not sensitive ([#369](https://github.com/opentofu/opentofu/pull/369))
* Managing large local terraform.tfstate files is now much faster. ([#579](https://github.com/opentofu/opentofu/pull/579))
- Previously, every call to state.Write() would also Persist to disk. This was not following the intended API and had longstanding TODOs in the code.
Expand Down
4 changes: 4 additions & 0 deletions internal/lang/funcs/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ var DescriptionList = map[string]descriptionEntry{
Description: "`templatefile` reads the file at the given path and renders its content as a template using a supplied set of template variables.",
ParamDescription: []string{"", ""},
},
"templatestring": {
Description: "`templatestring` processes the provided string as a template using a supplied set of template variables.",
ParamDescription: []string{"", ""},
},
"textdecodebase64": {
Description: "`textdecodebase64` function decodes a string that was previously Base64-encoded, and then interprets the result as characters in a specified character encoding.",
ParamDescription: []string{"", ""},
Expand Down
128 changes: 128 additions & 0 deletions internal/lang/funcs/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"regexp"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
Expand Down Expand Up @@ -164,3 +166,129 @@ func Replace(str, substr, replace cty.Value) (cty.Value, error) {
func StrContains(str, substr cty.Value) (cty.Value, error) {
return StrContainsFunc.Call([]cty.Value{str, substr})
}

// MakeTemplateStringFunc constructs a function that takes a string and
// an arbitrary object of named values and attempts to render that string
// as a template using HCL template syntax.
func MakeTemplateStringFunc(content string, funcsCb func() map[string]function.Function) function.Function {

params := []function.Parameter{
{
Name: "data",
Type: cty.String,
AllowMarked: true,
},
{
Name: "vars",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
}
loadTmpl := func(content string, marks cty.ValueMarks) (hcl.Expression, error) {

// This condition checks if the provided string to be rendered as a template is marked as sensitive.
// If the string is marked as sensitive, it returns an error indicating that sensitive strings cannot be used as template strings.
if strings.Contains(marks.GoString(), "Sensitive") {
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
return nil, function.NewArgErrorf(0, "Sensitive strings cannot be used as template strings. Please ensure that any sensitive information is removed from your template before using them")
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
}

expr, diags := hclsyntax.ParseTemplate([]byte(content), "NoFileNeeded", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
return nil, diags
}

return expr, nil
}

renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time
}

// This loop iterates over each template variable's value and checks if any sensitive values are present.
// If a sensitive value is found, it returns an error indicating that sensitive template variables cannot be used.
for _, vars := range varsVal.AsValueMap() {
_, varsMark := vars.Unmark()

// Check if the variable is marked as sensitive.
// If it is marked as sensitive, return an error.
if strings.Contains(varsMark.GoString(), "Sensitive") {
return cty.DynamicVal, function.NewArgErrorf(1, "Sensitive template variables cannot be used in the template. Please ensure that any sensitive information is removed from your template variables before using them")
}
}

ctx := &hcl.EvalContext{
Variables: varsVal.AsValueMap(),
}

// We require all of the variables to be valid HCL identifiers, because
// otherwise there would be no way to refer to them in the template
// anyway. Rejecting this here gives better feedback to the user
// than a syntax error somewhere in the template itself.
for n := range ctx.Variables {
if !hclsyntax.ValidIdentifier(n) {
// This error message intentionally doesn't describe _all_ of
// the different permutations that are technically valid as an
// HCL identifier, but rather focuses on what we might
// consider to be an "idiomatic" variable name.
return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n)
}
}

// We'll pre-check references in the template here so we can give a
// more specialized error message than HCL would by default, so it's
// clearer that this problem is coming from a templatestring call.
for _, traversal := range expr.Variables() {
root := traversal.RootName()
if _, ok := ctx.Variables[root]; !ok {
return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q", root)
}
}

givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
funcs := make(map[string]function.Function, len(givenFuncs))
for name, fn := range givenFuncs {
funcs[name] = fn
}
ctx.Functions = funcs

val, diags := expr.Value(ctx)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
return val, nil
}

return function.New(&function.Spec{
Params: params,
Type: func(args []cty.Value) (cty.Type, error) {
if !(args[0].IsKnown() && args[1].IsKnown()) {
return cty.DynamicPseudoType, nil
}

// We'll render our template now to see what result type it produces.
// A template consisting only of a single interpolation an potentially
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
// return any type.

pathArg, pathMarks := args[0].Unmark()
expr, err := loadTmpl(pathArg.AsString(), pathMarks)
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return cty.DynamicPseudoType, err
}

// This is safe even if args[1] contains unknowns because the HCL
// template renderer itself knows how to short-circuit those.
val, err := renderTmpl(expr, args[1])
return val.Type(), err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
pathArg, pathMarks := args[0].Unmark()
expr, err := loadTmpl(pathArg.AsString(), pathMarks)
if err != nil {
return cty.DynamicVal, err
}
result, err := renderTmpl(expr, args[1])
return result.WithMarks(pathMarks), err
},
})
}
118 changes: 118 additions & 0 deletions internal/lang/funcs/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"fmt"
"testing"

"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

func TestReplace(t *testing.T) {
Expand Down Expand Up @@ -255,3 +257,119 @@ func TestStartsWith(t *testing.T) {
})
}
}

func TestTemplateString(t *testing.T) {
tests := map[string]struct {
Content cty.Value
Vars cty.Value
Want cty.Value
Err string
}{
"Simple string template": {
cty.StringVal("Hello, Jodie!"),
cty.EmptyObjectVal,
cty.StringVal("Hello, Jodie!"),
``,
},
"String interpolation with variable": {
cty.StringVal("Hello, ${name}!"),
cty.MapVal(map[string]cty.Value{
"name": cty.StringVal("Jodie"),
}),
cty.StringVal("Hello, Jodie!"),
``,
},
"Looping through list": {
cty.StringVal("Items: %{ for x in list ~} ${x} %{ endfor ~}"),
cty.ObjectVal(map[string]cty.Value{
"list": cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.StringVal("Items: a b c "),
``,
},
"Looping through map": {
cty.StringVal("%{ for key, value in list ~} ${key}:${value} %{ endfor ~}"),
cty.ObjectVal(map[string]cty.Value{
"list": cty.ObjectVal(map[string]cty.Value{
"item1": cty.StringVal("a"),
"item2": cty.StringVal("b"),
"item3": cty.StringVal("c"),
}),
}),
cty.StringVal("item1:a item2:b item3:c "),
``,
},
"Invalid template variable name": {
cty.StringVal("Hello, ${1}!"),
cty.MapVal(map[string]cty.Value{
"1": cty.StringVal("Jodie"),
}),
cty.NilVal,
`invalid template variable name "1": must start with a letter, followed by zero or more letters, digits, and underscores`,
},
"Variable not present in vars map": {
cty.StringVal("Hello, ${name}!"),
cty.EmptyObjectVal,
cty.NilVal,
`vars map does not contain key "name"`,
},
"Interpolation of a boolean value": {
cty.StringVal("${val}"),
cty.ObjectVal(map[string]cty.Value{
"val": cty.True,
}),
cty.True,
``,
},
sanskruti-shahu marked this conversation as resolved.
Show resolved Hide resolved
"Sensitive string template": {
cty.StringVal("My password is 1234").Mark(marks.Sensitive),
cty.EmptyObjectVal,
cty.NilVal,
`Sensitive strings cannot be used as template strings. Please ensure that any sensitive information is removed from your template before using them`,
},
"Sensitive template variable": {
cty.StringVal("My password is ${pass}"),
cty.ObjectVal(map[string]cty.Value{
"pass": cty.StringVal("secret").Mark(marks.Sensitive),
}),
cty.NilVal,
`Sensitive template variables cannot be used in the template. Please ensure that any sensitive information is removed from your template variables before using them`,
},
}

templateStringFn := MakeTemplateStringFunc(".", func() map[string]function.Function {
return map[string]function.Function{}
})

for _, test := range tests {
t.Run(fmt.Sprintf("TemplateString(%#v, %#v)", test.Content, test.Vars), func(t *testing.T) {
got, err := templateStringFn.Call([]cty.Value{test.Content, test.Vars})

if argErr, ok := err.(function.ArgError); ok {
if argErr.Index < 0 || argErr.Index > 1 {
t.Errorf("ArgError index %d is out of range for templatestring (must be 0 or 1)", argErr.Index)
}
}

if err != nil {
if test.Err == "" {
t.Fatalf("unexpected error: %s", err)
} else {
if got, want := err.Error(), test.Err; got != want {
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
}
}
} else if test.Err != "" {
t.Fatal("succeeded; want error")
} else {
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
}
})
}
}
6 changes: 6 additions & 0 deletions internal/lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ func (s *Scope) Functions() map[string]function.Function {
return s.funcs
})

// Registers "templatestring" function in function map.
s.funcs["templatestring"] = funcs.MakeTemplateStringFunc(s.BaseDir, func() map[string]function.Function {
// This anonymous function returns the existing map of functions for initialization.
return s.funcs
})

if s.ConsoleMode {
// The type function is only available in OpenTofu console.
s.funcs["type"] = funcs.TypeFunc
Expand Down
7 changes: 7 additions & 0 deletions internal/lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,13 @@ func TestFunctions(t *testing.T) {
},
},

"templatestring": {
{
`templatestring("Hello, $${name}!", {name = "Jodie"})`,
cty.StringVal("Hello, Jodie!"),
},
},

"timeadd": {
{
`timeadd("2017-11-22T00:00:00Z", "1s")`,
Expand Down
5 changes: 5 additions & 0 deletions website/data/language-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,11 @@
"path": "language/functions/templatefile",
"hidden": true
},
{
"title": "templatestring",
"path": "language/functions/templatestring",
"hidden": true
},
{
"title": "textdecodebase64",
"path": "language/functions/textdecodebase64",
Expand Down