[Bug] Désinstallation impossible quand le type de package a changé (tenant → service) : finalizer bloqué #12
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Résumé
La suppression d'une instance reste bloquée indéfiniment (finalizer jamais retiré,
deletionTimestampfigé) lorsque le package a été republié avec unusagedifférent de celui utilisé lors de l'installation. Cas réel : un package historiquement installé commetenantpuis republié commeservice.L'opérateur boucle en erreur :
Reproduction réelle (cluster)
Instance
gretel(think/ollama), namespaceepikaf-nan-ia:Le package dans la jukebox
home-alphaest désormais de typeservice:L'instance est une
TenantInstance→T::package_type() == VynilPackageType::Tenant, mais le seul packagethink/ollamadisponible estusage == Service. Le lookup échoue.Analyse de la cause racine
Dans
operator/src/instance_common.rs,do_cleanup()cherche le package avec un filtre qui impose le type et la version mini :Deux problèmes combinés :
Le lookup de cleanup est trop strict. Pour une désinstallation, le filtre par
usageetmin_versionn'a pas de raison d'être : la suppression effective des objets est pilotée parinstance.status(befores/vitals/scalables/others/posts, tfstate/rhaistate), pas par leusagedu package. Le package n'est nécessaire que pour fournir l'image/registry/tag à dépaqueter et la présence des répertoires (others/,vitals/…). N'importe quelle révision du mêmename+categoryfait l'affaire.L'échec n'a pas de fallback. Quand
have_child()est vrai et que le package est introuvable, on renvoie une erreur dure au lieu de tenter une suppression sur la seule base du status. Or c'est précisément le cas où il faut à minima nettoyer ce que décrit le status (comme tu le suggères : « fallback sur le status »).C'est la cause du blocage :
have_child()est vrai (status non vide), le package « Tenant » n'existe plus → erreur dure → finalizer jamais satisfait → instance zombie.Correctif proposé
Étape 1 — Relâcher le lookup de cleanup avec une fonction testable
Extraire le lookup dans une fonction pure et lui donner une cascade de fallback. Pour le cleanup, on accepte une correspondance dégradée (n'importe quel
usage, sans contrainte de version mini), car seul le code de désinstallation du package importe.Dans
do_cleanup():Étape 2 — Ne plus bloquer indéfiniment quand le package est totalement absent
Quand aucun package ne correspond (même en dégradé) mais que
have_child()est vrai, deux choix possibles :warnexplicite et déclencher un job de suppression « best-effort » piloté uniquement par le status, en réutilisant l'image/tag enregistrés (cf.status.tag). À défaut d'image/registry dans le status aujourd'hui, voir l'étape 3.E_PACKAGE_GONE) pour que l'instance ne disparaisse pas silencieusement, et documenter la procédure de retrait manuel du finalizer.Reco : implémenter l'étape 1 (couvre le cas tenant→service signalé, le package existe toujours sous un autre
usage), et garder l'étape 2 « garde-fou » pour le cas où le package a réellement disparu de la jukebox.Étape 3 (optionnel, robustesse long terme)
Persister
registry+imagedans lestatusà l'installation (à côté detag). Le cleanup devient alors totalement indépendant de la jukebox : on peut dépaqueter et désinstaller même si le package a disparu du catalogue.Tests à réaliser
Tests unitaires sur
find_cleanup_package(aucun cluster requis) :Test d'intégration (manuel / e2e) :
tenant, le republierservicedans la jukebox, rescanner, puiskubectl delete vti→ la désinstallation doit aboutir et le finalizer se retirer.Déblocage immédiat de
gretelEn attendant le fix, retrait manuel du finalizer (⚠️ laisse les objets enfants orphelins à nettoyer à la main) :
Périmètre
Le même schéma s'applique à
SystemInstanceetServiceInstance(le codedo_cleanupest générique viaInstanceKind) : le fix les couvre toutes les trois.En fait, la proposition n'est pas idiote, mais manque complètement la big picture : le problème est pas sur la situation, mais sur l'enchaînement de décisions que les automates ont pris en amont qui a placé Xavier dans cette situation impossible.
Ici on a un enchaînement de bug qui nous mène dans une situation impossible. Il faut régler les problème en amont pour ne plus atterrir ici.
Il faut bien comprendre que le status des instances ne permet pas, seul, de faire un delete complet et propre : les packages disposent de hook de delete, dans la box kydah, beaucoup de package ont des delete_vitals_post hooks pour détruire ce qui a été créé indirectement et qui n'as donc pas les marqueurs d'owner correct. Faire un delete sans ces scripts c'est s'assurer de laisser des déchets derrière. Ca ne peux pas être un comportement automatique. Au mieux, c'est un comportement qu'on peut autoriser avec un gatekeeper basé sur une annotation sur l'objet.
Problèmes amont :
Régler ces 2 points en amont règle au fond 99% du problème.
Pour le 1% restant, ma proposition serait plutôt de supporter une annotation qui ferait un delete sans image
Proposition v2 — recentrée sur l'amont
Retour intégré : le delete « status seul » automatique est abandonné. L'analyse de packages en production le confirme — les hooks
delete_vitals_pre/posty sont fréquents et indispensables (repasser une base répliquée en mono-réplica avant destruction, purger les PVC créés par un opérateur tiers sans marqueurs d'owner corrects…). Sans l'image du paquet, ces nettoyages n'existent pas : un delete sans hooks ne peut être qu'une action explicitement demandée, jamais un fallback.La proposition v1 (relâcher le lookup de
do_cleanup) est également abandonnée : une fois l'amont corrigé, le lookup strict retrouve la bonne révision (celle de l'ancien type, donc avec les bons hooks) — c'est mieux que d'utiliser l'image d'un autreusage.A. Purge des images « type-aware » (amont n°1)
Le script de purge garde aujourd'hui les têtes de maturité et le chemin de migration (
minimum_previous_version), mais décide à partir des seuls noms de tags. Il est aveugle autype: il a détruit la dernière révisiontenantdethink/ollama.Règle à ajouter en plus du système existant : conserver la révision la plus récente de chaque
typeprésent dans l'historique du dépôt. Coût borné (≤ 3 types) ; cela impose de lirefr.solidite.vynil.metadatadans les manifests, ce que le scan fait déjà.Proposition structurelle qui en découle : rapatrier la logique de purge dans vynil (ex.
agent box cleanou un script embarquéboxes/clean.rhaipartageant les helpers semver/waypoints/types avecboxes/scan.rhai). Aujourd'hui chaque box embarque sa copie declean_harbor.rhaiqui dérive indépendamment du scan — c'est précisément cette divergence scan/purge qui a produit la situation. Purge et scan appliquent les mêmes règles : ils doivent partager le même code. Les boxes ne garderaient que la config (registre, credentials, politique SBOM/DT).B. Scan « type-aware » (amont n°2)
Dans
agent/scripts/boxes/scan.rhai,scan_image_with_maturity()(et son pendantcompute_waypoints_from_packages()pour http/s3) arrête la descente des tags au premier jalon sansminimum_previous_version. Même si la purge avait gardé la révisiontenant, elle n'apparaîtrait donc jamais dansbox.status.packages.Changement : mémoriser le
typedes versions retenues et poursuivre la descente tant qu'il reste des tags et qu'on n'a pas vérifié s'il existe une révision plus ancienne d'un autretype; n'ajouter au catalogue que la plus récente de chaque type supplémentaire. Coûts :Avec A + B,
do_cleanup()retrouve un paquetusage == Tenantdans le catalogue et le filtre strict existant fonctionne tel quel — les 99 %.C. Annotation gatekeeper pour le 1 % restant
Pour le cas où le paquet a réellement disparu (registre perdu, jukebox supprimée) : une annotation opt-in sur l'instance, par ex.
vynil.solidite.fr/delete-without-package: "true".E_PACKAGE_GONE) + requeue temporisé, et procédure documentée (fait dans la doc, page Dépannage).unpack, piloté uniquement parinstance.status(les scriptsdelete_<phase>itèrent déjà sur le status). Hooks absents ⇒ best-effort assumé,warndans les logs et condition dédiée. Le finalizer se retire à la fin.D. Garde-fou à la publication (optionnel, coût quasi nul)
Un changement de
typeest une frontière de migration. Au build/push (ou en lint CI), comparer letypeavec la dernière version publiée : s'il diffère, exiger/recommander unminimum_previous_versionsur la nouvelle version. L'ancienne révision devient alors un waypoint que la purge et le scan actuels savent déjà conserver — défense en profondeur pour A et B.Tests
compute_waypoints_from_packages(fonction pure) : sans changement de type (résultat identique à aujourd'hui), tenant→service (l'ancienne têtetenantest exposée), waypoints + changement de type combinés.E_PACKAGE_GONE, pas d'erreur dure ; avec annotation → job sans unpack, suppression des objets du status, finalizer retiré.minimum_previous_version→ warning (puis erreur à terme).Ordre suggéré : B (corrige la vue, débloquera les cas où l'image existe encore) → A (empêche la perte d'images) → C (sortie de secours propre) → D.
Analyse et rédaction : Claude (assistant IA), publié via le compte de Xavier.
A, B et C. Ok
D. Par contre, ca ne me convient pas : minimum_previous_version sert a plein d'endroit. Cette stratégie est, au final, dangereuse