[Robustesse] unwrap()/panics sur les chemins de reconcile (namespace, désérialisation JSON) #16
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?
Constat
Plusieurs
unwrap()/expect()sont présents sur des chemins chauds du reconcile, où une donnée inattendue fait paniquer la task du contrôleur au lieu de remonter une erreur gérée par l'error_policy:operator/src/instance_common.rs:inst.namespace().unwrap()(plusieurs occurrences dansdo_reconcile/do_cleanup),context.as_object_mut().unwrap()répété,serde_json::from_value(...).unwrap()sur les patchs JSON.common/src/k8sgeneric.rs: ~80unwrap().operator/src/jukebox.rs:serde_json::from_value(...).unwrap()pour les patchs.Pour un opérateur, une
Resourcenamespacée a normalement toujours un namespace, et lesjson!(...)littéraux ne devraient pas échouer — donc beaucoup de cesunwrapsont « théoriquement sûrs ». Mais :Errorloggée parerror_policy(qui requeue proprement).unwrap()sur des données issues du cluster ou de désérialisation de status/options (champsOption, JSON arbitraire fourni par l'utilisateur dansspec.options) peuvent réellement paniquer sur entrée malformée.Pistes
inst.namespace().unwrap()par une erreur explicite (Error::Other("instance without namespace")) ou ununwrap_or_default()documenté.k8sgeneric.rs: distinguer lesunwrapsur invariants internes (acceptables, à commenter) de ceux sur données externes (à convertir enResult).catch_unwind/ supervision au niveau du contrôleur pour qu'un panic isolé ne tue pas le watcher.Note
Pas de bug observé en production à ce jour ; ticket de durcissement préventif. Priorité basse, mais utile à traiter en même temps que les correctifs de
instance_common.rs(#12, #15) puisque le fichier sera déjà ouvert.Intéressant, et oui, dans un code Rust propre il ne faut pas de unwrap.
Pour autant, je serais intéressé à une analyse plus en profondeur : un unwrap pour un champ qui est garanti mécaniquement d'être là n'est pas un risque.
Ici, je ne vois que "j'ai vu des unwrap, c'est pas bien". C'est pas faux, mais très incomplet. Typiquement l'exemple donné :
inst.namespace().unwrap()sur une instance d'un objets k8s qui est namespacé, le champ namespace est garanti par kubernetes. Ce n'est une option dans l'API que parce que l'api gère Namespaced et clusterwide de la même manière, et en effet, pour les instance d'objet non-namespacés, le champ est a None. Ici, le retour est invalide.Retour accepté — l'issue initiale comptait les
unwrapsans les lire un par un. Voici l'audit en profondeur, occurrence par occurrence, avec classification.Classe A — invariant garanti mécaniquement : aucun risque
Tous les
unwrapdes chemins de reconcile de l'opérateur (instance_common.rs,jukebox.rs) tombent dans cette classe :inst.namespace().unwrap()(do_reconcile/do_cleanup, ×5)metadata.namespacesur tout objet servi par un watch namespaced. LeOptionn'existe que parce que l'API kube-rs unifie namespaced/cluster-wide — exactement ton point.context.as_object_mut().unwrap()(×13)base_contextest construit par l'opérateur lui-même comme objet JSON ; invariant interne au programme.serde_json::from_value(json!([...])).unwrap()(patchs littéraux)job.is_ok() && job.unwrap().is_some()(×5)is_ok()sur la même valeur, même expression. Non idiomatique (matches!(job, Ok(Some(_)))serait plus propre), mais pas un risque.Conclusion sur l'opérateur : aucun
unwrapréellement dangereux trouvé. L'issue surestimait le risque.Classe B — garanti par convention d'usage, pas par le type
Dans
common/src/k8sgeneric.rs(code exposé à Rhai, donc exécuté par l'agent — une panique y fait échouer le job, retenté ensuite ; elle ne tue pas le contrôleur) :unwrapdes chaînes de conditions sont en réalité gardés : chaqueas_object().unwrap()["x"]est précédé d'unis_object()+ test de présence de clé dans la même expression. Sûrs aujourd'hui. La fragilité est ailleurs : la garde et l'unwrapsont dupliqués et textuellement éloignés — un refactoring qui déplace l'un sans l'autre casse l'invariant silencieusement.self.obj.uid().unwrap(): sûr si l'objet vient de l'API (uid toujours posé par le serveur), paniquerait sur un objet construit localement avant création. La garantie tient à la convention d'appel des scripts, pas au type.Classe C — réellement faillible
Rien trouvé sur les chemins chauds après lecture complète. Les désérialisations de données externes (
spec.options, status) passent par?/map_err, pas parunwrap.Requalification proposée
Pas de chasse aux
unwrap. Au plus, du ménage opportuniste à coût quasi nul quand les fichiers seront ouverts pour #12/#15 :fn ns<T: Resource>(inst: &T) -> Stringportant un commentaire qui documente l'invariant namespaced — un seul endroit à lire au lieu de cinq ;k8sgeneric.rs, remplacer les chaînes garde+unwrap dupliquées par un accesseur (json_path_str(obj, "status.conditions")→Option) — motivation : lisibilité et robustesse au refactoring, pas correction de bug ;matches!(job, Ok(Some(_)))au passage.Si ce niveau de ménage ne vaut pas une issue ouverte, celle-ci peut être fermée — l'audit ci-dessus en garde la trace.
Analyse et rédaction : Claude (assistant IA), publié via le compte de Xavier.
La chasse aux unwrap en soit est pas le problème, de plus l'analyse en profondeur fourni est intéressante.
Au final tous ce qui rentre dans la classe B doit changer. Il n'y a pas d'urgence, mais oui, un code qui met mieux en évidence qu'il est robuste est clairement désirable. Un code plus idiomatique l'est tout autant. Les améliorations opportunistes sont les bienvenues