soma_infra/
container_recreate_engine.rs1use soma_fleet::HostRecord;
2use soma_ops::{MutationSendState, Timestamp, VerificationStatus};
3use tokio_util::sync::CancellationToken;
4
5use crate::{
6 ContainerRecreateClient, ContainerRecreateOutcome, ContainerRecreateRequest,
7 ContainerRecreateStage, ContainerState, MutationFailure, MutationResult, MutationVerification,
8};
9
10#[derive(Debug, Clone, Copy, Default)]
12pub struct ContainerRecreateEngine;
13
14impl ContainerRecreateEngine {
15 pub async fn execute(
17 &self,
18 client: &dyn ContainerRecreateClient,
19 host: &HostRecord,
20 request: &ContainerRecreateRequest,
21 cancellation: &CancellationToken,
22 ) -> MutationResult<ContainerRecreateOutcome> {
23 ensure_admitted(request, cancellation)?;
24 let current = client
25 .recreate_fingerprint(host, &request.expected().container, cancellation)
26 .await
27 .map_err(|error| MutationFailure::new(MutationSendState::NotSent, error))?;
28 if current != *request.expected() {
29 return Err(MutationFailure::new(
30 MutationSendState::NotSent,
31 crate::InfraError::InvalidRequest {
32 domain: "container-recreate",
33 message: "container configuration changed after planning".into(),
34 },
35 ));
36 }
37 let before = client
38 .inspect_container(host, &request.expected().container, cancellation)
39 .await
40 .map_err(|error| MutationFailure::new(MutationSendState::NotSent, error))?;
41 let receipt = client
42 .recreate_container(host, request, cancellation)
43 .await?;
44 let Some(new_id) = receipt.new_container.clone() else {
45 return Ok(ContainerRecreateOutcome {
46 host: host.id().clone(),
47 topology_revision: host.revision().clone(),
48 before,
49 after: None,
50 original_container: receipt.original_container,
51 new_container: None,
52 changed: receipt.stage != ContainerRecreateStage::Prepared,
53 stage: receipt.stage,
54 pulled: receipt.pulled,
55 send_state: receipt.send_state,
56 verification_status: VerificationStatus::Failed,
57 verification: MutationVerification {
58 status: "failed".into(),
59 summary: "replacement did not produce a new container identifier".into(),
60 },
61 });
62 };
63 let after_read = client.inspect_container(host, &new_id, cancellation).await;
64 let (after, verification_status, summary) = match after_read {
65 Ok(after) => {
66 let actual_name = after
67 .name
68 .as_deref()
69 .unwrap_or_default()
70 .trim_start_matches('/');
71 if actual_name == request.expected().name && after.state == ContainerState::Running
72 {
73 (
74 Some(after),
75 VerificationStatus::Verified,
76 "replacement container is running under the captured name".into(),
77 )
78 } else {
79 (
80 Some(after),
81 VerificationStatus::Failed,
82 "replacement container did not reach the captured running post-state"
83 .into(),
84 )
85 }
86 }
87 Err(error) => (
88 None,
89 VerificationStatus::Inconclusive,
90 format!("replacement was created but post-state inspection failed: {error}"),
91 ),
92 };
93 Ok(ContainerRecreateOutcome {
94 host: host.id().clone(),
95 topology_revision: host.revision().clone(),
96 before,
97 after,
98 original_container: receipt.original_container,
99 new_container: Some(new_id),
100 changed: true,
101 stage: receipt.stage,
102 pulled: receipt.pulled,
103 send_state: receipt.send_state,
104 verification_status,
105 verification: MutationVerification {
106 status: format!("{verification_status:?}").to_ascii_lowercase(),
107 summary,
108 },
109 })
110 }
111}
112
113fn ensure_admitted(
114 request: &ContainerRecreateRequest,
115 cancellation: &CancellationToken,
116) -> MutationResult<()> {
117 if cancellation.is_cancelled() {
118 return Err(MutationFailure::new(
119 MutationSendState::NotSent,
120 soma_fleet::FleetError::Cancelled.into(),
121 ));
122 }
123 if Timestamp::now() >= request.deadline() {
124 return Err(MutationFailure::new(
125 MutationSendState::NotSent,
126 soma_fleet::FleetError::DeadlineExceeded.into(),
127 ));
128 }
129 Ok(())
130}
131
132#[cfg(test)]
133#[path = "container_recreate_engine_tests.rs"]
134mod tests;