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:
parent
cfdf2da9a5
commit
3a5c5db187
@ -2050,11 +2050,31 @@ impl ProdContainerOrchestrator {
|
|||||||
lm.manifest.app.id
|
lm.manifest.app.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 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
|
self.runtime
|
||||||
.pull_image(&image, image_signature.as_deref())
|
.pull_image(&image, image_signature.as_deref())
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("pulling {image}"))?;
|
.with_context(|| format!("pulling {image}"))?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ResolvedSource::Build(mut bcfg) => {
|
ResolvedSource::Build(mut bcfg) => {
|
||||||
// Resolve a relative context: against the manifest's own directory.
|
// Resolve a relative context: against the manifest's own directory.
|
||||||
let ctx_path = Path::new(&bcfg.context);
|
let ctx_path = Path::new(&bcfg.context);
|
||||||
@ -4195,6 +4215,8 @@ mod tests {
|
|||||||
StdMutex<HashMap<String, Vec<archipelago_container::manifest::SecretEnvRef>>>,
|
StdMutex<HashMap<String, Vec<archipelago_container::manifest::SecretEnvRef>>>,
|
||||||
/// If set, the next `build_image` call fails with this message.
|
/// If set, the next `build_image` call fails with this message.
|
||||||
fail_build: StdMutex<Option<String>>,
|
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.
|
/// If set, `image_exists` fails for this image reference.
|
||||||
fail_image_exists: StdMutex<HashMap<String, String>>,
|
fail_image_exists: StdMutex<HashMap<String, String>>,
|
||||||
/// If set, `start_container` for this container fails with this message.
|
/// If set, `start_container` for this container fails with this message.
|
||||||
@ -4248,6 +4270,9 @@ mod tests {
|
|||||||
impl ContainerRuntimeTrait for MockRuntime {
|
impl ContainerRuntimeTrait for MockRuntime {
|
||||||
async fn pull_image(&self, image: &str, _sig: Option<&str>) -> Result<()> {
|
async fn pull_image(&self, image: &str, _sig: Option<&str>) -> Result<()> {
|
||||||
self.record(format!("pull_image:{image}"));
|
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);
|
self.mark_image_present(image);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -4691,6 +4716,44 @@ app:
|
|||||||
assert!(!calls.iter().any(|c| c.starts_with("build_image:")));
|
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]
|
#[tokio::test]
|
||||||
async fn knows_app_reflects_loaded_manifests() {
|
async fn knows_app_reflects_loaded_manifests() {
|
||||||
// The RPC install gate routes any app with a known manifest through
|
// The RPC install gate routes any app with a known manifest through
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user