Skip to content

Commit f0058c8

Browse files
committed
fix(update): fail fast on non-systemd opencode update
1 parent 80ad558 commit f0058c8

1 file changed

Lines changed: 78 additions & 2 deletions

File tree

packages/cli-rust/src/commands/update.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,10 @@ async fn run_selected_updates(
720720

721721
#[cfg(test)]
722722
mod tests {
723-
use super::parse_cargo_info_versions;
723+
use super::{
724+
container_not_running_update_error, ensure_in_place_opencode_update_supported,
725+
non_systemd_in_place_update_error, parse_cargo_info_versions,
726+
};
724727

725728
#[test]
726729
fn parse_cargo_info_versions_latest() {
@@ -745,6 +748,46 @@ mod tests {
745748
assert_eq!(parsed.target, "11.0.0");
746749
assert!(parsed.compatible.is_none());
747750
}
751+
752+
#[test]
753+
fn in_place_update_preflight_allows_systemd() {
754+
assert!(ensure_in_place_opencode_update_supported("systemd").is_ok());
755+
}
756+
757+
#[test]
758+
fn in_place_update_preflight_allows_unknown() {
759+
assert!(ensure_in_place_opencode_update_supported("unknown").is_ok());
760+
}
761+
762+
#[test]
763+
fn in_place_update_preflight_rejects_non_systemd() {
764+
let err = ensure_in_place_opencode_update_supported("runuser")
765+
.expect_err("runuser init should be rejected");
766+
let message = err.to_string();
767+
assert!(message.contains("runuser"));
768+
assert!(message.contains("occ update container"));
769+
assert!(message.contains("prebuilt image"));
770+
assert!(message.contains("occ config set image_source build"));
771+
}
772+
773+
#[test]
774+
fn non_systemd_error_includes_root_cause_and_remediation() {
775+
let message = non_systemd_in_place_update_error("tini").to_string();
776+
assert!(message.contains("non-systemd init"));
777+
assert!(message.contains("stop the whole container"));
778+
assert!(message.contains("occ update container"));
779+
assert!(message.contains("prebuilt image"));
780+
assert!(message.contains("from source"));
781+
assert!(message.contains("systemd-capable"));
782+
}
783+
784+
#[test]
785+
fn container_not_running_error_mentions_non_systemd_hint() {
786+
let message = container_not_running_update_error().to_string();
787+
assert!(message.contains("Non-systemd init"));
788+
assert!(message.contains("occ update container"));
789+
assert!(message.contains("prebuilt image"));
790+
}
748791
}
749792

750793
async fn cmd_update_cli(
@@ -1070,6 +1113,8 @@ pub(crate) async fn cmd_update_opencode(
10701113
let spinner = CommandSpinner::new_maybe("Updating opencode...", quiet);
10711114

10721115
ensure_container_running_for_update(&client, &config, quiet).await?;
1116+
let pid1_comm = get_pid1_comm(&client).await;
1117+
ensure_in_place_opencode_update_supported(&pid1_comm)?;
10731118

10741119
stop_opencode_for_update(&client, quiet).await?;
10751120

@@ -1421,7 +1466,38 @@ fn container_not_running_update_error() -> anyhow::Error {
14211466
anyhow!(
14221467
"Container is not running; cannot update opencode.\n\
14231468
Run:\n occ start\n\
1424-
If it exits immediately, run:\n occ logs"
1469+
If it exits immediately, run:\n occ logs\n\
1470+
Hint: Non-systemd init (for example runuser/tini) can stop the container during in-place updates.\n\
1471+
Alternatives:\n\
1472+
- Update container with prebuilt image (default): occ update container\n\
1473+
- Update container from source (slower):\n occ config set image_source build\n occ update container"
1474+
)
1475+
}
1476+
1477+
fn ensure_in_place_opencode_update_supported(pid1_comm: &str) -> Result<()> {
1478+
let normalized = pid1_comm.trim().to_ascii_lowercase();
1479+
if normalized.is_empty() || normalized == "systemd" || normalized == "unknown" {
1480+
return Ok(());
1481+
}
1482+
1483+
Err(non_systemd_in_place_update_error(pid1_comm))
1484+
}
1485+
1486+
fn non_systemd_in_place_update_error(pid1_comm: &str) -> anyhow::Error {
1487+
let trimmed = pid1_comm.trim();
1488+
let init_name = if trimmed.is_empty() {
1489+
"unknown"
1490+
} else {
1491+
trimmed
1492+
};
1493+
1494+
anyhow!(
1495+
"In-place opencode update is not supported when container init is '{init_name}'.\n\
1496+
This environment is running a non-systemd init, so stopping opencode can stop the whole container.\n\
1497+
Alternatives:\n\
1498+
- Update container with prebuilt image (default): occ update container\n\
1499+
- Update container from source (slower):\n occ config set image_source build\n occ update container\n\
1500+
If you need commit-pinned in-place updates, use a systemd-capable container runtime."
14251501
)
14261502
}
14271503

0 commit comments

Comments
 (0)