fix(orchestrator): durable uninstall marker for baseline apps + archival-bitcoin/version-report gaps
- mempool-api now declares dependencies:[bitcoin:archival] directly, closing a
gap where installing it standalone (a legitimate direct orchestrator-install
target) bypassed the mempool umbrella's pruning gate entirely.
- New durable user-uninstalled marker (crash_recovery.rs, mirrors user_stopped)
fixes required-baseline-app self-heal (bitcoin-knots/electrumx/lnd/mempool/
etc.) resurrecting itself after an explicit uninstall survives a restart or
reboot, since the in-memory disabled set is wiped by every load_manifests().
- installed_version() (set_config.rs) no longer trusts a floating image tag
("latest") as the reported running version -- a stale local :latest cache
reported "latest" forever regardless of what latest had moved on to. Now
falls back to asking the Bitcoin backend directly via `bitcoind --version`
when the tag is floating.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
de8b2bb812
commit
5b7cd5d5d0
@@ -1567,6 +1567,25 @@ impl ProdContainerOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
// Same durability problem as user-stopped above, but for uninstall:
|
||||
// `is_required_baseline_app` below otherwise self-heals bitcoin-knots,
|
||||
// electrumx, lnd, mempool, etc. the moment their container is missing —
|
||||
// including right after an explicit uninstall, since the in-memory
|
||||
// `disabled` set doesn't survive a `load_manifests()` reload (every
|
||||
// archipelago restart/reboot runs one before the first reconcile).
|
||||
{
|
||||
let user_uninstalled =
|
||||
crate::crash_recovery::load_user_uninstalled(&self.data_dir).await;
|
||||
if user_uninstalled.contains(&app_id) || user_uninstalled.contains(&name) {
|
||||
tracing::debug!(
|
||||
app_id = %app_id,
|
||||
container = %name,
|
||||
"reconcile skipped — app is user-uninstalled (must stay removed)"
|
||||
);
|
||||
return Ok(ReconcileAction::Left("user-uninstalled".into()));
|
||||
}
|
||||
}
|
||||
|
||||
match self.runtime.get_container_status(&name).await {
|
||||
Ok(status) => {
|
||||
// Phase 3.3: migrate pre-Phase-3 containers in place, but only
|
||||
@@ -3317,6 +3336,10 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
// `ensure_running_with_mode` doesn't skip the very container we're
|
||||
// installing. (start/restart RPC handlers clear it on their side too.)
|
||||
crate::crash_recovery::clear_user_stopped(&self.data_dir, app_id).await;
|
||||
// Same for the user-uninstalled marker — otherwise re-installing a
|
||||
// baseline app the user had previously uninstalled would hit the
|
||||
// same reconcile guard and silently no-op.
|
||||
crate::crash_recovery::clear_user_uninstalled(&self.data_dir, app_id).await;
|
||||
// Idempotent: if the container is already up and healthy, just
|
||||
// refresh hooks and return. If it's stopped, start it. If it's
|
||||
// missing or in a wedged state, install fresh.
|
||||
@@ -3364,6 +3387,7 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
// `ensure_running_with_mode` doesn't skip this container (symmetric with
|
||||
// install; the start/restart RPC handlers also clear it).
|
||||
crate::crash_recovery::clear_user_stopped(&self.data_dir, app_id).await;
|
||||
crate::crash_recovery::clear_user_uninstalled(&self.data_dir, app_id).await;
|
||||
let lm = self.loaded(app_id).await?;
|
||||
let action = self.ensure_running(&lm).await?;
|
||||
match action {
|
||||
@@ -3532,11 +3556,20 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.disabled.insert(app_id.to_string());
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.disabled.insert(app_id.to_string());
|
||||
}
|
||||
if let Some(e) = remove_err {
|
||||
return Err(e);
|
||||
}
|
||||
// Durable, unlike `state.disabled` above (wiped by every
|
||||
// `load_manifests()`, which runs on every archipelago restart/reboot
|
||||
// before the first reconcile) — without this, `is_required_baseline_app`
|
||||
// self-heals bitcoin-knots/electrumx/lnd/mempool/etc. right back after
|
||||
// an explicit uninstall survives to the next restart. Only mark on the
|
||||
// success path above — a failed removal means the app isn't actually gone.
|
||||
crate::crash_recovery::mark_user_uninstalled(&self.data_dir, app_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4611,6 +4644,38 @@ app:
|
||||
assert!(calls.iter().any(|c| c == "start_container:filebrowser"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_existing_respects_durable_user_uninstalled_marker_for_baseline_apps() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
let mut orch = orch_with(rt.clone()).await;
|
||||
orch.set_disk_gb_for_test(500);
|
||||
orch.insert_manifest_for_test(
|
||||
pull_manifest("filebrowser", "docker.io/filebrowser/filebrowser:latest"),
|
||||
PathBuf::from("/tmp/filebrowser"),
|
||||
)
|
||||
.await;
|
||||
// Simulates the real-world sequence: user uninstalls a required
|
||||
// baseline app, then archipelago restarts (which wipes the in-memory
|
||||
// `disabled` set via `load_manifests()` before this harness even
|
||||
// gets involved) — the durable marker is all that's left standing
|
||||
// between that and the baseline self-heal below reinstalling it.
|
||||
crate::crash_recovery::mark_user_uninstalled(&orch.data_dir, "filebrowser").await;
|
||||
|
||||
let report = orch.reconcile_existing().await;
|
||||
|
||||
assert_eq!(
|
||||
report.actions,
|
||||
vec![(
|
||||
"filebrowser".to_string(),
|
||||
ReconcileAction::Left("user-uninstalled".to_string())
|
||||
)]
|
||||
);
|
||||
assert!(report.failures.is_empty());
|
||||
let calls = rt.calls();
|
||||
assert!(!calls.iter().any(|c| c.starts_with("pull_image:")));
|
||||
assert!(!calls.iter().any(|c| c.starts_with("create_container:")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_existing_skips_archival_baseline_apps_on_pruned_hosts() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
|
||||
Reference in New Issue
Block a user