fix(quadlet): HealthCmd falls back to bash /dev/tcp before exit 0
The generated HealthCmd reported healthy (exit 0) whenever the image shipped neither wget nor curl — btcpay-server sat 'healthy' in podman ps while completely non-functional (live-testing report 2026-07-10). Try a bash /dev/tcp connect to the health URL's host:port before giving up; only images with none of wget/curl/bash still skip Podman health (dropping exit 0 entirely would restart-loop those). Podman's health timeout bounds the connect attempt. Note: the rendered unit text changes, so every http-healthcheck app gets a one-time unit rewrite + restart on first reconcile after this binary lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a5c5db187
commit
b7be296953
@ -538,13 +538,18 @@ fn translate_health_check(hc: &archipelago_container::HealthCheck) -> Option<Hea
|
|||||||
format!("{url}{path}")
|
format!("{url}{path}")
|
||||||
};
|
};
|
||||||
let helper_timeout = health_timeout_seconds(&hc.timeout);
|
let helper_timeout = health_timeout_seconds(&hc.timeout);
|
||||||
|
let (tcp_host, tcp_port) = health_url_host_port(&final_url);
|
||||||
// Images vary wildly: SearXNG ships wget but no curl, while some
|
// Images vary wildly: SearXNG ships wget but no curl, while some
|
||||||
// Node images ship neither. Use whichever probe helper exists and
|
// images (btcpay's dotnet base) ship neither but do have bash —
|
||||||
// skip Podman health if the image has none; host-side lifecycle
|
// fall through to a bash /dev/tcp connect probe there so the
|
||||||
// probes still verify reachability.
|
// container can't report healthy while its port is dead
|
||||||
|
// (btcpay-server, 2026-07-10). Podman's own health timeout bounds
|
||||||
|
// the connect attempt. Only images with none of the three skip
|
||||||
|
// Podman health entirely; host-side lifecycle probes still verify
|
||||||
|
// reachability for those.
|
||||||
format!(
|
format!(
|
||||||
"if command -v wget >/dev/null 2>&1; then wget -q -T {1} -O /dev/null {0}; elif command -v curl >/dev/null 2>&1; then curl -fsS -m {1} {0}; else exit 0; fi",
|
"if command -v wget >/dev/null 2>&1; then wget -q -T {1} -O /dev/null {0}; elif command -v curl >/dev/null 2>&1; then curl -fsS -m {1} {0}; elif command -v bash >/dev/null 2>&1; then bash -c 'exec 3<>/dev/tcp/{2}/{3}'; else exit 0; fi",
|
||||||
final_url, helper_timeout
|
final_url, helper_timeout, tcp_host, tcp_port
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"cmd" => hc.endpoint.as_deref()?.to_string(),
|
"cmd" => hc.endpoint.as_deref()?.to_string(),
|
||||||
@ -558,6 +563,24 @@ fn translate_health_check(hc: &archipelago_container::HealthCheck) -> Option<Hea
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host and port of an http(s) health URL, for the bash /dev/tcp fallback
|
||||||
|
/// probe. Defaults to port 80/443 by scheme when the authority carries none.
|
||||||
|
fn health_url_host_port(url: &str) -> (String, u16) {
|
||||||
|
let (default_port, rest) = if let Some(rest) = url.strip_prefix("https://") {
|
||||||
|
(443, rest)
|
||||||
|
} else {
|
||||||
|
(80, url.strip_prefix("http://").unwrap_or(url))
|
||||||
|
};
|
||||||
|
let authority = rest.split('/').next().unwrap_or(rest);
|
||||||
|
match authority.rsplit_once(':') {
|
||||||
|
Some((host, port)) => match port.parse::<u16>() {
|
||||||
|
Ok(port) => (host.to_string(), port),
|
||||||
|
Err(_) => (authority.to_string(), default_port),
|
||||||
|
},
|
||||||
|
None => (authority.to_string(), default_port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn health_timeout_seconds(raw: &str) -> u64 {
|
fn health_timeout_seconds(raw: &str) -> u64 {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@ -1459,7 +1482,22 @@ app:
|
|||||||
let h = translate_health_check(&http).expect("http must translate");
|
let h = translate_health_check(&http).expect("http must translate");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
h.cmd,
|
h.cmd,
|
||||||
"if command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null http://localhost:8080/health; elif command -v curl >/dev/null 2>&1; then curl -fsS -m 3 http://localhost:8080/health; else exit 0; fi"
|
"if command -v wget >/dev/null 2>&1; then wget -q -T 3 -O /dev/null http://localhost:8080/health; elif command -v curl >/dev/null 2>&1; then curl -fsS -m 3 http://localhost:8080/health; elif command -v bash >/dev/null 2>&1; then bash -c 'exec 3<>/dev/tcp/localhost/8080'; else exit 0; fi"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The /dev/tcp fallback must target the URL's host:port, with
|
||||||
|
// scheme-default ports when the authority carries none.
|
||||||
|
assert_eq!(
|
||||||
|
health_url_host_port("http://localhost:8080/health"),
|
||||||
|
("localhost".to_string(), 8080)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
health_url_host_port("http://127.0.0.1/"),
|
||||||
|
("127.0.0.1".to_string(), 80)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
health_url_host_port("https://localhost/status"),
|
||||||
|
("localhost".to_string(), 443)
|
||||||
);
|
);
|
||||||
|
|
||||||
let cmdck = HealthCheck {
|
let cmdck = HealthCheck {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user