fix(orchestrator): stop quadlet entrypoint split from reading as permanent command drift
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 <noreply@anthropic.com>
This commit is contained in:
parent
fcd5a065dc
commit
77c5f5641a
@ -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<String> {
|
||||
items.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_drift_tolerates_quadlet_entrypoint_split() {
|
||||
// Quadlet writes Entrypoint=sh + Exec=-lc "<script>", so podman
|
||||
// reports entry=["sh"], cmd=["-lc", script] for a manifest that
|
||||
// declared entrypoint ["sh", "-lc"] + custom_args [script]. Same
|
||||
// argv, different split — must NOT read as drift (the electrumx
|
||||
// recreate loop).
|
||||
let script = "export DAEMON_URL=...; exec electrumx_server";
|
||||
assert!(!command_argv_drifted(
|
||||
Some(&sv(&["sh", "-lc"])),
|
||||
&sv(&[script]),
|
||||
&sv(&["sh"]),
|
||||
&sv(&["-lc", script]),
|
||||
));
|
||||
// Legacy podman-create layout keeps the halves as declared.
|
||||
assert!(!command_argv_drifted(
|
||||
Some(&sv(&["sh", "-lc"])),
|
||||
&sv(&[script]),
|
||||
&sv(&["sh", "-lc"]),
|
||||
&sv(&[script]),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_drift_detected_when_argv_actually_differs() {
|
||||
assert!(command_argv_drifted(
|
||||
Some(&sv(&["sh", "-lc"])),
|
||||
&sv(&["new script"]),
|
||||
&sv(&["sh"]),
|
||||
&sv(&["-lc", "old script"]),
|
||||
));
|
||||
assert!(command_argv_drifted(
|
||||
Some(&sv(&["bash", "-c"])),
|
||||
&sv(&["run"]),
|
||||
&sv(&["sh"]),
|
||||
&sv(&["-lc", "run"]),
|
||||
));
|
||||
// No entrypoint override: args compare against Cmd directly.
|
||||
assert!(command_argv_drifted(
|
||||
None,
|
||||
&sv(&["-server=1"]),
|
||||
&sv(&["bitcoind"]),
|
||||
&sv(&["-server=0"]),
|
||||
));
|
||||
assert!(!command_argv_drifted(
|
||||
None,
|
||||
&sv(&["-server=1"]),
|
||||
&sv(&["bitcoind"]),
|
||||
&sv(&["-server=1"]),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_drift_entrypoint_only_ignores_image_cmd() {
|
||||
// Entrypoint override with no manifest args: the container's Cmd is
|
||||
// the image's baked-in CMD — only the entrypoint prefix matters.
|
||||
assert!(!command_argv_drifted(
|
||||
Some(&sv(&["/usr/local/bin/bitcoind"])),
|
||||
&[],
|
||||
&sv(&["/usr/local/bin/bitcoind"]),
|
||||
&sv(&["-image-default-arg"]),
|
||||
));
|
||||
assert!(command_argv_drifted(
|
||||
Some(&sv(&["/usr/local/bin/bitcoind"])),
|
||||
&[],
|
||||
&sv(&["/other/binary"]),
|
||||
&sv(&[]),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_drift_detected_when_host_port_differs() {
|
||||
// The .116 case: container publishes container-port 8080 on host 8080,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user