Управляющие конструкции: 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точка подменяется — корень всегда доступен через$.