From b7be296953ea3aa5e8b65af9031e07e496e04a54 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 10 Jul 2026 09:45:06 -0400 Subject: [PATCH] fix(quadlet): HealthCmd falls back to bash /dev/tcp before exit 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/archipelago/src/container/quadlet.rs | 50 ++++++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/core/archipelago/src/container/quadlet.rs b/core/archipelago/src/container/quadlet.rs index c0ad61c5..7c8bdd12 100644 --- a/core/archipelago/src/container/quadlet.rs +++ b/core/archipelago/src/container/quadlet.rs @@ -538,13 +538,18 @@ fn translate_health_check(hc: &archipelago_container::HealthCheck) -> Option/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", - final_url, helper_timeout + "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, tcp_host, tcp_port ) } "cmd" => hc.endpoint.as_deref()?.to_string(), @@ -558,6 +563,24 @@ fn translate_health_check(hc: &archipelago_container::HealthCheck) -> Option (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::() { + 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 { let trimmed = raw.trim(); if trimmed.is_empty() { @@ -1459,7 +1482,22 @@ app: let h = translate_health_check(&http).expect("http must translate"); assert_eq!( 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 {