refactor(install): route orchestrator-managed apps through orchestrator first
Phase 3a of the install path consolidation. Two coupled changes:
1. install.rs handle_package_install: gate the legacy "container exists →
adopt + return" probe on !orchestrator_managed. Apps the orchestrator
knows about (bitcoin-knots, bitcoin-core, lnd, electrumx, fedimint,
filebrowser, btcpay-server stack apps, mempool stack apps, plus the
companion UIs that just moved to Quadlet) skip the legacy probe and
fall straight into the orchestrator branch.
The legacy adopt block was returning success on a bare `podman start`
exit-0 — even when the process inside the container crashed seconds
later. That's the .228 "running but unreachable" failure mode. The
orchestrator's ensure_running honors the manifest's health check and
pre-start hooks (e.g. re-renders bitcoin-ui's nginx.conf if the RPC
password rotated), so this is a behavioral upgrade, not just a
refactor.
2. ProdContainerOrchestrator::install: make idempotent. Previously it
blindly called install_fresh which would fail on `podman create` if
the container name already existed. Now it delegates to ensure_running:
- Container Running + healthy → no-op (refresh hooks, restart if
config rewritten)
- Container Stopped/Exited → start (with hook refresh)
- Container missing → install_fresh
- Container in wedged state (Created/Paused/Unknown) → force-recreate
Without this, change #1 would regress every "container already exists"
case for the 18 orchestrator-managed app IDs. With it, install becomes
the single source of truth for "make app X be in the desired state."
Tests: 654 passed across the workspace (614 unit + 37 orchestration + 3
rpc), 0 failures. The 20 prod_orchestrator tests cover the install /
ensure_running / reconcile paths the new install delegates through.
Net delta: install.rs grows by ~30 lines (gating wrapper + comments),
prod_orchestrator.rs grows by ~30 lines (idempotent install body). Both
are temporary — the larger deletions (~1700 lines) come once every app
has been verified through the orchestrator path in subsequent phases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
23c4e7441f
commit
f9e34fd0c6
@@ -120,113 +120,133 @@ impl RpcHandler {
|
||||
false
|
||||
};
|
||||
|
||||
// Check if container already exists
|
||||
let check_output = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"ps",
|
||||
"-a",
|
||||
"--format",
|
||||
"{{.Names}}",
|
||||
"--filter",
|
||||
&format!("name=^{}$", package_id),
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to check existing containers")?;
|
||||
// For orchestrator-managed apps, skip the legacy "container exists →
|
||||
// adopt + return" probe entirely. The orchestrator's own install path
|
||||
// (below) calls ensure_running which:
|
||||
// - no-ops if the container is already up and healthy,
|
||||
// - removes + reinstalls if the container is broken,
|
||||
// - actually verifies health via the manifest's health check
|
||||
// (whereas the legacy adopt block returns success on a podman
|
||||
// `start` exit-0, even if the process inside crashed seconds
|
||||
// later — the .228 bitcoin "running but unreachable" failure
|
||||
// mode).
|
||||
// The adoption block is being phased out as apps move to the
|
||||
// orchestrator path. Non-orchestrator apps still hit it.
|
||||
let orchestrator_managed =
|
||||
should_try_orchestrator_install(package_id, self.orchestrator.is_some());
|
||||
|
||||
if !String::from_utf8_lossy(&check_output.stdout)
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
// Container already exists (e.g. created by first-boot) — adopt it
|
||||
info!(
|
||||
"Container {} already exists, adopting as installed",
|
||||
package_id
|
||||
);
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT: {} — container already exists",
|
||||
package_id
|
||||
))
|
||||
.await;
|
||||
|
||||
// Check container state
|
||||
let state_output = tokio::process::Command::new("podman")
|
||||
.args(["inspect", package_id, "--format", "{{.State.Status}}"])
|
||||
// Check if container already exists (legacy adoption — non-orchestrator
|
||||
// apps only).
|
||||
if !orchestrator_managed {
|
||||
let check_output = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"ps",
|
||||
"-a",
|
||||
"--format",
|
||||
"{{.Names}}",
|
||||
"--filter",
|
||||
&format!("name=^{}$", package_id),
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to inspect existing container")?;
|
||||
let state = String::from_utf8_lossy(&state_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
.context("Failed to check existing containers")?;
|
||||
|
||||
if state == "running" && repaired_bitcoin_conf {
|
||||
if !String::from_utf8_lossy(&check_output.stdout)
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
// Container already exists (e.g. created by first-boot) — adopt it
|
||||
info!(
|
||||
"Restarting existing container {} after bitcoin.conf RPC repair",
|
||||
"Container {} already exists, adopting as installed",
|
||||
package_id
|
||||
);
|
||||
let restart_output = tokio::process::Command::new("podman")
|
||||
.args(["restart", package_id])
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT: {} — container already exists",
|
||||
package_id
|
||||
))
|
||||
.await;
|
||||
|
||||
// Check container state
|
||||
let state_output = tokio::process::Command::new("podman")
|
||||
.args(["inspect", package_id, "--format", "{{.State.Status}}"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to restart existing container after bitcoin.conf repair")?;
|
||||
if !restart_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&restart_output.stderr);
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT FAIL: {} - restart after RPC repair failed: {}",
|
||||
package_id, stderr
|
||||
))
|
||||
.await;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Container {} exists but failed to restart after RPC repair: {}",
|
||||
package_id,
|
||||
stderr
|
||||
));
|
||||
}
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["restart", "archy-bitcoin-ui"])
|
||||
.output()
|
||||
.await;
|
||||
wait_for_adopted_container(package_id, package_id).await?;
|
||||
} else if state != "running" {
|
||||
// Start the stopped/exited container
|
||||
info!("Starting existing container {} (was {})", package_id, state);
|
||||
let start_output = tokio::process::Command::new("podman")
|
||||
.args(["start", package_id])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to start existing container")?;
|
||||
if !start_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&start_output.stderr);
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT FAIL: {} — start failed: {}",
|
||||
package_id, stderr
|
||||
))
|
||||
.await;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Container {} exists but failed to start: {}",
|
||||
package_id,
|
||||
stderr
|
||||
));
|
||||
.context("Failed to inspect existing container")?;
|
||||
let state = String::from_utf8_lossy(&state_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if state == "running" && repaired_bitcoin_conf {
|
||||
info!(
|
||||
"Restarting existing container {} after bitcoin.conf RPC repair",
|
||||
package_id
|
||||
);
|
||||
let restart_output = tokio::process::Command::new("podman")
|
||||
.args(["restart", package_id])
|
||||
.output()
|
||||
.await
|
||||
.context(
|
||||
"Failed to restart existing container after bitcoin.conf repair",
|
||||
)?;
|
||||
if !restart_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&restart_output.stderr);
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT FAIL: {} - restart after RPC repair failed: {}",
|
||||
package_id, stderr
|
||||
))
|
||||
.await;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Container {} exists but failed to restart after RPC repair: {}",
|
||||
package_id,
|
||||
stderr
|
||||
));
|
||||
}
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["restart", "archy-bitcoin-ui"])
|
||||
.output()
|
||||
.await;
|
||||
wait_for_adopted_container(package_id, package_id).await?;
|
||||
} else if state != "running" {
|
||||
// Start the stopped/exited container
|
||||
info!("Starting existing container {} (was {})", package_id, state);
|
||||
let start_output = tokio::process::Command::new("podman")
|
||||
.args(["start", package_id])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to start existing container")?;
|
||||
if !start_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&start_output.stderr);
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT FAIL: {} — start failed: {}",
|
||||
package_id, stderr
|
||||
))
|
||||
.await;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Container {} exists but failed to start: {}",
|
||||
package_id,
|
||||
stderr
|
||||
));
|
||||
}
|
||||
|
||||
wait_for_adopted_container(package_id, package_id).await?;
|
||||
}
|
||||
|
||||
wait_for_adopted_container(package_id, package_id).await?;
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT OK: {} — already running",
|
||||
package_id
|
||||
))
|
||||
.await;
|
||||
return Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": package_id,
|
||||
"message": format!("Package {} already installed and running", package_id)
|
||||
}));
|
||||
}
|
||||
|
||||
install_log(&format!(
|
||||
"INSTALL ADOPT OK: {} — already running",
|
||||
package_id
|
||||
))
|
||||
.await;
|
||||
return Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": package_id,
|
||||
"message": format!("Package {} already installed and running", package_id)
|
||||
}));
|
||||
}
|
||||
|
||||
// Preferred path for apps already modeled in the production orchestrator.
|
||||
// Keep legacy install flow as default while migration is in progress.
|
||||
if should_try_orchestrator_install(package_id, self.orchestrator.is_some()) {
|
||||
if orchestrator_managed {
|
||||
let orchestrator_app_id = orchestrator_install_app_id(package_id);
|
||||
self.set_install_phase(package_id, InstallPhase::CreatingContainer)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user