Aller au contenu
Les modèles en profondeur

Les modèles en profondeur

200 Pratiquer ⏱ 1 h 25 helmkubernetes

À la fin, vous saurez

  • Lire les objets intégrés d'un modèle et savoir d'où vient chaque valeur
  • Composer des pipelines avec les fonctions default, required, quote, toYaml, nindent et include
  • Écrire des conditions et des boucles en maîtrisant la portée du point et la variable $
  • Contrôler les espaces avec {{- et -}} pour produire du YAML valide
  • Expliquer pourquoi lookup renvoie une valeur vide avec helm template et quand il fonctionne
  • Diagnostiquer une erreur d'indentation, de type ou de pointeur nul à partir de son message

Prérequis

Testé avec helm 3.16.3 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

Le chart de Signalements écrit à la leçon 4 n'utilise qu'une poignée de constructions. Dès que l'on veut aller plus loin (un bloc optionnel, une liste de variables d'environnement fournie par l'utilisateur, une annotation calculée), on rencontre le langage des modèles dans toute sa subtilité : un langage de gabarits de texte, pas de YAML, avec ses règles de portée, d'espaces et de types. La plupart des difficultés de Helm viennent de là. Un modèle qui « semble juste » produit un YAML mal indenté, une valeur absente devient un pointeur nul, un with change le sens du point et fait échouer un .Release.Name quelques lignes plus bas.

Cette leçon donne le modèle mental qui évite de deviner. Chaque exemple a été rendu avec helm template (Helm 3.16.3), et les erreurs montrées sont des messages réels. Le langage est le même avec Helm 4.

Les concepts

Un moteur de texte

Helm utilise le package text/template du langage Go, enrichi des fonctions de la bibliothèque Sprig et de quelques fonctions propres à Helm (include, required, toYaml, tpl, lookup...). Ce moteur ne connaît ni YAML, ni Kubernetes : il lit un texte, remplace les actions entre {{ et }} par leur résultat, et laisse le reste intact. Le résultat est un texte que Helm lit ensuite comme du YAML. Tout ce qui suit découle de cette séparation.

Les objets intégrés

À l'entrée d'un modèle, un objet racine, le point (.), donne accès à des données organisées en sous-objets :

ObjetContenu
.Valuesles valeurs fusionnées (défauts du chart, fichiers, --set)
.Release.Name, .Namespace, .IsInstall, .IsUpgrade, .Revision, .Service (vaut Helm)
.Chartle contenu de Chart.yaml : .Name, .Version, .AppVersion... (la première lettre en majuscule)
.Capabilitiesce que le cluster sait faire : .KubeVersion.Version, .APIVersions.Has "...", .HelmVersion
.Filesl'accès aux fichiers du chart autres que les modèles : .Files.Get "chemin", .Files.Glob, .Files.AsConfig
.Template.Name et .BasePath du modèle en cours

Voyons-les ensemble avec un chart d'exemple dont le modèle est un ConfigMap :

# values.yaml
nom: signalements
port: 8000
actif: false
vide: ""
etiquettes:
  equipe: voirie
  cycle: prod
env:
  - {name: LOG_LEVEL, value: info}
  - {name: TZ, value: Europe/Paris}
modele: "https://{{ .Release.Name }}.formation.test"
# templates/t.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
data:
  release: {{ .Release.Name | quote }}
  ns: {{ .Release.Namespace | quote }}
  install: {{ .Release.IsInstall | quote }}
  revision: {{ .Release.Revision | quote }}
  chart: {{ .Chart.Name }}-{{ .Chart.Version }}
  appVersion: {{ .Chart.AppVersion | quote }}
  kube: {{ .Capabilities.KubeVersion.Version | quote }}
  helm: {{ .Capabilities.HelmVersion.Version | quote }}
  gateway: {{ .Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1" | quote }}
  fichier: {{ .Files.Get "files/exemple.txt" | trim | quote }}
  defaut: {{ .Values.vide | default "valeur-par-defaut" | quote }}
  majuscules: {{ .Values.nom | upper | quote }}
  port: {{ .Values.port | quote }}
  tpl: {{ tpl .Values.modele . | quote }}

Sortie réelle de helm template demo scratch -n formation :

apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-scratch
data:
  release: "demo"
  ns: "formation"
  install: "true"
  revision: "1"
  chart: scratch-0.1.0
  appVersion: "1.2.0"
  kube: "v1.31.0"
  helm: "v3.16.3"
  gateway: "false"
  fichier: "mot: bonjour"
  defaut: "valeur-par-defaut"
  majuscules: "SIGNALEMENTS"
  port: "8000"
  tpl: "https://demo.formation.test"

Plusieurs enseignements.

  • helm template invente son contexte : la release n'existe pas, donc .Release.IsInstall est vrai, la révision vaut 1, et .Capabilities.KubeVersion est une valeur par défaut de Helm (v1.31.0 pour cette version, sans rapport avec votre cluster). gateway: "false" parce que helm template ne sait pas quelles API le cluster expose. Pour tester un modèle qui dépend des capacités, il faut les fournir : --kube-version 1.36.0 --api-versions gateway.networking.k8s.io/v1, avec lequel la même ligne devient gateway: "true". Avec helm install sur un vrai cluster, ces valeurs sont lues sur le cluster.
  • Les noms des champs de .Chart commencent par une majuscule (.Chart.AppVersion), alors que les clés de .Values gardent la casse de votre fichier (.Values.replicaCount). La différence vient de ce que .Chart est une structure Go et .Values une table de clés libres.
  • .Files.Get lit un fichier du chart (ici files/exemple.txt, qui contient mot: bonjour) : utile pour embarquer un fichier de configuration dans un ConfigMap. Les fichiers listés dans .helmignore, et ceux de templates/, ne sont pas accessibles.

Les pipelines

Comme dans un shell, le tube | envoie le résultat de gauche comme dernier argument de la fonction de droite : .Values.nom | upper | quote équivaut à quote (upper .Values.nom). La forme tube se lit de gauche à droite, ce qui est plus naturelle pour enchaîner plusieurs transformations.

Le point crucial est « dernier argument ». Pour default, dont la signature est default VALEUR_PAR_DEFAUT VALEUR, on écrit donc .Values.vide | default "défaut", soit default "défaut" .Values.vide : la valeur à tester vient en dernier, donc par le tube.

Les fonctions de tous les jours

Les fonctions viennent de Sprig et de Helm. Voici celles à connaître, avec des sorties réelles pour les moins évidentes :

FonctionRôleExemple
defaultvaleur de repli si la valeur est « vide » (nulle, "", 0, false, liste ou map vide).Values.tag | default .Chart.AppVersion
requiredfait échouer le rendu avec un message si la valeur est viderequired "tag obligatoire" .Values.tag
quote, squoteentoure de guillemets doubles (simples).Values.nom | quote
upper, lower, trim, trunc N, trimSuffix, replacemanipulations de chaînes.Values.nom | trunc 5
printfformatage à la manière de Goprintf "%s-%s" .Release.Name .Chart.Name
toYaml, toJson, fromYamlconversion entre structure et textetoYaml .Values.resources
indent N, nindent Nindente de N espaces (nindent ajoute d'abord un retour à la ligne)toYaml .Values.x | nindent 4
includerend un fragment nommé et le renvoie comme texteinclude "chart.labels" .
tplrend une chaîne comme un modèletpl .Values.modele .
lookuplit un objet du clustervoir plus bas
failarrête le rendu avec un messagefail "combinaison invalide"

quote : pourquoi on l'utilise tant

Dans un fichier YAML, port: 8000 est un entier, actif: true un booléen, version: 1.20 un nombre à virgule, tag: 0123 un entier octal selon l'analyseur. Une valeur qui doit être une chaîne doit donc être citée : {{ .Values.tag | quote }}. C'est impératif pour les variables d'environnement (env.value doit être une chaîne : value: 8000 est refusé par l'API) et pour les étiquettes et annotations (leurs valeurs sont des chaînes). quote met des guillemets doubles et échappe ce qu'il faut.

toYaml et nindent

toYaml convertit une structure (map, liste) en texte YAML ; c'est l'outil pour insérer un bloc fourni par l'utilisateur (les ressources, les tolérances, les annotations). Le texte produit tient sur plusieurs lignes, et l'insertion dans un modèle demande de le réindenter à la profondeur de la clé :

  etiquettes:
    {{- toYaml .Values.etiquettes | nindent 4 }}

nindent 4 commence par un retour à la ligne, puis indente chaque ligne de 4 espaces. Le {{- qui précède supprime l'espace et le saut de ligne avant l'action, pour que l'on n'obtienne pas une ligne vide. C'est la forme canonique à connaître par cœur.

include et template

Pour appeler un fragment défini avec define, il existe deux formes. La action template "nom" . insère le résultat directement dans le texte ; elle n'est pas une fonction, donc on ne peut pas lui appliquer de tube. La fonction include "nom" . renvoie le résultat comme une chaîne, que l'on peut filtrer : include "chart.labels" . | nindent 4. C'est pour cela que l'on utilise toujours include dans Helm. Le piège est classique : template "x" . | upper ne produit pas l'erreur attendue, parce que le moteur lit | upper comme un tube appliqué à l'argument du template. Sortie réelle :

$ helm template demo scratch
Error: template: scratch/templates/t.yaml:1:36: executing "scratch/templates/t.yaml" at <upper>: wrong type for value; expected string; got chartutil.Values

upper a reçu non pas le texte du fragment, mais le point (chartutil.Values), et refuse un non-texte. Avec include, le résultat est une chaîne et le tube agit sur lui.

Le deuxième argument d'include est le contexte passé au fragment : presque toujours le point (.), pour que le fragment voie .Values, .Release et le reste. Passer autre chose (.Values.etiquettes) restreint le fragment à cette donnée : utile à l'occasion, mais le fragment ne verra plus .Release.

tpl

Les valeurs de values.yaml sont du texte que Helm ne rend pas comme un modèle. Si un utilisateur écrit modele: "https://{{ .Release.Name }}.formation.test", {{ .Values.modele }} affiche littéralement https://{{ .Release.Name }}.formation.test. tpl rend la chaîne comme un modèle, avec le contexte donné : {{ tpl .Values.modele . }} produit https://demo.formation.test (voir la sortie plus haut). C'est utile pour laisser l'utilisateur composer des noms ou des annotations avec des références à la release, mais c'est aussi une porte d'exécution de modèles à partir de valeurs : voir la section Sécurité.

Le contrôle de flux

if

  {{- if .Values.actif }}
  actif: "oui"
  {{- else }}
  actif: "non"
  {{- end }}

La condition est « vraie » si la valeur n'est pas vide. Sont faux : false, 0, "", nil, une liste ou une map vide. Sont vrais : tout le reste, y compris la chaîne "false" (non vide) ! Une valeur passée par --set-string actif=false est la chaîne "false" et donc vraie : un piège concret. Sortie réelle de quelques tests (valeurs du chart d'exemple) :

data:
  vide: vide
  zero: faux
  liste: oui
  inconnu: non
  eq: ok
  et: y
  taille: 2

Les modèles étaient {{ if .Values.vide }}rempli{{ else }}vide{{ end }} (chaîne vide, donc faux), {{ if 0 }}..., {{ if .Values.env }}oui{{ end }} (liste non vide), {{ if .Values.absent }}... (clé inexistante : faux, sans erreur), {{ if eq .Values.nom "signalements" }}, et {{ if and .Values.actif (not .Values.vide) }} (actif vaut false). Les comparaisons sont des fonctions à préfixe : eq, ne, lt, gt, and, or, not. On écrit eq .Values.nom "x", pas .Values.nom == "x".

with et la portée du point

with teste une valeur et, si elle n'est pas vide, change la portée du point : à l'intérieur, . désigne cette valeur.

  {{- with .Values.etiquettes }}
  {{- toYaml . | nindent 4 }}
  {{- end }}

L'intérêt : ne rien produire si les étiquettes sont vides, et écrire . au lieu de .Values.etiquettes. Le revers : à l'intérieur du bloc, .Release.Name n'existe plus, parce que le point n'est plus l'objet racine. Message réel :

Error: template: scratch/templates/t.yaml:3:16: executing "scratch/templates/t.yaml" at <.Release.Name>: nil pointer evaluating interface {}.Name

Le remède est la variable $, qui désigne toujours l'objet racine, quel que soit l'endroit : {{ $.Release.Name }}. On la retrouve dans presque tous les range et with qui touchent à la release.

range

range répète un bloc pour chaque élément d'une liste ou d'une map. Le point désigne à chaque tour l'élément courant.

  {{- range .Values.env }}
  {{ .name }}: {{ .value | quote }}
  {{- end }}
  {{- range $cle, $val := .Values.etiquettes }}
  etiquette-{{ $cle }}: {{ $val }}
  {{- end }}

Avec la forme $cle, $val :=, on obtient la clé (ou l'indice) et la valeur : pour une map, les clés sont parcourues dans l'ordre alphabétique, ce qui rend le rendu déterministe. Sortie réelle, dans le contexte complet ci-dessous.

Les variables

Une variable se déclare avec := et se réaffecte avec = :

  {{- $nom := printf "%s-%s" .Release.Name .Chart.Name }}
  nom: {{ $nom }}

Elle vit jusqu'à la fin du bloc où elle est déclarée (if, with, range, ou le fichier). Les variables servent à garder une valeur que le changement de portée rendrait inaccessible, ou à nommer un calcul répété.

Les espaces : {{- et -}}

C'est la source d'erreurs la plus fréquente. Le moteur laisse intact tout le texte entre les actions, y compris les retours à la ligne et les espaces. Une ligne qui ne contient qu'une action {{ if ... }} produit une ligne vide dans le résultat. Pour l'éviter, un tiret accolé à l'accolade supprime les espaces (et les retours à la ligne) du côté du tiret : {{- supprime tout ce qui précède, jusqu'au dernier caractère non blanc ; -}} supprime tout ce qui suit. La syntaxe exige une espace entre le tiret et l'action : {{- if ...}}, pas {{-if.

Voici un modèle réunissant les constructions précédentes, et son rendu réel :

apiVersion: v1
kind: ConfigMap
metadata:
  name: demo
  labels:
    {{- include "scratch.labels" . | nindent 4 }}
    {{- with .Values.etiquettes }}
    {{- toYaml . | nindent 4 }}
    {{- end }}
data:
  {{- if .Values.actif }}
  actif: "oui"
  {{- else }}
  actif: "non"
  {{- end }}
  {{- range .Values.env }}
  {{ .name }}: {{ .value | quote }}
  {{- end }}
  {{- range $cle, $val := .Values.etiquettes }}
  etiquette-{{ $cle }}: {{ $val }}
  {{- end }}
  {{- with .Values.etiquettes }}
  release-dans-with: {{ $.Release.Name }}
  {{- end }}
  {{- $nom := printf "%s-%s" .Release.Name .Chart.Name }}
  nom: {{ $nom }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo
  labels:
    app.kubernetes.io/name: scratch
    app.kubernetes.io/instance: demo
    cycle: prod
    equipe: voirie
data:
  actif: "non"
  LOG_LEVEL: "info"
  TZ: "Europe/Paris"
  etiquette-cycle: prod
  etiquette-equipe: voirie
  release-dans-with: demo
  nom: demo-scratch

Observez que chaque ligne de contrôle commence par {{-, ce qui « colle » son résultat à la ligne précédente, et que l'indentation du texte produit ne dépend que des lignes de contenu. Les clés des étiquettes sortent triées (cycle avant equipe) bien que le fichier les ait dans l'ordre inverse : toYaml trie les clés d'une map.

lookup : lire le cluster

lookup apiVersion kind namespace nom lit un objet existant dans le cluster pendant le rendu, et renvoie une map (ou une liste de maps pour une recherche sans nom). Cas d'usage typique : ne pas régénérer un mot de passe aléatoire à chaque mise à jour, en réutilisant celui du Secret déjà créé.

Mais lookup ne fonctionne que si Helm parle réellement au cluster. La documentation le dit : Helm « is not supposed to contact the Kubernetes API Server during a helm template|install|upgrade|delete|rollback --dry-run operation », donc lookup y renvoie une map vide. Réel :

$ helm template demo scratch    # avec  existe: {{ lookup "v1" "ConfigMap" "formation" "x" | toJson }}
  existe: {}

Pour le tester sans installer, la documentation indique d'utiliser --dry-run=server, qui contacte le serveur. Ce comportement a deux conséquences. Un modèle qui dépend de lookup doit toujours prévoir le cas d'une réponse vide ({{ if $secret }}...{{ else }}...{{ end }}). Et le rendu diffère entre helm template (sans cluster) et helm install (avec) : c'est un défaut pour la prévisibilité, et une raison de n'utiliser lookup qu'en dernier recours. Argo CD, qui rend les charts sans cluster à la manière de helm template, ne le résout pas non plus : un chart qui dépend de lookup se rend autrement sous Argo CD (leçon 10).

En pratique

La meilleure façon d'apprendre est de casser des modèles. Dans le chart d'exemple, placez ce fichier dans templates/t.yaml et lancez helm template demo . à chaque variante.

Une erreur à la fois

Indentation de include. Le fragment produit deux lignes ; avec indent au lieu de nindent, la première ligne se colle après {{, et l'indentation de la suivante est fausse :

metadata:
  labels:
    {{ include "scratch.labels" . | indent 4 }}
Error: YAML parse error on scratch/templates/t.yaml: error converting YAML to JSON: yaml: line 3: did not find expected key

Use --debug flag to render out invalid YAML

Avec --debug, Helm affiche le texte produit et son erreur. La première ligne est décalée (4 espaces d'indentation de la ligne + 4 d'indent), la deuxième est à 4 : le YAML est invalide. Correctif : {{- include "scratch.labels" . | nindent 4 }}, avec le tiret et nindent. À noter que, dans un rendu où l'indentation serait seulement fausse mais pas invalide, aucune erreur n'apparaît : les clés atterrissent au mauvais niveau. Réel, avec un nindent 4 sans {{- dans un bloc déjà indenté :

  annotations:
    a: "1"
    
    app.kubernetes.io/name: scratch
    app.kubernetes.io/instance: demo

Une ligne vide (inoffensive ici) et les étiquettes rangées parmi les annotations : l'objet est valide, et faux. Seule la relecture du rendu le révèle.

Pointeur nul. On écrit .Values.absent.profond alors que absent n'existe pas :

Error: template: scratch/templates/t.yaml:2:15: executing "scratch/templates/t.yaml" at <.Values.absent.profond>: nil pointer evaluating interface {}.profond

Lire .Values.absent donne nil sans erreur (c'est pourquoi if .Values.absent fonctionne), mais lire un champ de nil échoue. Le remède est de tester le parent ({{ if .Values.absent }}), ou d'utiliser dig ou default (dict) pour fournir une map de repli : (.Values.absent | default dict).profond.

Un type inattendu. {{ .Values.etiquettes }} sans toYaml ne produit pas du YAML mais la représentation Go d'une map :

data:
  a: map[cycle:prod equipe:voirie]
  b: [{"name":"LOG_LEVEL","value":"info"},{"name":"TZ","value":"Europe/Paris"}]

La première ligne est invalide pour Kubernetes (un objet là où il attendait une chaîne). La seconde montre toJson, utile pour insérer un petit JSON dans une annotation ou un ConfigMap.

Déclencher un redéploiement quand la configuration change

Un cas d'usage célèbre combine tout ce qui précède. Un Deployment ne redémarre pas quand un ConfigMap change (leçon 5 du cours Kubernetes). Une technique courante : ajouter au modèle de pod une annotation qui contient l'empreinte du ConfigMap rendu, de sorte qu'un changement de configuration change le modèle, donc déclenche un remplacement des pods.

  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

include rend le fichier configmap.yaml du chart comme texte, sha256sum le résume, et l'annotation change dès qu'un octet du ConfigMap rendu change. $.Template.BasePath donne le chemin du répertoire des modèles (signalements/templates). Le chart de Signalements n'a pas de ConfigMap pour l'instant ; la technique sera utile dès qu'il en aura un.

Sous le capot

Deux passes. Helm traite chaque fichier de templates/ en le passant au moteur avec l'objet racine, ce qui produit une chaîne ; il lit ensuite cette chaîne comme du YAML, ce qui produit les objets. Les erreurs de la première passe nomment le fichier et la ligne du modèle. Celles de la seconde nomment la ligne du texte rendu, que --debug permet de lire. La confusion entre les deux numéros de ligne est le piège de diagnostic le plus courant.

Les fonctions viennent de trois sources. Les fonctions standard de text/template (eq, and, len, printf, index), la bibliothèque Sprig (default, trunc, b64enc, dict...) et les ajouts de Helm (include, tpl, required, toYaml, lookup, fail). Le code de Helm (pkg/engine/funcs.go) retire de Sprig quelques fonctions qui accèdent à l'environnement du poste (env, expandenv), pour que le rendu ne dépende pas de la machine qui l'exécute et ne puisse pas en lire des secrets.

required et fail. Ce sont des fonctions qui retournent une erreur : le moteur arrête alors tout le rendu et Helm affiche le message. L'argument de required est évalué avant l'appel, ce qui compte pour required "..." (.Values.a | default .Values.b).

Pièges courants

default avec un booléen. .Values.actif | default true renvoie true même quand l'utilisateur a mis false, parce que false est « vide ». Pour un booléen avec défaut vrai, on teste l'existence de la clé : {{ if hasKey .Values "actif" }}... ou ternary. Mieux : on fixe actif: true dans values.yaml, et le modèle n'utilise plus default.

Les nombres d'un fichier de valeurs sont des flottants. Un entier lu dans un fichier YAML arrive dans le modèle en float64 : avec nombre: 1000000, {{ .Values.nombre }} affiche 1e+06, et 12345678901 affiche 1.2345678901e+10 (réel, Helm 3.16.3). {{ .Values.nombre | int }} ou printf "%d" rend l'entier. Les petits nombres (3, 8000) s'affichent normalement ; c'est avec les grands (délais en millisecondes, tailles en octets) que le piège se déclenche. Notez aussi que par --set, le même nombre reste un entier.

Les clés avec un tiret. .Values.mon-parametre est une erreur de syntaxe : le moteur lit mon moins parametre. On écrit index .Values "mon-parametre". Pour cette raison, les clés de valeurs sont en camelCase.

Sécurité

  • tpl exécute des modèles issus des valeurs. Qui contrôle une valeur passée à tpl contrôle ce qui est rendu avec le contexte entier du chart : il lit .Values (donc tout ce qu'il contient), peut appeler lookup, include, fail. Si les valeurs viennent d'utilisateurs de confiance (votre dépôt de déploiement), le risque est faible ; si elles viennent d'un système tiers (un formulaire, une API), n'utilisez pas tpl sur elles.
  • lookup lit le cluster avec vos droits. Un chart qui l'utilise peut lire des Secrets du cluster pendant le rendu. Relisez les modèles d'un chart tiers qui l'emploient.
  • Les valeurs injectées dans le YAML sont du texte. Une valeur sans quote, contenant des guillemets, des retours à la ligne ou des deux-points, peut casser le YAML, voire y ajouter des champs : une injection de configuration. Citez systématiquement ce qui doit être une chaîne, et toYaml ce qui est une structure.
  • Pas de secret dans le rendu. Les manifestes rendus sont écrits dans le Secret de release (leçon 3) et affichés par helm template et --debug. Un b64enc appliqué à un mot de passe en valeur n'est pas du chiffrement.

En production

  • Garder les modèles simples. Un modèle qui contient dix imbrications de if est un programme, et s'écrit mal en texte. Déplacez la logique dans des fragments nommés, et le choix dans des valeurs claires.
  • Tester le rendu comme du code. Rendre le chart avec chaque jeu de valeurs réel (recette, production), conserver les rendus de référence, et comparer à chaque modification : le diff du rendu est la revue la plus utile. Des outils (helm unittest, tests de rendu en CI) automatisent cette comparaison.
  • Rendre indépendant du cluster. Évitez lookup et les décisions fondées sur .Capabilities quand c'est possible : Argo CD et helm template ne voient pas les mêmes choses qu'un helm install, et un rendu non déterministe rend une application perpétuellement désynchronisée (leçon 5 du cours GitOps).

Exercices

1. Prévoir un if (niveau 100). Pour chaque valeur de .Values.x, le bloc {{ if .Values.x }}A{{ else }}B{{ end }} produit-il A ou B ? Valeurs : true, "false", 0, [], "0", {}, nil.

Solution

A pour true, "false" (chaîne non vide) et "0" (chaîne non vide). B pour 0, [], {} et nil. La règle : seules les valeurs vides (false, 0, "", nil, liste ou map vide) sont fausses, et le moteur ne convertit pas les chaînes. Une valeur "false" passée par --set-string est donc vraie.

2. Corriger une portée (niveau 200). Ce fragment échoue avec nil pointer evaluating interface {}.Name. Pourquoi, et comment le corriger sans changer sa logique ?

{{- range .Values.env }}
- name: {{ .name }}
  value: {{ .value | quote }}
  release: {{ .Release.Name }}
{{- end }}
Solution

Dans le range, le point est l'élément courant de la liste (une map avec name et value) : .Release n'existe pas à ce niveau. Remplacer .Release.Name par $.Release.Name, où $ est l'objet racine. Autre solution : déclarer {{- $release := .Release.Name }} avant le range et écrire {{ $release }}.

3. Rendre un bloc optionnel (niveau 200). Écrivez le fragment qui ajoute à metadata du Service un bloc annotations fourni par la valeur service.annotations (une map), et qui n'écrit rien du tout si la map est vide. Quelle sortie pour {service: {annotations: {exemple.com/a: "1"}}} ?

Solution
metadata:
  name: {{ include "signalements.fullname" . }}
  {{- with .Values.service.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}

Pour la valeur donnée : annotations: suivi de exemple.com/a: "1" indenté de 4 espaces. Avec une map vide, le with ne produit rien : ni la ligne annotations:, ni ligne vide, grâce aux {{-. toYaml conserve les guillemets sur "1", ce qui est nécessaire pour une valeur d'annotation.

4. Où est l'erreur (niveau 200) ? Un collègue écrit image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} et déclare que la version 1.20 n'est pas déployée correctement quand elle vient d'un fichier de valeurs avec tag: 1.20. Qu'obtient-il, pourquoi, et comment le corriger dans le fichier et dans le modèle ?

Solution

Dans un fichier YAML, 1.20 est un nombre à virgule flottante : il devient 1.2 à l'affichage, et l'image déployée est ...:1.2. Dans le fichier, il faut écrire tag: "1.20" avec des guillemets. Dans le modèle, on se protège avec {{ .Values.image.tag | default .Chart.AppVersion | toString }} ou, mieux, on documente l'obligation de guillemets dans values.yaml et l'on valide par un schéma (leçon 6) que tag est de type chaîne. Avec --set image.tag=1.20, en revanche, la valeur reste une chaîne "1.20" (le décodage de --set ne convertit pas les nombres à virgule) : le problème est propre aux fichiers.

Récapitulatif

  • Le moteur de modèles produit du texte ; Helm le lit ensuite comme du YAML. Les erreurs de la première passe nomment la ligne du modèle, celles de la seconde la ligne du texte rendu (--debug l'affiche).
  • Objets intégrés : .Values, .Release, .Chart (champs en majuscule), .Capabilities, .Files, .Template. helm template invente le contexte : passez --kube-version et --api-versions pour tester les capacités.
  • Le tube | transmet comme dernier argument. À retenir : default, required, quote (indispensable pour les chaînes), toYaml | nindent N, include (une chaîne que l'on peut filtrer, contrairement à template), tpl (rend une chaîne comme modèle).
  • if teste la non-vacuité : false, 0, "", nil, liste et map vides sont faux ; "false" est vrai. with et range changent le point ; $ désigne toujours la racine.
  • {{- et -}} suppriment les espaces voisins ; chaque ligne de contrôle commence par {{-.
  • lookup renvoie une map vide avec helm template et --dry-run, et ne lit le cluster qu'avec --dry-run=server, install ou upgrade : à éviter, et à prévoir vide.
  • Pièges de sécurité : tpl sur des valeurs non fiables, absence de quote, secrets dans le rendu.

Pour aller plus loin

  • Le Chart Template Guide de la documentation de Helm, qui suit pas à pas les mêmes notions avec d'autres exemples.
  • La documentation du package text/template de Go, qui définit précisément les types, les comparaisons et les règles d'espaces, et celle de Sprig pour le catalogue complet des fonctions.
  • Le code de pkg/engine dans helm/helm, en particulier funcs.go, pour voir ce que Helm ajoute et retire.
  • La leçon suivante, Valeurs, schéma et fonctions d'aide (leçon 6), qui valide les valeurs par un schéma plutôt que par required seul.
Voir ma constellation →

Sources