fix(orchestrator): make legacy mempool id start/stop/restart the split stack

Found by the .228 lifecycle gate (2026-07-08, first quadlet-mode run):
stop→start on the legacy `mempool` umbrella app id destroyed the mempool
deployment. Under quadlet, package.stop removes the containers; package.start
then failed with `unknown app_id: mempool` because load_manifests drops the
umbrella manifest whenever the three split-stack members are loaded — leaving
nothing to recreate from and the stack down.

Two fixes:
- prod_orchestrator start/stop/restart now resolve the historical
  `mempool`/`mempool-web` id to the split members (archy-mempool-db,
  mempool-api, archy-mempool-web) whenever the umbrella manifest was dropped —
  the same alias install already implements ("installing mempool assembles the
  split stack"). Restart falls back to start for members whose container/unit
  is gone. Legacy umbrella-only nodes are unaffected (alias inactive while the
  umbrella manifest is live).
- is_missing_container_error (3 sites) now recognizes podman 5.x's
  `no such object` inspect phrasing, so a genuinely absent container is
  classified as missing instead of surfacing as a hard inspect error.

Tests: 2 new alias tests + 2 classifier tests; prod_orchestrator suite 61/61
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-08 19:34:46 -04:00
co-authored by Claude Fable 5
parent 380f4f1947
commit 161a6e4dbd
4 changed files with 147 additions and 0 deletions
@@ -926,6 +926,8 @@ async fn inspect_runtime_container_state(container_name: &str) -> Result<Option<
fn is_missing_container_error(stderr: &str) -> bool {
stderr.contains("no such container")
|| stderr.contains("no container with name")
// podman 5.x `inspect` phrasing: `Error: no such object: "name"`
|| stderr.contains("no such object")
|| stderr.contains("does not exist")
|| stderr.contains("not found")
}
@@ -1966,6 +1968,22 @@ pub(super) fn orchestrator_uninstall_app_ids(package_id: &str) -> Vec<String> {
mod tests {
use super::*;
#[test]
fn missing_container_classifier_covers_podman5_phrasings() {
// Regression (.228 gate 2026-07-08): podman 5.x `inspect` on a missing
// container says `Error: no such object: "mempool"` — the classifier
// missed it, so a plain missing container surfaced as a hard inspect
// error and package.start aborted instead of proceeding to recreate.
assert!(is_missing_container_error(
"Error: no such object: \"mempool\""
));
assert!(is_missing_container_error("Error: no such container mempool"));
assert!(is_missing_container_error(
"Error: no container with name or id \"x\" found"
));
assert!(!is_missing_container_error("Error: OCI runtime error"));
}
#[test]
fn runtime_host_ports_are_manifest_derived_for_public_apps() {
assert_eq!(runtime_host_ports("photoprism"), vec![2342]);
@@ -130,6 +130,8 @@ async fn wait_for_stack_container_absent(container_name: &str, timeout: Duration
fn is_missing_container_error(stderr: &str) -> bool {
stderr.contains("no such container")
|| stderr.contains("no container with name")
// podman 5.x `inspect` phrasing: `Error: no such object: "name"`
|| stderr.contains("no such object")
|| stderr.contains("does not exist")
|| stderr.contains("not found")
}