Skip to main content

soma_infra/
container_recreate_engine.rs

1use 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/// Coordinates configuration drift checks, replacement, and post-state verification.
11#[derive(Debug, Clone, Copy, Default)]
12pub struct ContainerRecreateEngine;
13
14impl ContainerRecreateEngine {
15    /// Recreates one container and verifies the replacement is running under the captured name.
16    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;