Шаблонизация

Механизм шаблонизации в werf ничем не отличается от Helm. Используется движок шаблонов Go text/template, расширенный готовым набором функций Sprig и Helm.

Файлы шаблонов

В директории templates чарта находятся файлы шаблонов.

Файлы шаблонов templates/*.yaml формируют конечные Kubernetes-манифесты для развертывания. Каждый из этих файлов может формировать сразу несколько манифестов Kubernetes-ресурсов. Для этого манифесты должны быть разделены строкой ---.

Файлы шаблонов templates/_*.tpl содержат только именованные шаблоны для использования в других файлах. Файлы *.tpl не формируют Kubernetes-манифесты сами по себе.

Действия

Главный элемент шаблонизации — действие. Действие может возвращать только строки. Действие заключается в двойные фигурные скобки:

{{ print "hello" }}

Результат:

hello

Переменные

Переменные используются для хранения или указания на данные любого типа.

Объявление и присваивание переменной:

{{ $myvar := "hello" }}

Присваивание нового значения существующей переменной:

{{ $myvar = "helloworld" }}

Использование переменной:

{{ $myvar }}

Результат:

helloworld

Использование предопределенных переменных:

{{ $.Values.werf.env }}

Данные можно подставлять и без объявления переменных:

labels:
  app: {{ "myapp" }}

Результат:

labels:
  app: myapp

Также в переменные можно сохранять результат выполнения функций или конвейеров:

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

Результат:

3

Области видимости переменных

Область видимости ограничивает видимость переменных. По умолчанию область видимости переменных ограничена файлом-шаблоном.

Область видимости может меняться при использовании некоторых блоков и функций. К примеру, блок if создаёт новую область видимости, а переменные, объявленные в блоке if, будут недоступны снаружи:

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

{{ $myvar }}

Результат:

Error: ... undefined variable "$myvar"

Чтобы обойти это ограничение, объявите переменную за пределами блока, а значение присвойте ей внутри блока:

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

{{ $myvar }}

Результат:

hello

Типы данных

Доступные типы данных:

Тип данных Пример
Логический {{ true }}
Строка {{ "hello" }}
Целое число {{ 1 }}
Число с плавающей точкой {{ 1.1 }}
Список с элементами любого типа, упорядоченный {{ list 1 2 3 }}
Словарь с ключами-строками и значениями любого типа, неупорядоченный {{ dict "key1" 1 "key2" 2 }}
Специальные объекты {{ $.Files }}
Нуль {{ nil }}

Функции

В werf встроена обширная библиотека функций для использования в шаблонах. Основная их часть — функции Helm.

Функции можно использовать только в действиях. Функции могут иметь аргументы и могут возвращать данные любого типа. Например, приведенная ниже функция сложения принимает три аргумента-числа и возвращает число:

{{ add 3 2 1 }}

Результат:

6

Обратите внимание, что результат выполнения действия всегда конвертируется в строку независимо от возвращаемого функцией типа данных.

Аргументами функций могут быть:

  • простые значения: 1;

  • вызовы других функций: add 1 1;

  • конвейеры: 1 | add 1;

  • комбинации вышеперечисленных типов: 1 | add (add 1 1).

Если аргумент — не простое значение, а вызов другой функции или конвейер, заключите его в круглые скобки ():

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

Чтобы игнорировать возвращаемый функцией результат, просто сохраните его в переменную $_:

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

Конвейеры

Конвейеры позволяют передать результат выполнения первой функции как последний аргумент во вторую функцию, а результат второй функции — как последний аргумент в третью и так далее:

{{ now | unixEpoch | quote }}

Здесь результат выполнения функции now (получить текущую дату) передаётся как аргумент в функцию unixEpoch (преобразует дату в Unix time), после чего полученное значение передаётся в функцию quote (оборачивает в кавычки).

Итоговый результат:

"1671466310"

Использование конвейеров не обязательно, и при желании их можно переписать следующим образом:

{{ quote (unixEpoch (now)) }}

… однако рекомендуется использовать именно конвейеры.

Логические операции и сравнения

Логические операции реализуются следующими функциями:

Операция Функция Пример
НЕ not <arg> {{ not false }}
И and <arg> <arg> [<arg>, ...] {{ and true true }}
ИЛИ or <arg> <arg> [<arg>, ...] {{ or false true }}

Сравнения реализуются следующими функциями:

Сравнение Функция Пример
Эквивалентно eq <arg> <arg> [<arg>, ...] {{ eq "hello" "hello" }}
Не эквивалентно neq <arg> <arg> [<arg>, ...] {{ neq "hello" "world" }}
Меньше lt <arg> <arg> {{ lt 1 2 }}
Больше gt <arg> <arg> {{ gt 2 1 }}
Меньше или эквивалентно le <arg> <arg> {{ le 1 2 }}
Больше или эквивалентно ge <arg> <arg> {{ ge 2 1 }}

Пример комбинирования:

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

Ветвления

Ветвления if/else позволяют выполнять шаблонизацию только при выполнении/невыполнении определенных условий. Пример:

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

Условие считается невыполненным, если результатом его вычисления является:

  • логическое false;

  • число 0;

  • пустая строка "";

  • пустой список [];

  • пустой словарь {};

  • нуль: nil.

В остальных случаях условие считается выполненным. Условием могут быть данные, переменная, функция или конвейер.

Полный пример:

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

Простые ветвления можно реализовывать не только с if/else, но и с функцией ternary. Например, следующее выражение с ternary:

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

… аналогично приведенной ниже конструкции if/else:

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

Циклы

Циклы по спискам

Циклы range позволяют перебирать элементы списка и выполнять нужную шаблонизацию на каждой итерации:

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

Результат:

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

Относительный контекст . всегда указывает на элемент списка, соответствующий текущей итерации, хотя указатель можно сохранить и в произвольную переменную:

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

Результат будет таким же:

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

Получить индекс элемента в списке можно следующим образом:

{{ range $i, $elem := $urls }}
{{ $elem }} имеет индекс {{ $i }}
{{ end }}

Результат:

https://example.org имеет индекс 0
https://sub.example.org имеет индекс 1

Циклы по словарям

Циклы range позволяют перебирать ключи и значения словарей и выполнять нужную шаблонизацию на каждой итерации:

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

Результат:

openjdk
node

Относительный контекст . всегда указывает на значение элемента словаря, соответствующего текущей итерации, при этом указатель можно сохранить и в произвольную переменную:

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

Результат будет таким же:

openjdk
node

Получить ключ элемента словаря можно так:

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

Результат:

backend: openjdk
frontend: node

Контроль выполнения цикла

Специальное действие continue позволяет пропустить текущую итерацию цикла. В качестве примера пропустим итерацию для элемента https://example.org:

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

Специальное действие break позволяет не только пропустить текущую итерацию, но и прервать весь цикл:

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

Контекст

Корневой контекст ($)

Корневой контекст — словарь, на который ссылается переменная $. Через него доступны values и некоторые специальные объекты. Корневой контекст имеет глобальную видимость в пределах файла-шаблона (исключение — блок define и некоторые функции).

Пример использования:

{{ $.Values.mykey }}

Результат:

myvalue

К корневому контексту можно добавлять произвольные ключи/значения, которые также станут доступны из любого места файла-шаблона:

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

Результат:

myvalue

Корневой контекст остаётся неизменным даже в блоках, изменяющих относительный контекст (исключение — define):

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

Некоторые функции вроде tpl или include могут терять корневой контекст. Для сохранения доступа к корневому контексту многим из них можно передать корневой контекст аргументом:

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

Результат:

myvalue

Относительный контекст (.)

Относительный контекст — данные любого типа, на которые ссылается переменная .. По умолчанию относительный контекст указывает на корневой контекст.

Некоторые блоки и функции могут менять относительный контекст. В примере ниже в первой строке относительный контекст указывает на корневой контекст $, а во второй строке — уже на $.Values.containers:

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

Для смены относительного контекста можно использовать блок with:

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

Переиспользование шаблонов

Именованные шаблоны

Для переиспользования шаблонизации объявите именованные шаблоны в блоках define в файлах templates/_*.tpl:

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

Далее подставляйте именованные шаблоны в файлы templates/*.(yaml|tpl) функцией include:

# 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 }}

Результат:

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

Имя именованного шаблона для функции include может быть динамическим:

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

Именованные шаблоны обладают глобальной видимостью — единожды объявленный в родительском или любом дочернем чарте именованный шаблон становится доступен сразу во всех чартах — и в родительском, и в дочерних. Убедитесь, что в подключенных родительском и дочерних чартах нет именованных шаблонов с одинаковыми именами.

Параметризация именованных шаблонов

Функция include, подставляющая именованные шаблоны, принимает один произвольный аргумент. Этот аргумент можно использовать для параметризации именованного шаблона, где этот аргумент станет относительным контекстом .:

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

Результат:

app: myapp

Для передачи сразу нескольких аргументов используйте список с несколькими аргументами:

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

… или словарь:

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

Необязательные позиционные аргументы можно реализовать так:

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

А необязательные непозиционные аргументы — так:

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

Именованному шаблону, не требующему параметризации, просто передайте nil:

{{ include "labels" nil }}

Результат выполнения include

Функция include, подставляющая именованный шаблон, всегда возвращает только текст. Для возврата структурированных данных нужно десериализовать результат выполнения include с помощью функции fromYaml:

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

Результат:

myapp

Обратите внимание, что fromYaml не работает для списков. Специально для них (и только для них) предназначена функция fromYamlArray.

Для явной сериализации данных можно воспользоваться функциями toYaml и toJson, для десериализации — функциями fromYaml/fromYamlArray и fromJson/fromJsonArray.

Контекст именованных шаблонов

Объявленные в templates/_*.tpl именованные шаблоны теряют доступ к корневому и относительному контекстам файла, в который они включаются функцией include. Исправить это можно, передав корневой и/или относительный контекст в виде аргументов include:

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

include в include

В блоках define тоже можно использовать функцию include для включения именованных шаблонов:

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

Через include можно вызвать даже тот именованный шаблон, из которого и происходит вызов, т. е. вызвать его рекурсивно:

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

Шаблонизация с tpl

Функция tpl позволяет выполнить шаблонизацию любой строки и тут же получить результат. Она принимает один аргумент, который должен быть корневым контекстом.

Пример шаблонизации values:

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

Результат:

myapp-deployment

Пример шаблонизации произвольных файлов, которые сами по себе не поддерживают Helm-шаблонизацию:

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

Для передачи дополнительных аргументов в функцию tpl можно добавить аргументы как новые ключи корневого контекста:

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

Контроль отступов

Используйте функцию nindent для выставления отступов:

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

Результат:

      containers:
      - name: backend
        image: openjdk

Пример комбинации с другими данными:

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

Результат:

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

Используйте - после {{ и/или до }} для удаления лишних пробелов до и/или после результата выполнения действия, например:

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

Результат:

helloworld

Комментарии

Поддерживаются два типа комментариев — комментарии шаблонизации {{ /* */ }} и комментарии манифестов #.

Комментарии шаблонизации

Комментарии шаблонизации скрываются при формировании манифестов:

{{ /* Этот комментарий пропадёт */ }}
app: myApp

Комментарии могут быть многострочными:

{{ /*
Hello
World
/* }}

Шаблоны в них игнорируются:

{{ /*
{{ print "Эта шаблонизация игнорируется" }}
/* }}

Комментарии манифестов

Комментарии манифестов сохраняются при формировании манифестов:

# Этот комментарий сохранится
app: myApp

Комментарии могут быть только однострочнными:

# Для многострочных комментариев используйте
# несколько однострочных комментариев подряд

Шаблоны в них выполняются:

# {{ print "Эта шаблонизация выполняется" }}

Отладка

Используйте werf render, чтобы полностью сформировать и отобразить конечные Kubernetes-манифесты. Укажите опцию --debug, чтобы увидеть манифесты, даже если они не являются корректным YAML.

Отобразить содержимое переменной:

output: {{ $appName | toYaml }}

Отобразить содержимое переменной-списка или словаря:

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

Отобразить тип данных у переменной:

output: {{ kindOf $myvar }}

Отобразить произвольную строку, остановив дальнейшее формирование шаблонов:

{{ fail (printf "Тип данных: %s" (kindOf $myvar)) }}