Le ticket était précis, et il se trompait dans le sens qui coûte le plus cher : il sous-estimait le défaut.
sh0 génère un Dockerfile à partir de ce qu'il détecte dans votre dépôt. Pour Python, l'étape de build installe les dépendances dans /install, et l'étape de production copie /install vers /usr/local. Deux lignes de contrat, trois branches :
dockerfileCOPY requirements.txt* pyproject.toml* ./
RUN if [ -f requirements.txt ]; then pip install --prefix=/install -r requirements.txt; \
elif [ -f pyproject.toml ]; then pip install --prefix=/install .; \
elif [ -f Pipfile ]; then pip install pipenv && pipenv install --deploy --system; fiLe ticket déposé, ISSUE-073, disait : la troisième branche oublie --prefix=/install. pipenv --system installe dans les site-packages de l'étape de build ; l'étape de production ne copie que /install, donc un projet Pipfile livre une image avec gunicorn et aucune de ses propres dépendances. Build vert, ModuleNotFoundError au démarrage du conteneur.
Diagnostic correct. Relisez la ligne COPY.
requirements.txt<em>, pyproject.toml</em>. Pas de Pipfile. Le fichier que la branche teste n'est jamais arrivé dans l'étape de build. [ -f Pipfile ] était faux sur tous les déploiements jamais passés par ce template. La branche n'était pas mal configurée — elle était inatteignable. Personne n'avait jamais rencontré le bug décrit par le ticket, parce que personne n'avait jamais atteint le code qui l'aurait provoqué. Le symptôme observable était identique, et c'est exactement pour cela que la mauvaise cause racine restait plausible.
Pourquoi le ticket méritait quand même confiance
La leçon tentante, c'est « ne jamais faire confiance au rapport ». Elle est fausse, et coûteuse. ISSUE-073 a été rédigé à partir d'une lecture de code, pendant la revue d'un correctif voisin, par quelqu'un qui a trouvé une vraie violation de contrat et l'a décrite fidèlement jusqu'où il avait lu. Le mode d'échec n'est pas la négligence : c'est de s'être arrêté une ligne au-dessus de la réponse. L'instruction RUN est l'endroit où vit la logique, donc l'œil y va. Le COPY au-dessus a l'air de tuyauterie.
Le contrôle qui attrape cette classe de défauts ne coûte rien : devant toute condition portant sur un fichier, demander d'où vient ce fichier. Dans un build Docker multi-étapes, la réponse n'est jamais « il est là, c'est tout ».
Le correctif, et l'option écartée
Deux changements, appliqués aux quatre templates Python — générique, Django, FastAPI, Flask :
dockerfileCOPY requirements.txt* pyproject.toml* Pipfile* ./
...
elif [ -f Pipfile ]; then pip install pipenv && \
if [ ! -f Pipfile.lock ]; then pipenv lock; fi && \
pipenv requirements > /tmp/pipenv-req.txt && \
pip install --prefix=/install -r /tmp/pipenv-req.txt; fiLe glob ramasse aussi Pipfile.lock. La branche convertit ensuite le lock en fichier requirements et le passe au même pip install --prefix=/install qu'utilisent les deux autres branches.
L'autre candidat était PIP_PREFIX=/install pipenv install --deploy --system — plus court, et ça marche probablement, puisque pipenv délègue à pip. Probablement est le problème. Que pipenv transmette ou non cette variable d'environnement est un détail d'implémentation d'un outil qu'on ne contrôle pas ; pipenv requirements qui produit un fichier requirements est un comportement documenté. Une ligne de plus, et on cesse de dépendre de ce que personne n'a promis.
--deploy a été abandonné au passage, et cela mérite d'être dit plutôt que caché : il fait échouer le build quand le lock est périmé, ce qui est une vraie garantie dont certaines équipes veulent disposer. Le rétablir tient en une ligne si un utilisateur le demande. Garder en silence un drapeau dont le mode d'échec est « votre déploiement s'arrête » n'était pas une décision à prendre à leur place.
Ce que le test vérifie
Pas « la chaîne --prefix=/install apparaît dans le fichier » — elle y apparaît trois fois de toute façon, et le test serait passé sur la version cassée. L'assertion est bornée à la ligne de la branche Pipfile elle-même, et elle contrôle les deux moitiés : que Pipfile* parvienne à l'étape de build, et que la branche installe dans le préfixe partagé. Sur les quatre templates, parce qu'un correctif appliqué à trois sur quatre, c'est le même défaut avec un rayon d'action plus petit.
Ce qui n'est pas fait
Tout cela a été trouvé en lisant du code et corrigé en lisant du code. Aucun projet Pipfile n'a été déployé à travers le template corrigé sur une vraie machine. Le contrôle statique est vert — 730 tests, clippy propre — et un contrôle statique ne peut pas dire si pipenv requirements émet des hachages que pip install va ensuite imposer en mode hash-checking. Il le devrait. On ne l'a pas encore vu le faire.
C'est pour cela que la revérification en conditions réelles est un élément de file et non une case cochée dans la release, et pour cela que ce paragraphe existe au lieu d'une phrase affirmant que le sujet est clos.