[Bug] Désinstallation impossible quand le type de package a changé (tenant → service) : finalizer bloqué #12

Open
opened 2026-06-12 09:28:21 +02:00 by xmortelette · 3 comments

Résumé

La suppression d'une instance reste bloquée indéfiniment (finalizer jamais retiré, deletionTimestamp figé) lorsque le package a été republié avec un usage différent de celui utilisé lors de l'installation. Cas réel : un package historiquement installé comme tenant puis republié comme service.

L'opérateur boucle en erreur :

FinalizerError(CleanupFailed(Other("This install have child but the package cannot be found")))

Reproduction réelle (cluster)

Instance gretel (think/ollama), namespace epikaf-nan-ia :

$ kubectl -n epikaf-nan-ia get vti gretel -o yaml
  deletionTimestamp: "2026-06-11T12:46:41Z"   # supprimée depuis > 18h, toujours présente
  finalizers:
  - tenantinstances.vynil.solidite.fr
status:
  tag: 0.1.8-beta.50
  vitals:    [PVC gretel-ollama-models]       # status.have_child() == true
  scalables: [Deployment gretel-ollama]
  others:    [ConfigMap, Middlewares, Job, Services, Ingress]
  conditions:
  - type: AgentStarted, status: "False", message: "Package think/ollama is missing"

Le package dans la jukebox home-alpha est désormais de type service :

$ kubectl get jukebox home-alpha -o json | jq '.status.packages[] | select(.metadata.name=="ollama") | .metadata'
{ "name": "ollama", "category": "think", "type": "service", "app_version": "0.24.0" }

L'instance est une TenantInstanceT::package_type() == VynilPackageType::Tenant, mais le seul package think/ollama disponible est usage == 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 :

let pck = packages[jukebox]
    .packages
    .iter()
    .find(|p| {
        p.metadata.name == inst.spec_package()
            && p.metadata.category == inst.spec_category()
            && p.metadata.usage == T::package_type()       // ← échoue si tenant→service
            && p.is_min_version_ok(current_version.clone()) // ← échoue aussi si downgrade
            && p.is_vynil_version_ok()
    })
    .cloned();

let pck = match pck {
    Some(p) => p,
    None => {
        if inst.have_child() {
            return Err(Error::Other(String::from(
                "This install have child but the package cannot be found",
            )));   // ← erreur dure, le finalizer ne se retire jamais
        }
        return Ok(Action::await_change());
    }
};

Deux problèmes combinés :

  1. Le lookup de cleanup est trop strict. Pour une désinstallation, le filtre par usage et min_version n'a pas de raison d'être : la suppression effective des objets est pilotée par instance.status (befores/vitals/scalables/others/posts, tfstate/rhaistate), pas par le usage du 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ême name+category fait l'affaire.

  2. 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.

/// Sélectionne le package à utiliser pour une désinstallation.
///
/// Contrairement à l'installation, le cleanup s'appuie sur `instance.status`
/// pour savoir quoi détruire ; le `usage` et la version mini du package n'ont
/// donc pas à être vérifiés strictement. On tente d'abord la correspondance
/// exacte (type + version), puis on retombe sur n'importe quelle révision du
/// même name+category — ce qui couvre le cas d'un package republié avec un
/// `usage` différent (ex. tenant → service).
fn find_cleanup_package<'a>(
    packages: &'a [VynilPackage],
    name: &str,
    category: &str,
    package_type: VynilPackageType,
    current_version: &str,
) -> Option<&'a VynilPackage> {
    packages
        .iter()
        .find(|p| {
            p.metadata.name == name
                && p.metadata.category == category
                && p.metadata.usage == package_type
                && p.is_min_version_ok(current_version.to_string())
                && p.is_vynil_version_ok()
        })
        .or_else(|| {
            packages
                .iter()
                .find(|p| p.metadata.name == name && p.metadata.category == category)
        })
}

Dans do_cleanup() :

let pck = find_cleanup_package(
    &packages[jukebox].packages,
    inst.spec_package(),
    inst.spec_category(),
    T::package_type(),
    &current_version,
);

É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 :

  • Préféré : journaliser un warn explicite 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.
  • Garde-fou minimal : transformer l'erreur dure en requeue temporisé + une condition de status lisible (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 + image dans le status à l'installation (à côté de tag). 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]
fn cleanup_pkg_exact_match() {
    let pkgs = vec![make_package("ollama", "think", "0.1.8", VynilPackageType::Tenant)];
    let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.8");
    assert!(r.is_some());
}

#[test]
fn cleanup_pkg_usage_changed_falls_back() {
    // Package republié en service, instance installée en tenant
    let pkgs = vec![make_package("ollama", "think", "0.1.10", VynilPackageType::Service)];
    let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.8");
    assert!(r.is_some(), "doit retomber sur la révision service");
}

#[test]
fn cleanup_pkg_version_downgraded_falls_back() {
    // min_version du package > version installée
    let mut p = make_package("ollama", "think", "0.2.0", VynilPackageType::Tenant);
    // (configurer get_min_version() > current si applicable)
    let pkgs = vec![p];
    let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.0");
    assert!(r.is_some());
}

#[test]
fn cleanup_pkg_absent_returns_none() {
    let pkgs = vec![make_package("autre", "think", "1.0.0", VynilPackageType::Tenant)];
    let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.0");
    assert!(r.is_none());
}

Test d'intégration (manuel / e2e) :

  • Installer un package tenant, le republier service dans la jukebox, rescanner, puis kubectl delete vti → la désinstallation doit aboutir et le finalizer se retirer.

Déblocage immédiat de gretel

En attendant le fix, retrait manuel du finalizer (⚠️ laisse les objets enfants orphelins à nettoyer à la main) :

kubectl -n epikaf-nan-ia patch vti gretel --type=json \
  -p '[{"op":"remove","path":"/metadata/finalizers/0"}]'

Périmètre

Le même schéma s'applique à SystemInstance et ServiceInstance (le code do_cleanup est générique via InstanceKind) : le fix les couvre toutes les trois.

## Résumé La suppression d'une instance reste bloquée indéfiniment (finalizer jamais retiré, `deletionTimestamp` figé) lorsque le package a été **republié avec un `usage` différent** de celui utilisé lors de l'installation. Cas réel : un package historiquement installé comme `tenant` puis republié comme `service`. L'opérateur boucle en erreur : ``` FinalizerError(CleanupFailed(Other("This install have child but the package cannot be found"))) ``` ## Reproduction réelle (cluster) Instance `gretel` (`think/ollama`), namespace `epikaf-nan-ia` : ``` $ kubectl -n epikaf-nan-ia get vti gretel -o yaml deletionTimestamp: "2026-06-11T12:46:41Z" # supprimée depuis > 18h, toujours présente finalizers: - tenantinstances.vynil.solidite.fr status: tag: 0.1.8-beta.50 vitals: [PVC gretel-ollama-models] # status.have_child() == true scalables: [Deployment gretel-ollama] others: [ConfigMap, Middlewares, Job, Services, Ingress] conditions: - type: AgentStarted, status: "False", message: "Package think/ollama is missing" ``` Le package dans la jukebox `home-alpha` est désormais de type `service` : ``` $ kubectl get jukebox home-alpha -o json | jq '.status.packages[] | select(.metadata.name=="ollama") | .metadata' { "name": "ollama", "category": "think", "type": "service", "app_version": "0.24.0" } ``` L'instance est une `TenantInstance` → `T::package_type() == VynilPackageType::Tenant`, mais le seul package `think/ollama` disponible est `usage == Service`. Le lookup échoue. ## Analyse de la cause racine Dans [`operator/src/instance_common.rs`](operator/src/instance_common.rs), `do_cleanup()` cherche le package avec un filtre qui impose le type **et** la version mini : ```rust let pck = packages[jukebox] .packages .iter() .find(|p| { p.metadata.name == inst.spec_package() && p.metadata.category == inst.spec_category() && p.metadata.usage == T::package_type() // ← échoue si tenant→service && p.is_min_version_ok(current_version.clone()) // ← échoue aussi si downgrade && p.is_vynil_version_ok() }) .cloned(); let pck = match pck { Some(p) => p, None => { if inst.have_child() { return Err(Error::Other(String::from( "This install have child but the package cannot be found", ))); // ← erreur dure, le finalizer ne se retire jamais } return Ok(Action::await_change()); } }; ``` Deux problèmes combinés : 1. **Le lookup de cleanup est trop strict.** Pour une *désinstallation*, le filtre par `usage` et `min_version` n'a pas de raison d'être : la suppression effective des objets est pilotée par `instance.status` (`befores`/`vitals`/`scalables`/`others`/`posts`, tfstate/rhaistate), pas par le `usage` du 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ême `name`+`category` fait l'affaire. 2. **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. ```rust /// Sélectionne le package à utiliser pour une désinstallation. /// /// Contrairement à l'installation, le cleanup s'appuie sur `instance.status` /// pour savoir quoi détruire ; le `usage` et la version mini du package n'ont /// donc pas à être vérifiés strictement. On tente d'abord la correspondance /// exacte (type + version), puis on retombe sur n'importe quelle révision du /// même name+category — ce qui couvre le cas d'un package republié avec un /// `usage` différent (ex. tenant → service). fn find_cleanup_package<'a>( packages: &'a [VynilPackage], name: &str, category: &str, package_type: VynilPackageType, current_version: &str, ) -> Option<&'a VynilPackage> { packages .iter() .find(|p| { p.metadata.name == name && p.metadata.category == category && p.metadata.usage == package_type && p.is_min_version_ok(current_version.to_string()) && p.is_vynil_version_ok() }) .or_else(|| { packages .iter() .find(|p| p.metadata.name == name && p.metadata.category == category) }) } ``` Dans `do_cleanup()` : ```rust let pck = find_cleanup_package( &packages[jukebox].packages, inst.spec_package(), inst.spec_category(), T::package_type(), &current_version, ); ``` ### É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 : - **Préféré** : journaliser un `warn` explicite 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. - **Garde-fou minimal** : transformer l'erreur dure en requeue temporisé + une condition de status lisible (`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` + `image` dans le `status` à l'installation (à côté de `tag`). 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) : ```rust #[test] fn cleanup_pkg_exact_match() { let pkgs = vec![make_package("ollama", "think", "0.1.8", VynilPackageType::Tenant)]; let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.8"); assert!(r.is_some()); } #[test] fn cleanup_pkg_usage_changed_falls_back() { // Package republié en service, instance installée en tenant let pkgs = vec![make_package("ollama", "think", "0.1.10", VynilPackageType::Service)]; let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.8"); assert!(r.is_some(), "doit retomber sur la révision service"); } #[test] fn cleanup_pkg_version_downgraded_falls_back() { // min_version du package > version installée let mut p = make_package("ollama", "think", "0.2.0", VynilPackageType::Tenant); // (configurer get_min_version() > current si applicable) let pkgs = vec![p]; let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.0"); assert!(r.is_some()); } #[test] fn cleanup_pkg_absent_returns_none() { let pkgs = vec![make_package("autre", "think", "1.0.0", VynilPackageType::Tenant)]; let r = find_cleanup_package(&pkgs, "ollama", "think", VynilPackageType::Tenant, "0.1.0"); assert!(r.is_none()); } ``` Test d'intégration (manuel / e2e) : - Installer un package `tenant`, le republier `service` dans la jukebox, rescanner, puis `kubectl delete vti` → la désinstallation doit aboutir et le finalizer se retirer. ## Déblocage immédiat de `gretel` En attendant le fix, retrait manuel du finalizer (⚠️ laisse les objets enfants orphelins à nettoyer à la main) : ``` kubectl -n epikaf-nan-ia patch vti gretel --type=json \ -p '[{"op":"remove","path":"/metadata/finalizers/0"}]' ``` ## Périmètre Le même schéma s'applique à `SystemInstance` et `ServiceInstance` (le code `do_cleanup` est générique via `InstanceKind`) : le fix les couvre toutes les trois.
xmortelette added the Kind/Bug
Reviewed
Confirmed
4
Priority
High
2
labels 2026-06-12 09:28:21 +02:00
Owner

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 :

  • Purge des images : même si le script de purge des images maintient un chemin pour les migrations par étapes (minimum_previous_version), il ne tient pas du tout compte du type de package. Donc il a détruit une image qu'on voulait garder. Il faut gérer ce cas en plus du système existant.
  • Scan des images : même problème au fond : le scan n'est pas "aware" du type de package quand il scan pour donner la vue réduite au controlleur, donc même si la purge avait gardé l'image, vynil n'en aurait pas eu connaissance.

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

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 : - Purge des images : même si le script de purge des images maintient un chemin pour les migrations par étapes (minimum_previous_version), il ne tient pas du tout compte du type de package. Donc il a détruit une image qu'on voulait garder. Il faut gérer ce cas en plus du système existant. - Scan des images : même problème au fond : le scan n'est pas "aware" du type de package quand il scan pour donner la vue réduite au controlleur, donc même si la purge avait gardé l'image, vynil n'en aurait pas eu connaissance. 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
shuss removed the
Reviewed
Confirmed
4
label 2026-06-12 10:28:09 +02:00
shuss added the
Reviewed
Confirmed
4
label 2026-06-12 10:29:24 +02:00
shuss added the
Reviewed
analysed
1
label 2026-06-12 11:07:35 +02:00
Author

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/post y 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 autre usage.

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 au type : il a détruit la dernière révision tenant de think/ollama.

Règle à ajouter en plus du système existant : conserver la révision la plus récente de chaque type présent dans l'historique du dépôt. Coût borné (≤ 3 types) ; cela impose de lire fr.solidite.vynil.metadata dans 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 clean ou un script embarqué boxes/clean.rhai partageant les helpers semver/waypoints/types avec boxes/scan.rhai). Aujourd'hui chaque box embarque sa copie de clean_harbor.rhai qui 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 pendant compute_waypoints_from_packages() pour http/s3) arrête la descente des tags au premier jalon sans minimum_previous_version. Même si la purge avait gardé la révision tenant, elle n'apparaîtrait donc jamais dans box.status.packages.

Changement : mémoriser le type des 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 autre type ; n'ajouter au catalogue que la plus récente de chaque type supplémentaire. Coûts :

  • sources http/s3 : métadonnées déjà locales, surcoût nul ;
  • sources OCI : lectures de manifests supplémentaires, mais bornées par la purge (A) qui maintient les dépôts petits ; un plafond de profondeur configurable peut rassurer pour les registres non purgés.

Avec A + B, do_cleanup() retrouve un paquet usage == Tenant dans 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".

  • Sans annotation : plus d'erreur dure en boucle — condition de status explicite (E_PACKAGE_GONE) + requeue temporisé, et procédure documentée (fait dans la doc, page Dépannage).
  • Avec annotation : job de delete sans initContainer unpack, piloté uniquement par instance.status (les scripts delete_<phase> itèrent déjà sur le status). Hooks absents ⇒ best-effort assumé, warn dans 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 type est une frontière de migration. Au build/push (ou en lint CI), comparer le type avec la dernière version publiée : s'il diffère, exiger/recommander un minimum_previous_version sur 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

  • A : fixture de tags multi-types → la purge conserve têtes de maturité + waypoints + dernière révision de chaque type ; supprime le reste.
  • B : tests unitaires sur compute_waypoints_from_packages (fonction pure) : sans changement de type (résultat identique à aujourd'hui), tenant→service (l'ancienne tête tenant est exposée), waypoints + changement de type combinés.
  • C : instance avec status non vide, catalogue vide : sans annotation → condition E_PACKAGE_GONE, pas d'erreur dure ; avec annotation → job sans unpack, suppression des objets du status, finalizer retiré.
  • D : lint/publish — type changé sans 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.

## 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/post` y 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 autre `usage`. ### 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 au `type` : il a détruit la dernière révision `tenant` de `think/ollama`. Règle à ajouter **en plus** du système existant : conserver la révision la plus récente de **chaque `type`** présent dans l'historique du dépôt. Coût borné (≤ 3 types) ; cela impose de lire `fr.solidite.vynil.metadata` dans 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 clean` ou un script embarqué `boxes/clean.rhai` partageant les helpers semver/waypoints/types avec `boxes/scan.rhai`). Aujourd'hui chaque box embarque sa copie de `clean_harbor.rhai` qui 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 pendant `compute_waypoints_from_packages()` pour http/s3) arrête la descente des tags au premier jalon sans `minimum_previous_version`. Même si la purge avait gardé la révision `tenant`, elle n'apparaîtrait donc jamais dans `box.status.packages`. Changement : mémoriser le `type` des 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 autre `type` ; n'ajouter au catalogue que la plus récente de chaque type supplémentaire. Coûts : - sources http/s3 : métadonnées déjà locales, surcoût nul ; - sources OCI : lectures de manifests supplémentaires, mais bornées par la purge (A) qui maintient les dépôts petits ; un plafond de profondeur configurable peut rassurer pour les registres non purgés. Avec A + B, `do_cleanup()` retrouve un paquet `usage == Tenant` dans 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"`. - Sans annotation : plus d'erreur dure en boucle — condition de status explicite (`E_PACKAGE_GONE`) + requeue temporisé, et procédure documentée (fait dans la doc, page Dépannage). - Avec annotation : job de delete **sans** initContainer `unpack`, piloté uniquement par `instance.status` (les scripts `delete_<phase>` itèrent déjà sur le status). Hooks absents ⇒ best-effort assumé, `warn` dans 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 `type` est une frontière de migration. Au build/push (ou en lint CI), comparer le `type` avec la dernière version publiée : s'il diffère, exiger/recommander un `minimum_previous_version` sur 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 - A : fixture de tags multi-types → la purge conserve têtes de maturité + waypoints + dernière révision de chaque type ; supprime le reste. - B : tests unitaires sur `compute_waypoints_from_packages` (fonction pure) : sans changement de type (résultat identique à aujourd'hui), tenant→service (l'ancienne tête `tenant` est exposée), waypoints + changement de type combinés. - C : instance avec status non vide, catalogue vide : sans annotation → condition `E_PACKAGE_GONE`, pas d'erreur dure ; avec annotation → job sans unpack, suppression des objets du status, finalizer retiré. - D : lint/publish — type changé sans `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.*
Owner

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

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
shuss removed the
Reviewed
Confirmed
4
label 2026-06-12 17:37:45 +02:00
Sign in to join this conversation.