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. |
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. |
Values 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. |
Secrets 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. |
secrets.templates.outputs
is deprecated. It is recommended to migrate to using values.templates.secrets
instead. See
Working with secrets
for more details.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!"
quote
and squote
functions. It is advised not to use these functions as they only add quotes and do not escape. For example: {{ `Hello "World"` | quote }}
would produce the invalid YAML string: "Hello "World""
. Using toRawJson
would generate: "Hello \"World\""
which is a valid YAML string.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. (This includes any placeholders in comments.)
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 }}
Avoid using a hyphen-minus character “-
” in value names, e.g. referenced-Val
. The built-in template parser will fail with an error message “bad character U+002D '-'
”.
Use underscores instead, e.g. referenced_Val
, or the camel case referencedVal
as shown.
As a workaround, you can reference the value using the index
function like this:
test: {{ index .driver.values "referenced-Val" | 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.
- 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
- 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"
}
]
}
'