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
+103 -5
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
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"),