fix: prevent stale snapshots resurrecting orphaned dashboards
Demo images / Build & push demo images (push) Failing after 36s
Demo images / Build & push demo images (push) Failing after 36s
This commit is contained in:
@@ -103,6 +103,15 @@ pub fn companions_for(package_id: &str) -> &'static [CompanionSpec] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Missing companion UIs are provisioned here, never by snapshot recovery.
|
||||
/// A stale running-container snapshot must not resurrect an orphaned UI.
|
||||
pub fn is_companion_app(app_id: &str) -> bool {
|
||||
ALL_COMPANIONS
|
||||
.iter()
|
||||
.flat_map(|specs| specs.iter())
|
||||
.any(|spec| spec.image_base == app_id)
|
||||
}
|
||||
|
||||
/// Every companion this build knows how to provision. Kept beside
|
||||
/// `companions_for` — a new companion must be added to both, or the reaper
|
||||
/// will not recognise it as one of ours and will leave it running forever.
|
||||
|
||||
@@ -2071,6 +2071,10 @@ impl ProdContainerOrchestrator {
|
||||
Ok(ReconcileAction::Left(reason))
|
||||
if mode == ReconcileMode::ExistingOnly
|
||||
&& reason == "absent"
|
||||
// companion.rs owns missing UI provisioning/removal.
|
||||
// Never resurrect an orphan from a stale snapshot.
|
||||
// Existing UIs still pass through security config repair.
|
||||
&& !super::companion::is_companion_app(&app_id)
|
||||
&& (was_running.contains(&compute_container_name(&lm.manifest))
|
||||
// The durable answer, and the one that does not
|
||||
// erode. `was_running` only records what was
|
||||
@@ -7225,6 +7229,52 @@ app:
|
||||
assert!(!calls.iter().any(|c| c.starts_with("start_container:")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_existing_does_not_resurrect_orphaned_companions() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
let mut orch = orch_with(rt.clone()).await;
|
||||
orch.set_disk_gb_for_test(500);
|
||||
let companions = [
|
||||
"bitcoin-ui",
|
||||
"electrs-ui",
|
||||
"lnd-ui",
|
||||
"fedimint-ui",
|
||||
"cuprate-ui",
|
||||
];
|
||||
let mut names = Vec::new();
|
||||
for id in companions {
|
||||
let manifest = pull_manifest(id, "localhost/companion:local");
|
||||
names.push(compute_container_name(&manifest));
|
||||
orch.insert_manifest_for_test(manifest, PathBuf::from("/tmp/companion"))
|
||||
.await;
|
||||
}
|
||||
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||
crate::crash_recovery::save_container_snapshot_for_test(&orch.data_dir, &refs).await;
|
||||
// Repeated passes must leave lifecycle ownership with companion.rs.
|
||||
for _ in 0..3 {
|
||||
let report = orch.reconcile_existing().await;
|
||||
assert_eq!(report.actions.len(), companions.len());
|
||||
assert!(report
|
||||
.actions
|
||||
.iter()
|
||||
.all(|(_, action)| *action == ReconcileAction::Left("absent".into())));
|
||||
assert!(report.failures.is_empty());
|
||||
}
|
||||
let calls = rt.calls();
|
||||
for operation in [
|
||||
"pull_image:",
|
||||
"create_container:",
|
||||
"start_container:",
|
||||
"stop_container:",
|
||||
"remove_container:",
|
||||
] {
|
||||
assert!(
|
||||
!calls.iter().any(|call| call.starts_with(operation)),
|
||||
"{calls:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_existing_self_heals_missing_optional_installed_app() {
|
||||
// A non-baseline app (gitea) self-heals ONLY with installation
|
||||
|
||||
Reference in New Issue
Block a user