Resolve complete reviewed IndeeHub stack before image preparation
This commit is contained in:
@@ -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
|
||||
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));
|
||||
// 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?;
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user