Resolve complete reviewed IndeeHub stack before image preparation

This commit is contained in:
archipelago
2026-10-07 17:04:00 -04:00
parent cade9cb389
commit 2bfa84efa9
3 changed files with 136 additions and 5 deletions
+102 -4
View File
@@ -204,12 +204,36 @@ impl RpcHandler {
let images_to_pull =
targets.unwrap_or_else(|| self.resolve_images_to_pull(package_id, &pinned));
// Get all containers for this app
// Managed imports are intentionally private. Resolve the trusted catalog
// against the reviewed local digest plan before any registry preparation;
// never pull mutable dependency tags over an approved local plan. Every
// preparation refusal clears Updating, including inventory/plan failures.
let preparation = async {
let containers = get_containers_for_app(package_id).await?;
if containers.is_empty() {
self.clear_update_state(package_id).await;
return Err(anyhow::anyhow!("No containers found for {}", package_id));
anyhow::ensure!(
!containers.is_empty(),
"No containers found for {}",
package_id
);
let images = self
.reviewed_managed_image_references(
package_id,
&containers,
&images_to_pull,
&lifecycle_guard,
)
.await?;
Ok::<_, anyhow::Error>((containers, images))
}
.await;
let (containers, images_to_pull) = match preparation {
Ok(prepared) => prepared,
Err(error) => {
self.clear_install_progress(package_id).await;
self.clear_update_state(package_id).await;
return Err(error);
}
};
// Resolve every image while the old stack is still available. A
// registry outage or missing private import must not stop the app or
@@ -350,6 +374,57 @@ impl RpcHandler {
}
}
async fn reviewed_managed_image_references(
&self,
package_id: &str,
containers: &[String],
images: &[(String, String)],
guard: &crate::container::update_transaction::Guard,
) -> Result<Vec<(String, String)>> {
use crate::container::supervised_runtime::{
load_reviewed_plans, LegacyIndeeMaintenance, SystemdSupervisor,
};
use crate::container::supervised_update::Supervisor;
use crate::container::update_transaction::Podman;
let mut managed = 0;
for name in containers {
if crate::container::quadlet::unit_exists(name).await {
managed += 1;
}
}
if managed == 0 {
return Ok(images.to_vec());
}
anyhow::ensure!(
managed == containers.len() && package_id == "indeedhub",
"Managed stack requires its complete qualified maintenance plan; originals unchanged"
);
let catalog = Podman::targets(images, false).await?;
let names: HashSet<_> = containers.iter().map(String::as_str).collect();
anyhow::ensure!(
catalog.len() == names.len()
&& catalog
.iter()
.all(|target| names.contains(target.name.as_str())),
"Reviewed managed target membership differs from installed stack"
);
let adapter = SystemdSupervisor::new(
self.config.data_dir.clone(),
load_reviewed_plans(&self.config.data_dir, package_id)?,
LegacyIndeeMaintenance::new(guard)?,
)
.await?;
let targets = adapter.reviewed_targets(&catalog).await?;
for target in &targets {
let original = adapter.capture(&target.name).await?;
adapter.prepare_target(target, &original).await?;
}
Ok(targets
.into_iter()
.map(|target| (target.name, target.reference))
.collect())
}
async fn recreate_container_for_update(
&self,
package_id: &str,
@@ -853,6 +928,29 @@ mod tests {
}
}
#[test]
fn indeehub_update_resolves_every_member_in_dependency_order() {
let members = crate::container::image_versions::containers_for_stack("indeedhub");
let expected = super::all_container_names("indeedhub");
assert_eq!(
members.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
expected
);
let source = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../scripts/image-versions.sh"
));
for (_, variable) in members {
assert!(
source
.lines()
.any(|line| line.starts_with(&format!("{variable}="))),
"Missing image pin {variable}"
);
}
assert!(super::uses_legacy_update_flow("indeedhub"));
}
#[tokio::test]
async fn stack_image_failure_precedes_every_lifecycle_action() {
use std::sync::{Arc, Mutex};
@@ -361,6 +361,16 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> {
("immich_redis", "REDIS_IMAGE"),
("immich_server", "IMMICH_SERVER_IMAGE"),
],
// Dependency order is also the supervised startup/rollback order.
"indeedhub" => vec![
("indeedhub-postgres", "INDEEDHUB_POSTGRES_IMAGE"),
("indeedhub-redis", "INDEEDHUB_REDIS_IMAGE"),
("indeedhub-minio", "MINIO_IMAGE"),
("indeedhub-relay", "NOSTR_RS_RELAY_IMAGE"),
("indeedhub-api", "INDEEDHUB_API_IMAGE"),
("indeedhub-ffmpeg", "INDEEDHUB_FFMPEG_IMAGE"),
("indeedhub", "INDEEDHUB_IMAGE"),
],
"netbird" => vec![
("netbird", "NETBIRD_PROXY_IMAGE"),
("netbird-dashboard", "NETBIRD_DASHBOARD_IMAGE"),
@@ -167,3 +167,26 @@ files, which verifies the intended absence of a fabricated completed transaction
Evidence: `/tmp/archy-20261007-indeehub-final-backend-recheck.log`; initial failed
fixture evidence remains in `/tmp/archy-20261007-indeehub-final-backend.log`.
Optimized artifact build and full real VM adapter acceptance remain pending.
## Full RPC dispatch correction — 2026-10-07
Tracing the actual `package.update` path found that IndeeHub was marked as a stack
but missing from the pinned stack-image mapping. It therefore resolved only its
frontend before the seven-member runtime membership check. The mapping now covers
PostgreSQL, Redis, MinIO, relay, API, worker and frontend in dependency order.
Managed preflight now resolves the signed catalog references locally against the
reviewed immutable plan and checks original unit hashes/registration pins before
image preparation. Only verified local digest references reach preparation, so
unchanged mutable dependency tags cannot trigger a registry pull before the private
plan is checked. Unmanaged updates retain registry preparation. Inventory and plan
refusals clear progress and the inner Updating state; the asynchronous RPC wrapper
also retains its existing failure/scanner cleanup.
The final source passes **2,004 isolated backend tests, zero failures, five ignored**:
`/tmp/archy-20261007-indeehub-dispatch-final-backend.log`. A prior 2,004-test receipt
predated the last preparation-state cleanup and is not the final-byte result.
The optimized build was deliberately interrupted after these integration gaps were
found. A test-profile application executable is being built solely for the isolated
full RPC rehearsal; final release optimization/deployment remains pending that
rehearsal. No live IndeeHub stack or catalog has been changed.