From 77c5f5641a2f61f60ccf7c7d9563bdd34a7ca23f Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 8 Jul 2026 17:08:41 -0400 Subject: [PATCH] fix(orchestrator): stop quadlet entrypoint split from reading as permanent command drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit container_command_drifted compared the manifest's entrypoint and custom_args against Config.Entrypoint and Config.Cmd separately, but quadlet's Entrypoint= takes a single value, so a manifest entrypoint of [sh, -lc] is written as Entrypoint=sh + Exec=-lc ... and podman reports entry=[sh], cmd=[-lc, script]. Same argv, different split — every quadlet-created app with a multi-element entrypoint read as drifted forever. On .228 this recreated electrumx on every reconcile pass (114 times in 6h); bitcoin-knots and fedimint-gateway showed the same false drift and were only spared by the restart-sensitive guard. Compare the concatenated argv (entrypoint ++ args vs Entrypoint ++ Cmd) instead — that is what actually runs. When the manifest declares no custom_args, only the entrypoint prefix must match, since the trailing Cmd may be the image's baked-in CMD. Co-Authored-By: Claude Fable 5 --- .../src/container/prod_orchestrator.rs | 121 ++++++++++++++++-- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index f72a3d8a..fa78e912 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -3106,17 +3106,12 @@ impl ProdContainerOrchestrator { .flatten() .unwrap_or_default(); - if let Some(expected_entry) = &manifest.app.container.entrypoint { - if current_entry != *expected_entry { - return true; - } - } - if !manifest.app.container.custom_args.is_empty() - && current_cmd != manifest.app.container.custom_args - { - return true; - } - false + command_argv_drifted( + manifest.app.container.entrypoint.as_deref(), + &manifest.app.container.custom_args, + ¤t_entry, + ¤t_cmd, + ) } /// True when a *running* container publishes a manifest container-port to a @@ -3872,6 +3867,36 @@ impl ContainerOrchestrator for ProdContainerOrchestrator { } } +/// Compare a manifest's command against a live container's, over the +/// concatenated argv (entrypoint ++ args) rather than each half separately. +/// The halves are not stable across create paths: quadlet's `Entrypoint=` +/// takes a single value, so a manifest entrypoint of `sh -lc` is written as +/// `Entrypoint=sh` + `Exec=-lc ...`, and podman then reports +/// `Config.Entrypoint=["sh"]`, `Config.Cmd=["-lc", ...]` — a per-half +/// comparison reads that as permanent drift and recreates the container on +/// every reconcile pass (electrumx on .228, 2026-07-08: 114 recreates in 6h). +/// The concatenation is what actually runs, so it is the thing to compare. +fn command_argv_drifted( + expected_entry: Option<&[String]>, + expected_args: &[String], + current_entry: &[String], + current_cmd: &[String], +) -> bool { + let Some(expected_entry) = expected_entry else { + // No entrypoint override: the manifest args map to Cmd directly. + return !expected_args.is_empty() && current_cmd != expected_args; + }; + let expected: Vec<&String> = expected_entry.iter().chain(expected_args).collect(); + let actual: Vec<&String> = current_entry.iter().chain(current_cmd).collect(); + if expected_args.is_empty() { + // With no manifest args, the container's trailing Cmd may be the + // image's baked-in CMD — only the entrypoint prefix must match. + actual.len() < expected.len() || actual[..expected.len()] != expected[..] + } else { + actual != expected + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -3891,6 +3916,80 @@ mod tests { } } + fn sv(items: &[&str]) -> Vec { + items.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn command_drift_tolerates_quadlet_entrypoint_split() { + // Quadlet writes Entrypoint=sh + Exec=-lc "