fix(cluster): remove stale image on destroy and verify architecture after pull

Destroy now removes the cluster image so the next deploy always pulls the
latest version from the registry.  After pulling on a remote host the
image architecture is verified against the host platform, surfacing a
clear error instead of a cryptic 'exec format error' at container start.
This commit is contained in:
Drew Newberry
2026-02-19 21:39:46 -08:00
parent 302551aca7
commit 14859fd48e
2 changed files with 55 additions and 1 deletions
+32 -1
View File
@@ -9,7 +9,7 @@ use bollard::models::{
};
use bollard::query_parameters::{
CreateContainerOptions, CreateImageOptions, InspectContainerOptions, InspectNetworkOptions,
RemoveContainerOptions, RemoveVolumeOptions, StartContainerOptions,
RemoveContainerOptions, RemoveImageOptions, RemoveVolumeOptions, StartContainerOptions,
};
use futures::StreamExt;
use miette::{IntoDiagnostic, Result, WrapErr};
@@ -427,6 +427,15 @@ pub async fn destroy_cluster_resources(
let container_name = container_name(name);
let volume_name = volume_name(name);
// Capture the container's image reference before removing the container so
// we can clean it up afterwards. This prevents stale images from being
// re-used on subsequent deploys.
let container_image = docker
.inspect_container(&container_name, None::<InspectContainerOptions>)
.await
.ok()
.and_then(|info| info.image);
let _ = stop_container(docker, &container_name).await;
let remove_container = docker
@@ -444,6 +453,28 @@ pub async fn destroy_cluster_resources(
return Err(err).into_diagnostic();
}
// Remove the cluster image so the next deploy always pulls the latest
// version from the registry instead of reusing a stale local copy.
if let Some(ref image_id) = container_image {
tracing::debug!("Removing cluster image: {}", image_id);
let result = docker
.remove_image(
image_id,
Some(RemoveImageOptions {
force: true,
noprune: true,
..Default::default()
}),
None,
)
.await;
if let Err(err) = result
&& !is_not_found(&err)
{
tracing::warn!("Failed to remove cluster image {}: {}", image_id, err);
}
}
let remove_volume = docker
.remove_volume(&volume_name, Some(RemoveVolumeOptions { force: true }))
.await;
+23
View File
@@ -252,6 +252,29 @@ pub async fn pull_remote_image(
format!("failed to tag {registry_image} as {target_repo}:{target_tag} on remote")
})?;
// Verify that the pulled image matches the expected architecture.
// This catches cases where the registry returned the wrong platform
// variant (e.g., amd64 on an arm64 host) which would cause an
// "exec format error" at container start time.
let inspect = remote
.inspect_image(image_ref)
.await
.into_diagnostic()
.wrap_err_with(|| format!("failed to inspect pulled image {image_ref} on remote"))?;
let actual_arch = inspect.architecture.as_deref().unwrap_or("unknown");
if actual_arch != remote_platform.arch {
return Err(miette::miette!(
"architecture mismatch: pulled image {image_ref} is {actual_arch} but remote host is {expected}; \
try removing stale images on the remote host and re-deploying",
expected = remote_platform.arch,
));
}
info!(
"Verified image architecture: {} matches remote host",
actual_arch
);
on_progress(format!("[status] Image {image_ref} ready on remote host"));
info!("Remote image pull and tag complete: {}", image_ref);