0066 — Rollback atomique : composants + graphe de dépendances unique
Accepted (2026-06-13)
Contexte
Section intitulée « Contexte »Le rollback par phase (ADR 0054) défait une
phase de run-phases.sh via une table de périmètre (rollback-lib.sh :
rollback_phase_namespaces / _targeted_resources / _crd_groups /
_has_nodeside / _downstream). Cette table est indexée par phase — mais
une phase est composite : dataops monte ~10 composants (registry,
cert-manager, operator CNPG, plugin Barman, instance CNPG, backing S3, Dagster,
Marquez…) vivant dans 5 namespaces (postgres, cnpg-system, dagster,
marquez, + une OBC dans rook-ceph). La table doit ré-énumérer à la main
l’union de ces composants.
Ce modèle est fragile par construction, et un run réel l’a prouvé en cascade :
rollback_phase_namespaces(dataops)oubliaitcnpg-system(l’operator CNPG survivait au rollback et re-réconciliait le Cluster pendant la destruction — drift bloquant) ;- l’OBC
cnpg-backups(dansrook-ceph) fut oubliée, puis rajoutée ; - l’OBC
loki-bucketsde même (#319) ; - l’
ObjectStoreBarman (postgres) coinçait le ns enTerminating, rajouté après coup aux finalizers forcés.
Le motif n’est pas un bug isolé : chaque composant d’une phase composite est une occasion d’oublier une ressource, découverte rétrospectivement par un run qui laisse un résidu (coûteux). S’ajoutent deux défauts structurels :
- deux graphes de dépendances parallèles, tous deux manuels et au grain
phase :
rollback_phase_downstream(rollback-lib) et_DEPENDENTS(roundtrip.py, « validé à la main », explicitement non dérivé du premier) → divergence latente ; - le périmètre d’une phase agrégée (
atlas-ceph) n’existe qu’en intension (« on défait dans l’ordre inverse ») : il est supposé être l’union correcte des tables atomiques — l’hypothèse même que les oublis ci-dessus invalident.
Décision
Section intitulée « Décision »Déplacer l’unité du périmètre et du graphe de la PHASE (composite) vers le COMPOSANT ATOMIQUE, et faire du graphe de dépendances atomique la SOURCE UNIQUE.
Composant atomique
Section intitulée « Composant atomique »La plus petite unité dont le périmètre de rollback est à la fois trivial (≤ 1 namespace propre + ses CRD propres + ses ressources hors-ns explicitement attachées) et complet (tout ce qu’il pose, rien qu’un autre pose). Règle de découpe :
- poser X crée un namespace que personne d’autre ne possède → X est atomique
sur ce ns (
cnpg-operatorpossèdecnpg-system,cnpg-cluster-pgpossèdepostgres: deux composants distincts — l’oubli decnpg-systemdevient structurellement impossible) ; - poser X dépose une ressource dans le ns d’un autre (OBC dans
rook-ceph, plugin danscnpg-system) → cette ressource est untargetedexplicite du composant qui la crée, jamais un résidu du composant qui possède le ns.
Trois invariants (testables sans banc, bats)
Section intitulée « Trois invariants (testables sans banc, bats) »- Trivialité : un composant a au plus un namespace propre.
- Complétude par ownership : toute ressource hors-ns est attachée
déclarativement à son producteur (OBC
cnpg-backups→ composants3-backing-cnpg). Pas de ressource orpheline. - Graphe unique : un seul graphe atomique (composant → dépendances) est
la source de vérité. Il dérive (a) l’ordre de montage d’un alias (tri
topologique), (b) l’ordre de rollback (inverse), (c) la clôture descendante
du
roundtrip(transitive).rollback_phase_downstreametroundtrip.py:_DEPENDENTSsont remplacés par des dérivations de ce graphe — fin des deux sources.
Phase = alias
Section intitulée « Phase = alias »dataops/monitoring/gitops/atlas-ceph ne sont plus des entités de premier
ordre côté périmètre : ce sont des alias désignant un sous-ensemble du
graphe atomique. Monter un alias = monter ses composants en ordre topologique ;
le défaire = clôture descendante en ordre inverse. Le périmètre composite
n’est plus en intension : c’est l’union CALCULÉE des périmètres atomiques
(garantie complète par l’invariant de complétude). Un composant peut être
conditionnel au profil (seaweedfs vs s3-backing-loki) ; la condition vit
dans le composant (when:), pas dans l’alias.
Compatibilité doctrine
Section intitulée « Compatibilité doctrine »- ADR 0045 (chemins nommés
codés) : les alias restent des chemins nommés codés — définis comme
expansion du graphe au lieu d’une séquence en dur. L’API CLI de
run-phases.shne change pas (up/bootstrap/ceph/dataops/atlas-ceph… restent les noms publics). - ADR 0054 : généralisé en rollback par composant (l’alias-rollback dérive la clôture). 0054 reste valable ; cet ADR le raffine.
- ADR 0023 : valeurs génériques
inchangées. « Corriger le code, pas l’état » : la complétude est
prouvée par un cycle
monte → rollback → état-propre(zéro résidu) sur banc, pas supposée.
Conséquences
Section intitulée « Conséquences »rollback-lib.sh: les fonctions*_phase_*deviennent*_component_*+component_deps(graphe) +component_expand_alias+topo_sort(pur, remplace un_MOUNT_ORDERen dur)._STUCK_CR_KINDSdevient une union dérivée des CRD à finalizer des composants.roundtrip.py: supprime_DEPENDENTSet_MOUNT_ORDER, les dérive du graphe atomique derollback-lib.sh.closure()opère sur composants. Fin de la seconde source de vérité.run-phases.sh: dispatch et noms publics inchangés ; en interne, un alias itère ses composants en ordre topologique.
Migration incrémentale (le graphe ne casse rien avant d’être prouvé)
Section intitulée « Migration incrémentale (le graphe ne casse rien avant d’être prouvé) »Cet ADR fonde un plan (plan-rollback-atomique.md) :
- Lot 0-2 (CI seule, zéro banc) : écrire le graphe atomique + les fonctions
par composant à côté des fonctions par phase (rien retiré) ; tests bats
des invariants (trivialité, acyclicité, déterminisme, et l’assertion qui
aurait attrapé l’oubli
cnpg-system) ; faireroundtrip.pyconsommer le graphe (fin de la 2ᵉ source). Aucune bascule du rollback réel. - Lot 3 (bascule alias par alias, avec banc) : un alias dérive sa clôture du
graphe ; commencer par
dataops(oublis prouvés) ; prouver par cycle monte→rollback→état-propre (zéro résidu) avant de retirer l’anciencase. - Lot 4 (montage par graphe) : l’ordre de montage dérive du tri topologique
; pré-condition vérifiée en lot 1 (le topo-sort reproduit exactement
l’ordre codé actuel). Étape la plus risquée → en dernier, après que le
rollback est atomique et prouvé. Un run
atlas-cephfrom-scratch inchangé valide (ADR 0034).
Alternatives écartées
Section intitulée « Alternatives écartées »- Garder le modèle par phase et compléter la table : c’est ce qu’on faisait — chaque oubli rattrapé rétrospectivement par un run. Ne supprime pas la cause (granularité fausse).
- Dériver le périmètre des rôles Ansible automatiquement (introspection) : séduisant mais fragile (un rôle ne déclare pas toujours ses ressources hors-ns, ni l’ordre) ; un graphe explicite et testé est plus sûr qu’une introspection.
- Tout réécrire d’un coup : casse les chemins nommés et exige une re-preuve massive. La migration incrémentale (graphe en CI d’abord, bascule alias par alias avec preuve banc) tient l’invariant byte/état à chaque étape.