Resolve complete reviewed IndeeHub stack before image preparation
This commit is contained in:
@@ -204,12 +204,36 @@ impl RpcHandler {
|
|||||||
let images_to_pull =
|
let images_to_pull =
|
||||||
targets.unwrap_or_else(|| self.resolve_images_to_pull(package_id, &pinned));
|
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?;
|
let containers = get_containers_for_app(package_id).await?;
|
||||||
if containers.is_empty() {
|
anyhow::ensure!(
|
||||||
self.clear_update_state(package_id).await;
|
!containers.is_empty(),
|
||||||
return Err(anyhow::anyhow!("No containers found for {}", package_id));
|
"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
|
// Resolve every image while the old stack is still available. A
|
||||||
// registry outage or missing private import must not stop the app or
|
// 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(
|
async fn recreate_container_for_update(
|
||||||
&self,
|
&self,
|
||||||
package_id: &str,
|
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]
|
#[tokio::test]
|
||||||
async fn stack_image_failure_precedes_every_lifecycle_action() {
|
async fn stack_image_failure_precedes_every_lifecycle_action() {
|
||||||
use std::sync::{Arc, Mutex};
|
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_redis", "REDIS_IMAGE"),
|
||||||
("immich_server", "IMMICH_SERVER_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" => vec![
|
||||||
("netbird", "NETBIRD_PROXY_IMAGE"),
|
("netbird", "NETBIRD_PROXY_IMAGE"),
|
||||||
("netbird-dashboard", "NETBIRD_DASHBOARD_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
|
Evidence: `/tmp/archy-20261007-indeehub-final-backend-recheck.log`; initial failed
|
||||||
fixture evidence remains in `/tmp/archy-20261007-indeehub-final-backend.log`.
|
fixture evidence remains in `/tmp/archy-20261007-indeehub-final-backend.log`.
|
||||||
Optimized artifact build and full real VM adapter acceptance remain pending.
|
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