fix(orchestrator): install_fresh skips pull when the image is local

Reconcile recreates route through install_fresh, which pulled
unconditionally: with git.tx1138.com dead, every boot reconcile of
archy-btcpay-db/archy-nbxplorer hard-failed on 'pulling ...' even
though both image:tags sat in local storage the whole time
(live-testing report 2026-07-10). Check image_exists first and only
pull when the ref is truly absent — the same semantics as
ensure_resolved_source_available and the generated quadlets'
Pull=never.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-10 08:48:36 -04:00
parent cfdf2da9a5
commit 3a5c5db187

View File

@ -2050,10 +2050,30 @@ impl ProdContainerOrchestrator {
lm.manifest.app.id
);
}
self.runtime
.pull_image(&image, image_signature.as_deref())
.await
.with_context(|| format!("pulling {image}"))?;
// Reconcile recreates route through here too: an unreachable
// registry must not brick an app whose exact image:tag is
// already in local storage (boot reconcile of archy-btcpay-db
// /archy-nbxplorer with git.tx1138.com dead, 2026-07-10).
// Same exists-first semantics as
// ensure_resolved_source_available and the quadlets'
// Pull=never.
let exists = match self.runtime.image_exists(&image).await {
Ok(exists) => exists,
Err(err) => {
tracing::warn!(
image = %image,
error = %err,
"image existence check failed; pulling image instead"
);
false
}
};
if !exists {
self.runtime
.pull_image(&image, image_signature.as_deref())
.await
.with_context(|| format!("pulling {image}"))?;
}
}
ResolvedSource::Build(mut bcfg) => {
// Resolve a relative context: against the manifest's own directory.
@ -4195,6 +4215,8 @@ mod tests {
StdMutex<HashMap<String, Vec<archipelago_container::manifest::SecretEnvRef>>>,
/// If set, the next `build_image` call fails with this message.
fail_build: StdMutex<Option<String>>,
/// If set, every `pull_image` call fails with this message.
fail_pull: StdMutex<Option<String>>,
/// If set, `image_exists` fails for this image reference.
fail_image_exists: StdMutex<HashMap<String, String>>,
/// If set, `start_container` for this container fails with this message.
@ -4248,6 +4270,9 @@ mod tests {
impl ContainerRuntimeTrait for MockRuntime {
async fn pull_image(&self, image: &str, _sig: Option<&str>) -> Result<()> {
self.record(format!("pull_image:{image}"));
if let Some(msg) = self.fail_pull.lock().unwrap().clone() {
return Err(anyhow::anyhow!("{msg}"));
}
self.mark_image_present(image);
Ok(())
}
@ -4691,6 +4716,44 @@ app:
assert!(!calls.iter().any(|c| c.starts_with("build_image:")));
}
#[tokio::test]
async fn install_fresh_skips_pull_when_image_local() {
// An unreachable registry must not brick a reconcile recreate whose
// exact image:tag is already in local storage (archy-btcpay-db /
// archy-nbxplorer boot reconcile with git.tx1138.com dead,
// 2026-07-10).
let rt = Arc::new(MockRuntime::default());
rt.mark_image_present("docker.io/bitcoin/knots:28");
*rt.fail_pull.lock().unwrap() = Some("registry unreachable".into());
let orch = orch_with(rt.clone()).await;
orch.insert_manifest_for_test(
pull_manifest("bitcoin-knots", "docker.io/bitcoin/knots:28"),
PathBuf::from("/tmp/fixtures/bitcoin-knots"),
)
.await;
let name = orch.install("bitcoin-knots").await.unwrap();
assert_eq!(name, "bitcoin-knots");
let calls = rt.calls();
assert!(!calls.iter().any(|c| c.starts_with("pull_image:")));
assert!(calls.iter().any(|c| c == "start_container:bitcoin-knots"));
}
#[tokio::test]
async fn install_fresh_still_fails_when_image_truly_absent() {
let rt = Arc::new(MockRuntime::default());
*rt.fail_pull.lock().unwrap() = Some("registry unreachable".into());
let orch = orch_with(rt.clone()).await;
orch.insert_manifest_for_test(
pull_manifest("bitcoin-knots", "docker.io/bitcoin/knots:28"),
PathBuf::from("/tmp/fixtures/bitcoin-knots"),
)
.await;
let err = orch.install("bitcoin-knots").await.unwrap_err();
assert!(err.to_string().contains("pulling"), "err: {err}");
}
#[tokio::test]
async fn knows_app_reflects_loaded_manifests() {
// The RPC install gate routes any app with a known manifest through