Call to function "templatefile" failed

Hi @apparentlymart ,

I need your help once again with terraform v0.13.0-beta3. I was checking the following snippet under

and have the following definition

resource "cloudfoundry_service_instance" "xsuaa_broker" {
  name  = var.xsuaa_broker_name
  space = data.cloudfoundry_space.landscape_helper_services_space.id
  service_plan = data.cloudfoundry_service.xsuaa_broker_service.service_plans["broker"]
  json_params = templatefile("${path.module}/templates/xsuaa-broker.json.tmpl", {
          xsuaa_landscape_name = var.xsuaa_landscape_name
          xsuaa_app_name = var.xsuaa_app_name
        })
}

${jsonencode({
 "description": "Systemsin-${xsuaa_landscape_name}",
 "foreign-scope-references": [
         "uaa.user"
  ],
  "oauth2-configuration": {
        "redirect-uris": [
               "http://localhost:*/**"
         ]
  },
 "xsappname": ${xsuaa_app_name}
})}

I get the following error when i do a terrafrom plan under the module which i am creating

  on main.tf line 55, in resource "cloudfoundry_service_instance" "xsuaa_broker":
  55:   json_params = templatefile("${path.module}/templates/xsuaa-broker.json.tmpl", {
  56:           xsuaa_landscape_name = var.xsuaa_landscape_name
  57:           xsuaa_app_name = var.xsuaa_app_name
  58:         })
    |----------------
    | path.module is "."

Call to function "templatefile" failed:
./templates/xsuaa-broker.json.tmpl:11,15-16: Invalid character; This character
is not used within the language., and 1 other diagnostic(s).

Kevin

I think the error here is likely because your template is not quoting the app name value. This is one of the tricky things about using template files to generate JSON: you have to remember to surround string values with double quotes.

Since you’re already using jsonencode in the template file, it would probably be easier to skip the template altogether. Something like this:

resource "cloudfoundry_service_instance" "xsuaa_broker" {
  name  = var.xsuaa_broker_name
  space = data.cloudfoundry_space.landscape_helper_services_space.id
  service_plan = data.cloudfoundry_service.xsuaa_broker_service.service_plans["broker"]
  json_params = jsonencode(
    {
      "description": "Systemsin-${var.xsuaa_landscape_name}",
      "foreign-scope-references": [
        "uaa.user"
      ],
      "oauth2-configuration": {
        "redirect-uris": [
          "http://localhost:*/**"
        ]
      },
      "xsappname": var.xsuaa_app_name
    }
  )
}
1 Like

Hi @alisdair,

Thanks for the info. It worked with the json as inline . However I would like to also use yaml as a templatefile because I have a CR which is quite long with lots is params. For example in the example below the resource is created inline. How would I solve the below with a template ? Do you also have any example of using loops in a template file ?

https://github.com/gavinbunney/terraform-provider-kubectl#quick-start

Kevin

I would again recommend not using a template to generate YAML. Since YAML is a superset of JSON, any JSON document is a valid YAML document. You should be able to use jsonencode in the same way as above.

If there’s some reason that the output must be in a more common YAML format, you might want to try the yamlencode function instead.


But for when you need to use templates, here’s the documentation on for loop directives.

@alisdair’s solution can apply to your jsonencode call inside your template as well: the important thing is that you tried to use template interpolation nested inside another template interpolation:

${jsonencode({
 # ...
 "xsappname": ${xsuaa_app_name}
})}

This is wrong, because ${ ... } is how we switch from literal text mode into expression mode, but we’re already in expression mode here because of the initial ${ in your file.

The solution is to write it the same way @alisdair wrote it in his example:

${jsonencode({
 # ...
 "xsappname": xsuaa_app_name
})}

Note that this doesn’t apply to your description property, which is already correct:

  "description": "Systemsin-${var.xsuaa_landscape_name}",

It is correct to use template interpolation here, because quotes " switch back to literal text mode, so the nested ${ again switches back to expression mode to evaluate var.xsuaa_landscape_name.

Bringing that all together in a full example:

${jsonencode({
  "description": "Systemsin-${xsuaa_landscape_name}",
  "foreign-scope-references": [
    "uaa.user"
  ],
  "oauth2-configuration": {
    "redirect-uris": [
      "http://localhost:*/**"
    ]
  },
  "xsappname": xsuaa_app_name
})}

A fundamental thing to recognize here is that the Terraform language has two different “contexts” or “modes” when producing values: template mode and expression mode.

  • In the main configuration files, argument values are interpreted as expressions initially but can switch to template mode by using quotes ".
  • In external templates loaded from files, the content is interpreted as template initially but can switch to expression mode by using the interpolation syntax ${ ... }.

These two modes can also nest inside each other, as we can see in your description property which contains a quoted template that contains another level of interpolation.

2 Likes

Hi @apparentlymart,

Thanks for excellent explanation. I will try it and provide you a feedback. Do you have a sample example for a variable with list values and it usage in a template file ? I presume I have to do something similar mentioned in the above issue link which i pasted

Kevin

Hi @apparentlymart,

I am getting the following error when using a yaml template file

Error: Error in function call

  on main.tf line 24, in resource "kubectl_manifest" "test":
  24:    yaml_body = templatefile("${path.module}/templates/secret-binding.yaml.tmpl", {
  25:           cloud_provider = var.cloud_provider
  26:           project_name = var.project_name
  27:           secret_name = var.secret_name
  28:         })
    |----------------
    | path.module is "."

Call to function "templatefile" failed:
./templates/secret-binding.yaml.tmpl:3,11-12: Missing argument separator; A
comma is required to separate each function argument from the next..

main.tf

resource "kubectl_manifest" "test" {
   yaml_body = templatefile("${path.module}/templates/secret-binding.yaml.tmpl", {
          cloud_provider = var.cloud_provider,
          project_name = var.project_name,
          secret_name = var.secret_name
        })
}
${yamlencode(
apiVersion: core.test.cloud/v1beta1
kind: SecretBinding
metadata:
  labels:
    cloudprofile.test.io/name: cloud_provider
  name: secret_name
  namespace: project_name
secretRef:
  name: secret_name
  namespace: project_name
)}

What am i doing wrong ?

Kevin

Hi @apparentlymart.

I did some changes now after removing the yamlencode in the template file

Terraform will perform the following actions:

  # kubectl_manifest.test will be created
  + resource "kubectl_manifest" "test" {
      + api_version             = "core.gardener.cloud/v1beta1"
      + force_new               = false
      + id                      = (known after apply)
      + kind                    = "SecretBinding"
      + live_manifest_incluster = (known after apply)
      + live_resource_version   = (known after apply)
      + live_uid                = (known after apply)
      + name                    = "test-secret"
      + namespace               = "garden_abap"
      + resource_version        = (known after apply)
      + uid                     = (known after apply)
      + wait_for_rollout        = true
      + yaml_body               = <<~EOT
            ---
            apiVersion: core.test.cloud/v1beta1
            kind: SecretBinding
            metadata:
              labels:
                cloudprofile.test.io/name: aws
              name: test-secret
              namespace: garden_abap
            secretRef:
              name: test-secret
              namespace: garden_abap
            
        EOT
      + yaml_incluster          = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

cat templates/secret-binding.yaml.test.tmpl

---
apiVersion: core.test.cloud/v1beta1
kind: SecretBinding
metadata:
  labels:
    cloudprofile.test..io/name: ${cloud_provider}
  name: ${secret_name}
  namespace: ${project_name}
secretRef:
  name: ${secret_name}
  namespace: ${project_name}

I am still wondering if I need the yamlencode for K8s manifests file

Kevin

Hi @linuxbsdfreak,

In your new example you seem to be using YAML syntax for your object, instead of Terraform Language syntax. Although the yamlencode function will produce YAML, its input must be a normal Terraform expression just like with jsonencode.

Note that the Terraform syntax for constructing objects resembles the JSON syntax for constructing objects, so it might’ve seemed like the jsonencode examples were using JSON syntax for the argument, but those were actually normal Terraform object constructor expressions. You should use the same syntax for yamlencode as for jsonencode, and then the function itself will format it into YAML block style.

(As @alisdair noted earlier, the result of jsonencode is also valid YAML, but it’s YAML in flow style, rather than block style.)

Hi @apparentlymart,

Thanks for the explanation. I can use the plain yaml without the yamlencode . What do I have to do use yamlencode along with terraform objects and fix that error ? I am trying to understand the difference. I am following the example similar to jsonencode but in a template file with yamlencode.

Kevin

Hi @apparentlymart,

Could you provide me a sample example of yamlencode with templatefile? I still do not get what am I missing in the error ?

Kevin

Hi @linuxbsdfreak,

I’ve been taking some time off work for the last couple of weeks. Sorry for the slow response.

If you take my earlier example with jsonencode and just change the function name to yamlencode (without changing any of the arguments) you should see it produce the same data structure in YAML block style, rather than in JSON. You can then change the values inside the braces if you want to change what data will be included in the resulting YAML, but you must always use Terraform expression syntax to write the data structure, regardless of which serialization format you intend to produce.

Hi @apparentlymart,

I hope you had a nice time away from work. Thanks for your response. However i am doing what you mentioned as follows and i get the below message.

main.tf

resource "kubectl_manifest" "gardener_dashboard_secret_binding_tmp" {
   yaml_body = templatefile("${path.module}/templates/secret-binding.yaml_org.tmpl", {
          cloud_provider = var.cloud_provider,
          project_name = var.project_name,
          secret_name = kubernetes_secret.gardener_dashboard_secret.metadata.0.name
        })
}

templates/secret-binding.yaml_org.tmpl

${yamlencode(
apiVersion: core.gardener.cloud/v1beta1
kind: SecretBinding
metadata:
  labels:
    cloudprofile.garden.cloud.io/name: cloud_provider
  name: secret_name
  namespace: project_name
secretRef:
  name: secret_name
  namespace: project_name
)}

Error

 on main.tf line 48, in resource "kubectl_manifest" "gardener_dashboard_secret_binding_tmp":
  48:    yaml_body = templatefile("${path.module}/templates/secret-binding.yaml_org.tmpl", {
  49:           cloud_provider = var.cloud_provider,
  50:           project_name = var.project_name,
  51:           secret_name = kubernetes_secret.gardener_dashboard_secret.metadata.0.name
  52:         })
    |----------------
    | path.module is "."

Call to function "templatefile" failed:
./templates/secret-binding.yaml_org.tmpl:2,11-12: Missing argument separator;
A comma is required to separate each function argument from the next..

Kevin

The example you’ve shown here contains a lot more than just renaming jsonencode to yamlencode.

Here is my previous example, exactly as I wrote it except for changing the function name to yamlencode:

${yamlencode({
  "description": "Systemsin-${xsuaa_landscape_name}",
  "foreign-scope-references": [
    "uaa.user"
  ],
  "oauth2-configuration": {
    "redirect-uris": [
      "http://localhost:*/**"
    ]
  },
  "xsappname": xsuaa_app_name
})}

You can change this example to produce the kubectl manifest by changing the data in this object but still using Terraform syntax, not YAML syntax.

${yamlencode({
  "apiVersion": "core.gardener.cloud/v1beta",
  "kind": "SecretBinding",
  "metadata": {
    "labels": {
      "cloudprofile.garden.cloud.io/name": cloud_provider,
    },
    "name": secret_name,
    "namespace": project_name,
  },
  "secretRef": {
    "name": secret_name,
    "namespace": project_name,
  },
})}

Hi @apparentlymart,

Thanks for the reply. The yamlencode works now.

Kevin