Skip to main content

soma_infra/
container_recreate.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use soma_fleet::{HostId, HostRecord, TopologyRevision};
6use soma_ops::{MutationSendState, OperationId, OperationName, Timestamp, VerificationStatus};
7use tokio_util::sync::CancellationToken;
8
9use crate::{
10    ContainerInspect, ContainerReader, ContainerState, InfraError, InfraResult, MutationResult,
11    MutationVerification,
12};
13
14/// Stable digest and selected identity captured before a container replacement.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ContainerRecreateFingerprint {
17    /// Original container identifier.
18    pub container: String,
19    /// Container name without Docker's leading slash.
20    pub name: String,
21    /// Configured image reference.
22    pub image: String,
23    /// Current runtime state.
24    pub state: ContainerState,
25    /// SHA-256 of replacement-relevant Docker configuration.
26    pub sha256: String,
27}
28
29impl ContainerRecreateFingerprint {
30    /// Creates a validated fingerprint.
31    pub fn new(
32        container: impl Into<String>,
33        name: impl Into<String>,
34        image: impl Into<String>,
35        state: ContainerState,
36        sha256: impl Into<String>,
37    ) -> InfraResult<Self> {
38        let container = container.into();
39        let name = name.into();
40        let image = image.into();
41        let sha256 = sha256.into();
42        if container.is_empty() || name.is_empty() || image.is_empty() {
43            return Err(InfraError::InvalidRequest {
44                domain: "container-recreate",
45                message: "container, name, and image are required".into(),
46            });
47        }
48        if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
49            return Err(InfraError::InvalidRequest {
50                domain: "container-recreate",
51                message: "configuration fingerprint must be SHA-256 hex".into(),
52            });
53        }
54        Ok(Self {
55            container,
56            name,
57            image,
58            state,
59            sha256: sha256.to_ascii_lowercase(),
60        })
61    }
62}
63
64/// Deadline-bound request to replace one container from its captured configuration.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ContainerRecreateRequest {
67    operation_id: OperationId,
68    operation: OperationName,
69    expected: ContainerRecreateFingerprint,
70    pull: bool,
71    deadline: Timestamp,
72}
73
74impl ContainerRecreateRequest {
75    /// Creates a validated recreate request.
76    #[must_use]
77    pub fn new(
78        operation_id: OperationId,
79        operation: OperationName,
80        expected: ContainerRecreateFingerprint,
81        pull: bool,
82        deadline: Timestamp,
83    ) -> Self {
84        Self {
85            operation_id,
86            operation,
87            expected,
88            pull,
89            deadline,
90        }
91    }
92    /// Returns the operation identity.
93    #[must_use]
94    pub fn operation_id(&self) -> &OperationId {
95        &self.operation_id
96    }
97    /// Returns the canonical operation.
98    #[must_use]
99    pub fn operation(&self) -> &OperationName {
100        &self.operation
101    }
102    /// Returns the expected pre-state fingerprint.
103    #[must_use]
104    pub const fn expected(&self) -> &ContainerRecreateFingerprint {
105        &self.expected
106    }
107    /// Returns whether the image should be pulled first.
108    #[must_use]
109    pub const fn pull(&self) -> bool {
110        self.pull
111    }
112    /// Returns the absolute deadline.
113    #[must_use]
114    pub const fn deadline(&self) -> Timestamp {
115        self.deadline
116    }
117}
118
119/// Furthest destructive stage reached by a container recreation.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum ContainerRecreateStage {
123    /// No destructive request was sent.
124    Prepared,
125    /// Original container was stopped.
126    Stopped,
127    /// Original container was removed.
128    Removed,
129    /// Replacement container was created.
130    Created,
131    /// Replacement container was started.
132    Started,
133}
134
135/// Driver receipt for one replacement attempt.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct ContainerRecreateReceipt {
138    /// Target host.
139    pub host: HostId,
140    /// Exact topology revision.
141    pub topology_revision: TopologyRevision,
142    /// Original container identifier.
143    pub original_container: String,
144    /// New container identifier when creation completed.
145    pub new_container: Option<String>,
146    /// Captured container name.
147    pub name: String,
148    /// Captured image reference.
149    pub image: String,
150    /// Furthest stage reached.
151    pub stage: ContainerRecreateStage,
152    /// Backend send state.
153    pub send_state: MutationSendState,
154    /// Whether an image pull was requested.
155    pub pulled: bool,
156}
157
158/// Verified container replacement outcome.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct ContainerRecreateOutcome {
161    /// Target host.
162    pub host: HostId,
163    /// Exact topology revision.
164    pub topology_revision: TopologyRevision,
165    /// Captured pre-state.
166    pub before: ContainerInspect,
167    /// Verified post-state when available.
168    pub after: Option<ContainerInspect>,
169    /// Original identifier.
170    pub original_container: String,
171    /// Replacement identifier when created.
172    pub new_container: Option<String>,
173    /// Whether a replacement was observed.
174    pub changed: bool,
175    /// Furthest destructive stage reached.
176    pub stage: ContainerRecreateStage,
177    /// Whether an image pull was requested before replacement.
178    pub pulled: bool,
179    /// Backend send state.
180    pub send_state: MutationSendState,
181    /// Verification status.
182    pub verification_status: VerificationStatus,
183    /// Verification explanation.
184    pub verification: MutationVerification,
185}
186
187/// Reads a driver-native replacement fingerprint without leaking SDK models.
188#[async_trait]
189pub trait ContainerRecreateInspector: Send + Sync {
190    /// Captures replacement-relevant container configuration.
191    async fn recreate_fingerprint(
192        &self,
193        host: &HostRecord,
194        container: &str,
195        cancellation: &CancellationToken,
196    ) -> InfraResult<ContainerRecreateFingerprint>;
197}
198
199/// Performs one container replacement while preserving partial-stage evidence.
200#[async_trait]
201pub trait ContainerRecreateMutator: Send + Sync {
202    /// Replaces a container from its captured configuration.
203    async fn recreate_container(
204        &self,
205        host: &HostRecord,
206        request: &ContainerRecreateRequest,
207        cancellation: &CancellationToken,
208    ) -> MutationResult<ContainerRecreateReceipt>;
209}
210
211/// Complete client required by the verified recreate engine.
212pub trait ContainerRecreateClient:
213    ContainerReader + ContainerRecreateInspector + ContainerRecreateMutator
214{
215}
216impl<T> ContainerRecreateClient for T where
217    T: ContainerReader + ContainerRecreateInspector + ContainerRecreateMutator
218{
219}
220
221/// Supplies one host-bound container replacement client.
222#[async_trait]
223pub trait ContainerRecreateClientProvider: Send + Sync {
224    /// Creates a client bound to the exact host topology revision.
225    async fn recreate_client(
226        &self,
227        host: &HostRecord,
228        cancellation: &CancellationToken,
229    ) -> InfraResult<Arc<dyn ContainerRecreateClient>>;
230}
231
232#[cfg(test)]
233#[path = "container_recreate_tests.rs"]
234mod tests;