From 391031ddf0d511f873a5635cc7dae91cf22d85aa Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 20 Aug 2026 11:45:57 +0200 Subject: [PATCH] fix(docker): preserve provisioning failure status Signed-off-by: Evan Lezar --- crates/openshell-driver-docker/src/lib.rs | 42 +++++++++++++--- crates/openshell-driver-docker/src/tests.rs | 55 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 7fa57ed07f..e17c644e55 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -1279,8 +1279,12 @@ impl DockerComputeDriver { sandbox_id: &str, sandbox_name: &str, ) -> Result, Status> { - if let Some(pending) = self.pending_snapshot(sandbox_id, sandbox_name).await? { - return Ok(Some(pending)); + let pending = self.pending_snapshot(sandbox_id, sandbox_name).await?; + if pending + .as_ref() + .is_some_and(pending_sandbox_has_provisioning_failure) + { + return Ok(pending); } let container = self .find_managed_container_summary(sandbox_id, sandbox_name) @@ -1292,7 +1296,7 @@ impl DockerComputeDriver { return Ok(Some(sandbox)); } - Ok(None) + Ok(pending) } async fn current_snapshots(&self) -> Result, Status> { @@ -1341,10 +1345,7 @@ impl DockerComputeDriver { .into_iter() .map(|sandbox| (sandbox.id.clone(), sandbox)) .collect::>(); - // Provisioning state is authoritative until the supervisor has - // attached to both the sandbox and gateway. A running workload - // container alone is not a usable sandbox. - by_id.extend(self.pending_snapshot_map().await); + merge_pending_sandbox_snapshots(&mut by_id, self.pending_snapshot_map().await); let mut sandboxes = by_id.into_values().collect::>(); sandboxes.sort_by(|left, right| left.id.cmp(&right.id)); Ok(sandboxes) @@ -3397,6 +3398,33 @@ fn pending_sandbox_record_id( Ok(first) } +/// A pending sandbox holds either the ordinary in-progress snapshot or the +/// explicit error published by its provisioning task. The latter is more +/// informative than a transient Docker state observed while that task cleans +/// up a failed start, so it must win during snapshot reconciliation. +fn pending_sandbox_has_provisioning_failure(sandbox: &DriverSandbox) -> bool { + sandbox.status.as_ref().is_some_and(|status| { + status.conditions.iter().any(|condition| { + condition.r#type == "Ready" + && condition.status.eq_ignore_ascii_case("false") + && condition.reason != "Starting" + }) + }) +} + +fn merge_pending_sandbox_snapshots( + snapshots: &mut HashMap, + pending: HashMap, +) { + for (sandbox_id, sandbox) in pending { + if pending_sandbox_has_provisioning_failure(&sandbox) { + snapshots.insert(sandbox_id, sandbox); + } else { + snapshots.entry(sandbox_id).or_insert(sandbox); + } + } +} + fn provisioning_condition() -> DriverCondition { DriverCondition { r#type: "Ready".to_string(), diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index a0f45f31b7..30f8688159 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -3160,6 +3160,61 @@ fn pending_lookup_is_id_authoritative_and_rejects_ambiguous_names() { assert!(pending_sandbox_record_id(&pending, "", "demo").is_err()); } +#[test] +fn pending_provisioning_failure_overrides_transient_container_state() { + let sandbox = test_sandbox(); + let dead_container = pending_sandbox_snapshot( + &sandbox, + "default", + error_condition("ContainerDead", "Container is dead"), + false, + ); + let start_failure = pending_sandbox_snapshot( + &sandbox, + "default", + error_condition( + "ContainerStartFailed", + "Docker responded with status code 500: CDI device injection failed", + ), + false, + ); + + let mut snapshots = HashMap::from([(sandbox.id.clone(), dead_container)]); + merge_pending_sandbox_snapshots( + &mut snapshots, + HashMap::from([(sandbox.id.clone(), start_failure)]), + ); + + let status = snapshots[&sandbox.id].status.as_ref().expect("status"); + assert_eq!(status.conditions[0].reason, "ContainerStartFailed"); + assert!( + status.conditions[0] + .message + .contains("CDI device injection failed") + ); +} + +#[test] +fn pending_starting_snapshot_does_not_override_container_state() { + let sandbox = test_sandbox(); + let dead_container = pending_sandbox_snapshot( + &sandbox, + "default", + error_condition("ContainerDead", "Container is dead"), + false, + ); + let starting = pending_sandbox_snapshot(&sandbox, "default", provisioning_condition(), false); + + let mut snapshots = HashMap::from([(sandbox.id.clone(), dead_container)]); + merge_pending_sandbox_snapshots( + &mut snapshots, + HashMap::from([(sandbox.id.clone(), starting)]), + ); + + let status = snapshots[&sandbox.id].status.as_ref().expect("status"); + assert_eq!(status.conditions[0].reason, "ContainerDead"); +} + #[test] fn workload_mounts_only_the_shared_channel_volume() { let config = runtime_config();