Шаблонизация
Механизм шаблонизации в 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)) }}