From 1e2d4a2ecc695a5dbcc53a315458622878ab19da Mon Sep 17 00:00:00 2001 From: kmoe <5575356+kmoe@users.noreply.github.com> Date: Tue, 28 May 2024 16:50:25 +0100 Subject: [PATCH] lang: stabilise templatestring func experiment (#35224) * lang: stabilise templatestring func experiment * command/jsonfunction: marshal templatestring * docs: add templatestring --- internal/command/jsonfunction/function.go | 27 ++++++++ internal/experiments/experiment.go | 2 +- internal/lang/funcs/descriptions.go | 4 +- internal/lang/functions.go | 4 +- internal/lang/functions_test.go | 23 ++++++- website/data/language-nav-data.json | 9 +++ .../docs/language/functions/templatefile.mdx | 1 + .../language/functions/templatestring.mdx | 67 +++++++++++++++++++ 8 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 website/docs/language/functions/templatestring.mdx diff --git a/internal/command/jsonfunction/function.go b/internal/command/jsonfunction/function.go index 9cedf4e1db..83cc59c8e7 100644 --- a/internal/command/jsonfunction/function.go +++ b/internal/command/jsonfunction/function.go @@ -86,6 +86,8 @@ func Marshal(f map[string]function.Function) ([]byte, tfdiags.Diagnostics) { signatures.Signatures[name] = marshalCan(v) } else if name == "try" || name == "core::try" { signatures.Signatures[name] = marshalTry(v) + } else if name == "templatestring" || name == "core::templatestring" { + signatures.Signatures[name] = marshalTemplatestring(v) } else { signature, err := marshalFunction(v) if err != nil { @@ -194,3 +196,28 @@ func marshalCan(can function.Function) *FunctionSignature { }, } } + +// marshalTemplatestring returns a static function signature for the +// templatestring function. +// We need this exception because the function implementation uses capsule +// types that we can't marshal. +func marshalTemplatestring(templatestring function.Function) *FunctionSignature { + return &FunctionSignature{ + Description: templatestring.Description(), + ReturnType: cty.String, + Parameters: []*parameter{ + { + Name: templatestring.Params()[0].Name, + Description: templatestring.Params()[0].Description, + IsNullable: templatestring.Params()[0].AllowNull, + Type: cty.String, + }, + { + Name: templatestring.Params()[1].Name, + Description: templatestring.Params()[1].Description, + IsNullable: templatestring.Params()[1].AllowNull, + Type: cty.DynamicPseudoType, + }, + }, + } +} diff --git a/internal/experiments/experiment.go b/internal/experiments/experiment.go index 8a7b770962..637a91b3de 100644 --- a/internal/experiments/experiment.go +++ b/internal/experiments/experiment.go @@ -33,7 +33,7 @@ func init() { registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") registerConcludedExperiment(VariableValidationCrossRef, "Input variable validation rules may now refer to other objects in the same module without enabling any experiment.") registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.") - registerCurrentExperiment(TemplateStringFunc) + registerConcludedExperiment(TemplateStringFunc, "The templatestring function can now be used without enabling an experiment.") registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.") registerConcludedExperiment(PreconditionsPostconditions, "Condition blocks can now be used by default, without enabling an experiment.") registerConcludedExperiment(ModuleVariableOptionalAttrs, "The final feature corresponding to this experiment differs from the experimental form and is available in the Terraform language from Terraform v1.3.0 onwards.") diff --git a/internal/lang/funcs/descriptions.go b/internal/lang/funcs/descriptions.go index 1967793ea7..e21dad10ad 100644 --- a/internal/lang/funcs/descriptions.go +++ b/internal/lang/funcs/descriptions.go @@ -423,8 +423,8 @@ var DescriptionList = map[string]descriptionEntry{ "templatestring": { Description: "`templatestring` takes a string from elsewhere in the module and renders its content as a template using a supplied set of template variables.", ParamDescription: []string{ - "a simple reference to a string value containing the template source code", - "object of variables to expose in the template scope", + "A simple reference to a string value containing the template source code.", + "Object of variables to expose in the template scope.", }, }, "textdecodebase64": { diff --git a/internal/lang/functions.go b/internal/lang/functions.go index f811f0b503..0ae17d0836 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -190,9 +190,7 @@ func (s *Scope) Functions() map[string]function.Function { return s.funcs, filesystemFunctions, templateFunctions } coreFuncs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, funcsFunc) - if s.activeExperiments.Has(experiments.TemplateStringFunc) { - coreFuncs["templatestring"] = funcs.MakeTemplateStringFunc(funcsFunc) - } + coreFuncs["templatestring"] = funcs.MakeTemplateStringFunc(funcsFunc) if s.ConsoleMode { // The type function is only available in terraform console. diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go index bdf1b16de7..d6ebdaafec 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/lang/marks" homedir "github.com/mitchellh/go-homedir" @@ -976,6 +977,21 @@ func TestFunctions(t *testing.T) { }, }, + "templatestring": { + { + `templatestring(local.greeting_template, { + name = "Arthur" +})`, + cty.StringVal("Hello, Arthur!"), + }, + { + `core::templatestring(local.greeting_template, { + name = "Namespaced Arthur" +})`, + cty.StringVal("Hello, Namespaced Arthur!"), + }, + }, + "timeadd": { { `timeadd("2017-11-22T00:00:00Z", "1s")`, @@ -1311,9 +1327,14 @@ func TestFunctions(t *testing.T) { for _, test := range funcTests { t.Run(test.src, func(t *testing.T) { - data := &dataForTests{} // no variables available; we only need literals here + data := &dataForTests{ + LocalValues: map[string]cty.Value{ + "greeting_template": cty.StringVal("Hello, ${name}!"), + }, + } scope := &Scope{ Data: data, + ParseRef: addrs.ParseRef, BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem PlanTimestamp: time.Date(2004, 04, 25, 15, 00, 00, 000, time.UTC), ExternalFuncs: externalFuncs, diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index bee6f0b01a..0d812ea150 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -365,6 +365,10 @@ "title": "substr", "href": "/language/functions/substr" }, + { + "title": "templatestring", + "href": "/language/functions/templatestring" + }, { "title": "title", "href": "/language/functions/title" @@ -893,6 +897,11 @@ "path": "functions/templatefile", "hidden": true }, + { + "title": "templatestring", + "path": "functions/templatestring", + "hidden": true + }, { "title": "terraform-encode_tfvars", "path": "functions/terraform-encode_tfvars", "hidden": true }, { "title": "terraform-decode_tfvars", "path": "functions/terraform-decode_tfvars", "hidden": true }, { "title": "terraform-encode_expr", "path": "functions/terraform-encode_expr", "hidden": true }, diff --git a/website/docs/language/functions/templatefile.mdx b/website/docs/language/functions/templatefile.mdx index 36e0b37e74..3ab12d9596 100644 --- a/website/docs/language/functions/templatefile.mdx +++ b/website/docs/language/functions/templatefile.mdx @@ -148,3 +148,4 @@ For more information, see the main documentation for * [`file`](/terraform/language/functions/file) reads a file from disk and returns its literal contents without any template interpretation. +* [`templatestring`](/terraform/language/functions/templatestring) takes a simple reference to a string value containing the template and renders its content. diff --git a/website/docs/language/functions/templatestring.mdx b/website/docs/language/functions/templatestring.mdx new file mode 100644 index 0000000000..8f97bb4e1b --- /dev/null +++ b/website/docs/language/functions/templatestring.mdx @@ -0,0 +1,67 @@ +--- +page_title: templatestring - Functions - Configuration Language +description: |- + The templatestring function takes a string from elsewhere in the module and renders its content as a template using a supplied set of template variables. +--- + +# `templatestring` Function + +-> **Note:** The `templatestring` function is intended for advanced use cases. Most use cases require only a [string template expression](/terraform/language/expressions/strings#string-templates). To render a template from a file, use the [`templatefile` function](/terraform/language/functions/templatefile). + +`templatestring` renders a template using a supplied set of template variables. + +```hcl +templatefile(ref, vars) +``` + +The first parameter must be a simple reference to string value containing the template: for example, `data.aws_s3_object.example.body` or `local.inline_template`. + +It is **not** valid to supply the template expression directly as the first argument: + +```hcl +# The following is not allowed +templatestring("Hello, $${name}", { + name = var.name +}) +``` + +Instead of the above, you should instead use a string template expression: + +```hcl +"Hello, ${var.name}" +``` + +The `templatestring` function is needed only when the template is available as a named object in the current module. + +The template syntax is the same as for +[string templates](/terraform/language/expressions/strings#string-templates) +in the main Terraform language, including interpolation sequences delimited with +`${` ... `}`. + +Strings in the Terraform language are sequences of Unicode characters, so +this function will interpret the file contents as UTF-8 encoded text and +return the resulting Unicode characters. If the template contains invalid UTF-8 +sequences then this function will produce an error. + +## Example + +The following example retrieves a template from S3 and dynamically renders it: + +```hcl +data "aws_s3_object" "example" { + bucket = "example-example" + key = "example.tmpl" +} + +output "example" { + value = templatestring(data.aws_s3_object.example.body, { + name = var.name + }) +} +``` + +For more examples of how to use templates, please see the documentation for the [`templatefile`](/terraform/language/functions/templatefile#Examples) function. + +## Related Functions + +* [`templatefile`](/terraform/language/functions/templatefile) reads a file from disk and renders its content as a template.