Template

What is the Template Driver?

The Template Driver is a fully customizable Driver offered by the Humanitec Platform Orchestrator. It allows you to provision any Resource Type your Workloads depend on. It does that using Go templates. The templates are very similar to the templates used in Helm. Just like Helm, the Template Driver makes use of the Sprig template functions for Go templates.

Why use the Template Driver?

The Template Driver is primarily used to generate manifests that should be created in the Kubernetes Cluster. For example, generating Kubernetes manifests such as ServiceAccounts, NetworkPolicies, Ingress or Custom Resources. The Template Driver can be used to inject any manifest into a Kubernetes cluster.

The Template Driver can also generate any outputs for a resource. This makes it useful for generating values that can be computed from its inputs - such as IDs and names.

How does the Template Driver work?

The Template Driver takes a set of templates as inputs. These templates are evaluated as Go-templates and the outputs are parsed as YAML. The templates are evaluated in a fixed order. Each template takes a common set of inputs as well as outputs of previous templates in the sequence.

All templates receive an object as input. The top level properties of this object are as follows:

Property Type Description
cookie any The content the Driver cookie that was set by a previous provisioning of this resource. This is useful for persisting state between resource provisioning. It will be null if no cookie has been set.
driver object The value of driver_inputs supplied in a Resource Definition. This object has 2 top level properties of values and secrets. Placeholders will already have been replaced by the time the driver property will be used in a template.
id string The Globally Unique Resource ID (GUResID) for this instance of the resource. This ID is guaranteed to be unique and consistent for future provisioning of this resource.
resource object The value of any Resource Inputs that a Resource Type may have. Note that most Resource Types do not support resource inputs so this is normally an empty object {}.
type object The Resource Type of a resource.

The templates are evaluated in the following order with additional properties to the ones described above:

Template Additional Properties Output Type Description
init None any Is evaluated first and its output is made available to the other temaplates through the init property. It is normally used for setup such as extracting values from the cookie if present.
outputs init object Is evaluated as the non secret Driver outputs.
secrets init, outputs.values object Is evaluated as the secret Driver outputs. Note, the secrets template is not itself secret, but is used to generate secret outputs. See Working with secrets.
cookie init, outputs.values, outputs.secrets any Is evaluated as the cookie that will be stored and passed into the next invocation of this resource provisioning. Note, the output of this template does not affect the cookie property passed into the manifests template.
manifests init, outputs.values, outputs.secrets object Is evaluated to an object of Manifest Location objects. The top level object keys are only used in debug messages. The output of the template will be transformed and returned as the manifests property in the Driver outputs. See Generating manifests for more details.
The data object in the Manifest Location objects must evaluate to a valid Kubernetes manifest as raw YAML, i.e. not a string containing YAML.

Templates as objects

The Template Driver will accept either objects or strings as templates. If an object is provided, any values that are strings will be treated as templates, any values that are objects will then be evaluated based on the same rule.

For example, the following templates inputs generate the identical output:

people:
  Pat:
    age: 23
  Jo:
    age: 25
people: |
  Pat:
    age: 23
  Jo:
    age: 25
people:
  Pat:
    age: "{{ add 20 3 }}"
  Jo: "age: {{ `25` }}"

Properties

Property Description
Resource type Any
Account type None

Inputs

Values

Name Type Description
templates object Contains objects that will be evaluated as templates.

Templates object

Each property in the template will be evaluated using the same evaluation algorithm. For details of how each template is used, see How does the Template Driver Work above. All properties are optional.

Object Type Description
init string or object Must evaluate to any valid YAML. The init object is evaluated first and then made available as .init to other objects.
outputs string or object Must evaluate to an object. Properties to be available as non-secret outputs for the resource.
secrets string or object Must evaluate to an object. Properties to be available as secret outputs for the resource.
cookie string or object Must evaluate to any valid YAML. The result of evaluating this object is stored as a cookie. This will be available on subsequent requests as .cookie.
manifests string or object Must evaluated to an object of Manifest Location objects.

Secrets

Name Type Description
templates object DEPRECATED
Contains an object that will be evaluated as a template.

Template objects

Object Type Description
outputs object DEPRECATED
Must evaluate to an object. Properties to be available as outputs for the resource.
If the secrets template is also specified, then this template will be ignored.

Working with templates

Ensuring correct YAML

Care should be taken when dealing with templates to ensure that the output is always valid YAML. As YAML is a superset of JSON, all JSON is valid YAML. This means that the toRawJson template function can be used to ensure that all values are correctly formatted.

For example:

driver_inputs:
  values:
    referencedVal: ${resources.config.outputs.someVal}
    templates:
      init: |
        test: {{ .driver.values.referencedVal | toRawJson }}
     

Consider what would happen if ${resources.config.outputs.someVal} resolves to a 2 line string and toRawJson was not used. The result would be invalid YAML:

test: Hello
World!

Using toRawJson results in valid YAML:

test: "Hello\nWorld!"

Using the init template object

The init template object is a useful way to generate common data that is needed across templates. For example, generating a random ID to return from the Driver. The ID should be stored as a cookie so that the same ID can be returned in future.

Here is an example set of template objects that could do this:

templates:
  init:
    randomId: |
      {{- if and .cookie .cookie.randomId }}
        {{- .cookie.randomId }}
      {{- else }}
        {{- randAlphaNum 16 }}
      {{- end }}
  cookie:
    randomId: {{ .init.randomId }}
  outputs:
    randomId: {{ .init.randomId }}

Templates and placeholders

Placeholders are resolved by the Platform Orchestrator before the Template Driver is called. This means that the content of the placeholder will already be resolved in the Driver inputs that the Template Driver receives.

Placeholders can resolve differently depending on the type of the data a placeholder represents. Care should therefore be taken when working with placeholders and templates.

It is suggested to avoid using resource reference placeholders directly in templates.

Placeholders are not guaranteed to be valid YAML, so if possible, do not put resource references inline inside a template. Instead, use a top level value and the use it via a template expression.

For example, rather than this:

driver_inputs:
  values:
    templates:
      init: |
        test: ${resources.config.outputs.someVal}

Do this:

driver_inputs:
  values:
    referencedVal: ${resources.config.outputs.someVal}
    templates:
      init: |
        test: {{ .driver.values.referencedVal | toRawJson }}

Working with secrets

The secrets template can be used to generate outputs that should be treated as secrets. The template itself is not secret, so care should be taken when working with secret outputs.

A common need to expose a secret that is made up of secret values being retrieved as outputs from another resource. An initial idea might be to include the resource reference directly in the secrets template. This is discouraged in any case (see Templates and placeholders) but is especially problematic when using secrets. Instead, it is best to resolve the secret as a Driver input and then reference that via a template expansion.

For example:

driver_inputs:
  secrets:
    referencedPassword: ${resources.config.outputs.secretValue}
  values:
    templates:
      secrets: |
        password: {{ cat .driver.secrets.referencedPassword "-suffix"| toRawJson }}

Examples

See the Template Driver examples page for a collection of examples.

Here is an example of using the template Driver generate an Ingress manifest. This can be used as a drop in replacement for the humanitec/ingress Driver as long as there is no more than one ingress configured per Environment.

Set the following environment variables for the CLI and API commands:

Variable Example Description
HUMANITEC_TOKEN my-token The authentication token for accessing the Humanitec API
HUMANITEC_ORG my-org-id The unique identifier for the Humanitec Organization

Use the command below for the interface of your choice.

  1. Create a file defining the Resource Definition you want to create:
cat << "EOF" > template-ingress.yaml
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: template-ingress
entity:
  name: Template Ingress
  type: ingress
  driver_type: humanitec/template
  driver_inputs:
    values:
      host: ${resources.dns.outputs.host}
      routePaths: ${resources.dns<route.outputs.path}
      routePorts: ${resources.dns<route.outputs.port}
      routeServices: ${resources.dns<route.outputs.service}
      tlsSecretName: ${resources.tls-cert.outputs.tls_secret_name}
      templates:
        init: |
          tlsSecretName: {{ .driver.values.tls_secret_name | default "" | toRawJson }}

        manifests: |
          {{- /*
            Only generate an ingress manifest if there are any routes defined.
          */ -}}
          {{- if gt (len .driver.values.routePaths ) 0 -}}
          ingress.yaml:
            location: namespace
            data:
              apiVersion: networking.k8s.io/v1
              kind: Ingress
              metadata:
                {{- if hasKey .driver.values "annotations" }}
                annotations:
                  {{- range $k, $v := .driver.values.annotations }}
                  {{ $k | toRawJson }}: {{ $v | toRawJson }}
                  {{- end}}
                {{- end}}
                {{- if hasKey .driver.values "labels" }}
                labels:
                  {{- range $k, $v := .driver.values.labels }}
                  {{ $k | toRawJson }}: {{ $v | toRawJson }}
                  {{- end}}
                {{- end}}
                name: {{ .id }}-ingress
              spec:
                {{- if .driver.values.class }}
                ingressClassName: {{ .driver.values.class | toRawJson }}
                {{- end }}
                rules:
                - host: {{ .driver.values.host | toRawJson }}
                  http:
                    paths:
                    {{- /*
                      We are guaranteed that .driver.values.routePaths is
                      non-zero in length, so we dont need to deal with the empty condition.
                    */ -}}
                    {{- range $path, $rule := .init.ingressPaths }}
                    - path: {{ ternary "/" $path (eq $path "*") | toRawJson }}
                      pathType: {{ $lcType := lower $rule.type -}}
                        {{- if eq $lcType "implementationspecific" -}}
                          {{- "ImplementationSpecific" -}}
                        {{- else if eq $lcType "exact" -}}
                          {{- "Exact" -}}
                        {{- else -}}
                          {{- "Prefix" -}}
                        {{- end }}
                      backend:
                        service:
                          name: {{ $rule.name | toRawJson }}
                          port:
                            number: {{ $rule.port }}
                    {{- end }}
                    {{- range $index, $path := .driver.values.routePaths }}
                    - path: {{ $path | toRawJson }}
                      {{/*
                        TODO: Check the path to choose exact or prefix
                      */}}
                      pathType: {{ $.driver.values.path_type | default "Prefix" | toRawJson }}
                      backend:
                        service:
                          name: {{ index $.driver.values.routeServices $index  | toRawJson }}
                          port:
                            number: {{ index $.driver.values.routePorts $index }}
                    {{- end }}
                {{- if not (or .driver.values.no_tls (eq .init.tlsSecretName "")) }}
                tls:
                - hosts:
                  - {{ .init.host | toRawJson }}
                  secretName: {{ .init.tlsSecretName | toRawJson }}
                {{- end }}
          {{- end -}}
  criteria:
  - env_type: test-envs
EOF
  1. Use the humctl create command to create the Resource Definition in the Organization defined by your configured context:
humctl create -f template-ingress.yaml
rm template-ingress.yaml

Expected output:

id                  type     driver_type             driver_account
template-ingress    ingress  humanitec/template      -

Note that the data in the manifest is provided as a JSON encoded string which will evaluated as a template to a raw YAML object.

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary '{
  "id": "template-ingress",
  "name": "Template Ingress",
  "type": "ingress",
  "driver_type": "humanitec/template",
  "driver_inputs": {
    "values": {
      "templates": {
        "init": "tlsSecretName: {{ .driver.values.tls_secret_name | default \"\" | toRawJson }}\n",
        "manifests": "{{- /*\n  Only generate an ingress manifest if there are any routes defined.\n*/ -}}\n{{- if gt (len .driver.values.routePaths ) 0 -}}\ningress.yaml:\n  location: namespace\n  data:\n    apiVersion: networking.k8s.io/v1\n    kind: Ingress\n    metadata:\n      {{- if hasKey .driver.values \"annotations\" }}\n      annotations:\n        {{- range $k, $v := .driver.values.annotations }}\n        {{ $k | toRawJson }}: {{ $v | toRawJson }}\n        {{- end}}\n      {{- end}}\n      {{- if hasKey .driver.values \"labels\" }}\n      labels:\n        {{- range $k, $v := .driver.values.labels }}\n        {{ $k | toRawJson }}: {{ $v | toRawJson }}\n        {{- end}}\n      {{- end}}\n      name: {{ .id }}-ingress\n    spec:\n      {{- if .driver.values.class }}\n      ingressClassName: {{ .driver.values.class | toRawJson }}\n      {{- end }}\n      rules:\n      - host: {{ .driver.values.host | toRawJson }}\n        http:\n          paths:\n          {{- /*\n            We are guaranteed that .driver.values.routePaths is\n            non-zero in length, so we dont need to deal with the empty condition.\n          */ -}}\n          {{- range $path, $rule := .init.ingressPaths }}\n          - path: {{ ternary \"/\" $path (eq $path \"*\") | toRawJson }}\n            pathType: {{ $lcType := lower $rule.type -}}\n              {{- if eq $lcType \"implementationspecific\" -}}\n                {{- \"ImplementationSpecific\" -}}\n              {{- else if eq $lcType \"exact\" -}}\n                {{- \"Exact\" -}}\n              {{- else -}}\n                {{- \"Prefix\" -}}\n              {{- end }}\n            backend:\n              service:\n                name: {{ $rule.name | toRawJson }}\n                port:\n                  number: {{ $rule.port }}\n          {{- end }}\n          {{- range $index, $path := .driver.values.routePaths }}\n          - path: {{ $path | toRawJson }}\n            {{/*\n              TODO: Check the path to choose exact or prefix\n            */}}\n            pathType: {{ $.driver.values.path_type | default \"Prefix\" | toRawJson }}\n            backend:\n              service:\n                name: {{ index $.driver.values.routeServices $index  | toRawJson }}\n                port:\n                  number: {{ index $.driver.values.routePorts $index }}\n          {{- end }}\n      {{- if not (or .driver.values.no_tls (eq .init.tlsSecretName \"\")) }}\n      tls:\n      - hosts:\n        - {{ .init.host | toRawJson }}\n        secretName: {{ .init.tlsSecretName | toRawJson }}\n      {{- end }}\n{{- end -}}\n"
      },
      "host": "${resources.dns.outputs.host}",
      "routePaths": "${resources.dns<route.outputs.path}",
      "routePorts": "${resources.dns<route.outputs.port}",
      "routeServices": "${resources.dns<route.outputs.service}",
      "tlsSecretName": "${resources.tls-cert.outputs.tls_secret_name}"
    }
  },
  "criteria": [
    {
      "env_type": "test-envs"
    }
  ]
}
'
Top