Управляющие конструкции: if, else, with, range

Шаблон становится программой: включаем блоки по условию, упрощаем доступ через with и генерируем повторяющиеся куски через range.

Управляющие конструкции Go templates — это if/else, with (сменить контекст) и range (цикл). Каждая открывается ключевым словом и закрывается {{ end }}.

if / else — условные блоки

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Release.Name }}
{{- else }}
# ingress отключён
{{- end }}

Что считается «ложью» в Helm: false, 0, пустая строка "", пустой список/карта, nil. Всё остальное — истина. Поэтому проверка if .Values.replicaCount провалится при replicaCount: 0 — для чисел лучше явное сравнение.

Особый и коварный случай — обращение к отсутствующему вложенному ключу. Если в values нет секции ingress вовсе, то .Values.ingress.enabled вернёт nil (ложь) и блок просто не отрендерится — это удобно и часто именно то, что нужно. Но стоит структуре оказаться частично заданной не так, как ожидает чарт, и можно получить ошибку «nil pointer evaluating». Поэтому в зрелых чартах в values.yaml всегда объявляют полную структуру со значениями по умолчанию — даже выключенные секции присутствуют как ingress: { enabled: false }. Это превращает «может, ключа нет» в предсказуемое «ключ есть, значение false» и убирает целый класс ошибок.

Зачем вообще условия в манифестах? Один и тот же чарт обслуживает десятки разных установок: где-то нужен Ingress, где-то — нет; где-то включают метрики, автоскейлинг, отдельный ServiceAccount. Вместо того чтобы плодить пять вариантов чарта, автор пишет один и обкладывает опциональные части условиями if. Включение фичи становится одной строкой в values пользователя — в этом и состоит главная ценность шаблонизации: один чарт, множество конфигураций.

Операторы сравнения — это функции

В Go templates нет инфиксных операторов; сравнения — функции eq, ne, lt, gt, and, or, not:

{{- if eq .Values.service.type "LoadBalancer" }}
  # ...
{{- end }}
{{- if and .Values.metrics.enabled (gt (.Values.replicaCount | int) 1) }}
  # метрики при нескольких репликах
{{- end }}

Запомните: a < b пишется как lt a b, а a == b — как eq a b. Это сбивает с толку новичков, привыкших к инфиксу.

with — сменить контекст и сократить путь

Когда вы много раз обращаетесь к глубоко вложенному значению, with временно делает его текущим контекстом (точкой):

{{- with .Values.image }}
image: "{{ .repository }}:{{ .tag }}"
pullPolicy: {{ .pullPolicy }}
{{- end }}

Внутри with .Values.image точка . = .Values.image, поэтому пишем просто .repository. Бонус: блок with выполняется, только если значение непустое — это и проверка на существование. Чтобы достать корневые объекты внутри, используйте $: $.Release.Name.

У двойной природы with — и сокращение пути, и проверка на непустоту — есть оборотная сторона. Поскольку блок не выполнится при «ложном» значении, нельзя использовать with там, где пустое значение легитимно и блок всё равно нужен. Если же with применяют только ради короткого синтаксиса, держите в голове, что вложенные обращения вроде .Values.global внутри блока сломаются — точка-то уже другая. На практике with особенно уместен для опциональных секций конфигурации: «если пользователь задал блок image, отрендерь его поля» — здесь совмещение проверки и сокращения пути работает идеально и читается естественно.

range — цикл по списку или карте

env:
{{- range .Values.extraEnv }}
  - name: {{ .name }}
    value: {{ .value | quote }}
{{- end }}

Внутри range точка — текущий элемент. Для списка пар name/value это удобно. По карте можно итерировать с ключом и значением:

{{- range $key, $val := .Values.labels }}
  {{ $key }}: {{ $val | quote }}
{{- end }}

Форма с переменными $key, $val особенно ценна потому, что обходит главную ловушку range — подмену точки. Когда вы пишете range $item := .Values.list, точка внутри остаётся прежней (это поведение отличается от range .Values.list без переменных), а текущий элемент лежит в $item. Это даёт доступ и к элементу, и к окружающему контексту одновременно, без обращения к $. Многие опытные авторы чартов предпочитают именно форму с явной переменной — код получается понятнее и реже ломается при рефакторинге.

Ещё одна тонкость: порядок обхода карты в range $key, $val отсортирован по ключу. Это сделано намеренно, чтобы рендер был детерминированным — иначе один и тот же чарт при каждом запуске давал бы манифесты с переставленными строками, и helm diff или GitOps-инструменты постоянно видели бы «изменения», которых нет. Детерминированность вывода — важное свойство: одинаковый вход обязан давать побайтово одинаковый выход.

Как меняется контекст под капотом

Ключевой механизм: и with, и range переопределяют точку внутри блока. Это причина №1 ошибок «не вижу .Values внутри цикла». Helm хранит исходный корень в переменной $, доступной всегда. Поэтому правило: как только вы вошли в with/range и вам понадобился .Release, .Values или .Chart — пишите $.Release, $.Values. Можно также сохранить нужное в переменную до входа: {{- $relName := .Release.Name }}, и пользоваться $relName внутри.

Частые ошибки

  • Числовой ноль как «ложь». if .Values.count при count: 0 ложно; используйте if not (empty .Values.count) или сравнение.
  • Потеря контекста в range/with. .Release.Name внутри не работает — нужен $.Release.Name.
  • Инфиксные операторы. if .a == .b — синтаксическая ошибка; пишите if eq .a .b.

Итог

  • if/else с функциями-сравнениями (eq, lt, and); ноль и пустые значения — ложь.
  • with сокращает путь и проверяет существование; range итерирует списки и карты.
  • Внутри with/range точка подменяется — корень всегда доступен через $.
Проверьте себя
1. Как записать условие «service.type равен LoadBalancer»?
Aif .Values.service.type == "LoadBalancer"
Bif eq .Values.service.type "LoadBalancer"
Cif .Values.service.type is LoadBalancer
Dif equal(...)
2. Что делает блок with .Values.image?
AУдаляет image
BДелает .Values.image текущим контекстом (точкой) внутри блока и выполняется, если значение непусто
CСоздаёт цикл
DШифрует image
3. Как получить .Release.Name внутри блока range?
AПросто .Release.Name
B$.Release.Name — через ссылку на корневой контекст
CНикак
D.Values.Release.Name