Templating

The templating mechanism in werf is the same as in Helm. It uses the Go text/template template engine, enhanced with the Sprig and Helm feature set.

Template files

Template files are located in the templates directory of the chart.

The templates/*.yaml files are used to generate final, deployment-ready Kubernetes manifests. Each of these files can be used to generate multiple Kubernetes resource manifests. For this, insert a --- separator between the manifests.

The templates/_*.tpl files only contain named templates for using in other files. Kubernetes manifests cannot be generated using the `*.tpl’ files alone.

Actions

Actions is the key element of the templating process. Actions can only return strings. The action must be wrapped in double curly braces:

{{ print "hello" }}

Output:

hello

Variables

Variables are used to store or refer to data of any type.

This is how you can declare a variable and assign a value to it:

{{ $myvar := "hello" }}

This is how you can assign a new value to an existing variable:

{{ $myvar = "helloworld" }}

Here’s an example of how to use a variable:

{{ $myvar }}

Output:

helloworld

Here’s how to use predefined variables:

{{ $.Values.werf.env }}

You can also substitute the data without first declaring a variable:

labels:
  app: {{ "myapp" }}

Output:

labels:
  app: myapp

You can also store function or pipeline results in variables:

{{ $myvar := 1 | add 1 1 }}
{{ $myvar }} 

Output:

3

Variable scope

Scope limits the visibility of variables. By default, the scope is limited to the template file.

The scope can change for some blocks and functions. For example, the if statement creates a different scope, and the variables declared in the if statement will not be accessible outside it:

{{ if true }}
  {{ $myvar := "hello" }}
{{ end }}

{{ $myvar }}

Output:

Error: ... undefined variable "$myvar"

To get around this limitation, declare the variable outside the statement and assign a value to it inside the statement:

{{ $myvar := "" }}
{{ if true }}
  {{ $myvar = "hello" }}
{{ end }}

{{ $myvar }}

Output:

hello

Data types

Available data types:

Data type Example
Boolean {{ true }}
String {{ "hello" }}
Integer {{ 1 }}
Floating-point number {{ 1.1 }}
List with elements of any type (ordered) {{ list 1 2 3 }}
Dictionary with string keys and values of any type (unordered) {{ dict "key1" 1 "key2" 2 }}
Special objects {{ $.Files }}
Null {{ nil }}

Functions

werf has an extensive library of functions for use in templates. Most of them are Helm functions.

Functions can only be used in actions. Functions can have arguments and can return data of any type. For example, the add function below takes three numeric arguments and returns a number:

{{ add 3 2 1 }}

Output:

6

Note that the action result is always converted to a string regardless of the data type returned by the function.

A function may have arguments of the following types:

  • regular values: 1

  • calls of other functions: add 1 1

  • pipes: 1 | add 1

  • combination of the above types: 1 | add (add 1 1)

Put the argument in parentheses () if it is a call to another function or pipeline:

{{ add 3 (add 1 1) (1 | add 1) }}

To ignore the result returned by the function, simply assign it to the $_ variable:

{{ $_ := set $myDict "mykey" "myvalue"}}

Pipelines

Pipelines allow you to pass the result of the first function as the last argument to the second function, and the result of the second function as the last argument to the third function, and so on:

{{ now | unixEpoch | quote }}

Here, the result of the now function (gets the current date) is passed as an argument to the unixEpoch function (converts the date to Unix time). The resulting value is then passed to the quote function (adds quotation marks).

Output:

"1671466310"

The use of pipelines is optional; you can rewrite them as follows:

{{ quote (unixEpoch (now)) }}

… however, we recommend using the pipelines.

Logic gates and comparisons

The following logic gates are available:

Operation Function Example
Not not <arg> {{ not false }}
And and <arg> <arg> [<arg>, ...] {{ and true true }}
Or or <arg> <arg> [<arg>, ...] {{ or false true }}

The following comparison operators are available:

comparison Function Example
Equal eq <arg> <arg> [<arg>, ...] {{ eq "hello" "hello" }}
Not equal neq <arg> <arg> [<arg>, ...] {{ neq "hello" "world" }}
Less than lt <arg> <arg> {{ lt 1 2 }}
Greater than gt <arg> <arg> {{ gt 2 1 }}
Less than or equal le <arg> <arg> {{ le 1 2 }}
Greater than or equal ge <arg> <arg> {{ ge 2 1 }}

Example of combining various operators

{{ and (eq true true) (neq true false) (not (empty "hello")) }}

Conditionals

The if/else conditionals allows to perform templating only if specific conditions are met/not met, for example:

{{ if $.Values.app.enabled }}
# ...
{{ end }}

A condition is considered failed if the result of its calculation is either of:

  • boolean false;

  • zero 0;

  • an empty string "";

  • an empty list [];

  • an empty dictionary {};

  • null: nil.

In all other cases the condition is considered satisfied. A condition may include data, a variable, a function, or a pipeline.

Example:

{{ if eq $appName "backend" }}
app: mybackend
{{ else if eq $appName "frontend" }}
app: myfrontend
{{ else }}
app: {{ $appName }}
{{ end }}

Simple conditionals can be implemented not only with if/else, but also with the ternary function. For example, the following ternary expression:

{{ ternary "mybackend" $appName (eq $appName "backend") }}

… is similar to the `if/else’ construction below:

{{ if eq $appName "backend" }}
app: mybackend
{{ else }}
app: {{ $appName }}
{{ end }}

Cycles

Cycling through lists

The range cycles allow you to cycle through the list items and do the necessary templating at each iteration:

{{ range $urls }}
{{ . }}
{{ end }}

Output:

https://example.org
https://sub.example.org

The . relative context always points to the list element that corresponds to the current iteration; the pointer can also be assigned to an arbitrary variable:

{{ range $elem := $urls }}
{{ $elem }}
{{ end }}

The output is the same:

https://example.org
https://sub.example.org

Here’s how you can get the index of an element in the list:

{{ range $i, $elem := $urls }}
{{ $elem }} has an index of {{ $i }}
{{ end }}

Output:

https://example.org has an index of 0
https://sub.example.org has an index of 1

Cycling through dictionaries

The range cycles allow you to cycle through the dictionary keys and values and do the necessary templating at each iteration:

# values.yaml:
apps:
  backend:
    image: openjdk
  frontend:
    image: node
# templates/app.yaml:
{{ range $.Values.apps }}
{{ .image }}
{{ end }}

Output:

openjdk
node

The . relative context always points to the value of the dictionary element that corresponds to the current iteration; the pointer can also be assigned to an arbitrary variable:

{{ range $app := $.Values.apps }}
{{ $app.image }}
{{ end }}

The output is the same:

openjdk
node

Here’s how you can get the key of the dictionary element:

{{ range $appName, $app := $.Values.apps }}
{{ $appName }}: {{ $app.image }}
{{ end }}

Output:

backend: openjdk
frontend: node

Cycle control

The continue statement allows you to skip the current cycle iteration. To give you an example, let’s skip the iteration for the https://example.org element:

{{ range $url := $urls }}
{{ if eq $url "https://example.org" }}{{ continue }}{{ end }}
{{ $url }}
{{ end }}

In contrast, the break statement lets you both skip the current iteration and terminate the whole cycle:

{{ range $url := $urls }}
{{ if eq $url "https://example.org" }}{{ break }}{{ end }}
{{ $url }}
{{ end }}

Context

Root context ($)

The root context is the dictionary to which the $ variable refers. You can use it to access values and some special objects. The root context has global visibility within the template file (except for the define block and some functions).

Example of use:

{{ $.Values.mykey }}

Output:

myvalue

You can add custom keys/values to the root context. They will also be available throughout the template file:

{{ $_ := set $ "mykey" "myvalue"}}
{{ $.mykey }}

Output:

myvalue

The root context remains intact even in blocks that change the relative context (except for define):

{{ with $.Values.backend }}
- command: {{ .command }}
  image: {{ $.Values.werf.image.backend }}
{{ end }}

Functions like tpl or include can lose the root context. You can pass the root context as an argument to them to restore access to it:

{{ tpl "{{ .Values.mykey }}" $ }}

Output:

myvalue

Relative context (.)

The relative context is any type of data referenced by the . variable. By default, the relative context points to the root context.

Some blocks and functions can modify the relative context. In the example below, in the first line, the relative context points to the root context $, while in the second line, it points to $.Values.containers:

{{ range .Values.containers }}
{{ . }}
{{ end }}

Use the with block to modify the relative context:

{{ with $.Values.app }}
image: {{ .image }}
{{ end }}

Reusing templates

Named templates

To reuse templating, declare named templates in the define blocks in the templates/_*.tpl files:

# templates/_helpers.tpl:
{{ define "labels" }}
app: myapp
team: alpha
{{ end }}

Next, insert the named templates into the templates/*.(yaml|tpl) files using the include function:

# templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels: {{ include "labels" nil | nindent 6 }}
  template:
    metadata:
      labels: {{ include "labels" nil | nindent 8 }}

Output:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
      team: alpha
  template:
    metadata:
      labels:
        app: myapp
        team: alpha

The name of the named template to use in the include function may be dynamic:

{{ include (printf "%s.labels" $prefix) nil }}

Named templates are globally visible - once declared in a parent or any child chart, a named template becomes available in all charts at once: in both parent and child charts. Make sure there are no named templates with the same name in the parent and child charts.

Parameterizing named templates

The include function that inserts named templates takes a single optional argument. This argument can be used to parameterize a named template, where that argument becomes the . relative context:

{{ include "labels" "myapp" }}
{{ define "labels" }}
app: {{ . }}
{{ end }}

Output:

app: myapp

To pass several arguments at once, use a list containing multiple arguments:

{{ include "labels" (list "myapp" "alpha") }}
{{ define "labels" }}
app: {{ index . 0 }}
team: {{ index . 1 }}
{{ end }}

…or a dictionary:

{{ include "labels" (dict "app" "myapp" "team" "alpha") }}
{{ define "labels" }}
app: {{ .app }}
team: {{ .team }}
{{ end }}

Optional positional arguments can be handled as follows:

{{ include "labels" (list "myapp") }}
{{ include "labels" (list "myapp" "alpha") }}
{{ define "labels" }}
app: {{ index . 0 }}
{{ if gt (len .) 1 }}
team: {{ index . 1 }}
{{ end }}
{{ end }}

Optional non-positional arguments can be handled as follows:

{{ include "labels" (dict "app" "myapp") }}
{{ include "labels" (dict "team" "alpha" "app" "myapp") }}
{{ define "labels" }}
app: {{ .app }}
{{ if hasKey . "team" }}
team: {{ .team }}
{{ end }}
{{ end }}

Pass nil to a named template that does not require parametrizing:

{{ include "labels" nil }}

The result of running include

The include function that inserts a named template returns text data. To return structured data, you need to deserialize the result of include using the fromYaml function:

{{ define "commonLabels" }}
app: myapp
{{ end }}
{{ $labels := include "commonLabels" nil | fromYaml }}
{{ $labels.app }}

Output:

myapp

Note that fromYaml does not support lists. For lists, use the dedicated fromYamlArray function.

You can use the toYaml and toJson functions for data serialization, and the fromYaml/fromYamlArray and fromJson/fromJsonArray functions for deserialization.

Named template context

The named templates declared in templates/_*.tpl cannot use the root and relative contexts of the file into which they are included by the include function. You can fix this by passing the root and/or relative context as include arguments:

{{ include "labels" $ }}
{{ include "labels" . }}
{{ include "labels" (list $ .) }}
{{ include "labels" (list $ . "myapp") }}

include in include

You can also use the include function in the define blocks to include named templates:

{{ define "doSomething" }}
{{ include "doSomethingElse" . }}
{{ end }}

You can even call the include function to include a named template from this very template, i.e., recursively:

{{ define "doRecursively" }}
{{ if ... }}
{{ include "doRecursively" . }}
{{ end }}
{{ end }}

tpl templating

The tpl function allows you to process any line in real time. It takes one argument (the root context).

In the example below, we use the tpl function to retrieve the deployment name from the values.yaml file:

# values.yaml:
appName: "myapp"
deploymentName: "{{ .Values.appName }}-deployment"
# templates/app.yaml:
{{ tpl $.Values.deploymentName $ }}

Output:

myapp-deployment

And here’s how you can process arbitrary files that don’t support Helm templating:

{{ tpl ($.Files.Get "nginx.conf") $ }} 

You can add arguments as new root context keys to the tpl function to pass additional arguments:

{{ $_ := set $ "myarg" "myvalue"}}
{{ tpl "{{ $.myarg }}" $ }}

Indentation control

Use the nindent function to set the indentation:

       containers: {{ .Values.app.containers | nindent 6 }}

Output:

      containers:
      - name: backend
        image: openjdk

And here’s how you can mix it with other data:

       containers:
       {{ .Values.app.containers | nindent 6 }}
       - name: frontend
         image: node

Output:

      containers:
      - name: backend
        image: openjdk
      - name: frontend
        image: node

Use - after {{ and/or before }} to remove extra spaces before and/or after the action result, for example:

  {{- "hello" -}} {{ "world" }}

Output:

helloworld

Comments

werf supports two types of comments — template comments {{ /* */ }}} and manifest comments #.

Template comments

The template comments are stripped off during manifest generation:

{{ /* This comment will be stripped off */ }}
app: myApp

Comments can be multi-line:

{{ /*
Hello
World
/* }}

Template actions are ignored in such comments

{{ /*
{{ print "This template action will be ignored" }}
/* }}

Manifest comments

The manifest comments are retained during manifest generation:

# This comment will stay in place
app: myApp

Only single-line comments of this type are supported:

# For multi-line comments, use several
# single-line comments in a row

The template actions encountered in them are carried out:

# {{ print "This template action will be carried out" }}

Debugging

Use werf render to render and display ready-to-use Kubernetes manifests. The --debug option displays manifests even if they are not valid YAML.

Here’s how you can display the variable contents:

output: {{ $appName | toYaml }}

Display the contents of a list or dictionary variable:

output: {{ $dictOrList | toYaml | nindent 2 }}

Display the variable’s data type:

output: {{ kindOf $myvar }}

Display some string and stop template rendering:

{{ fail (printf "Data type: %s" (kindOf $myvar)) }}