1use std::sync::Arc;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use soma_fleet::{HostId, HostRecord, TopologyRevision};
7use soma_ops::{MutationSendState, Timestamp, VerificationStatus};
8use tokio_util::sync::CancellationToken;
9
10use crate::{ContainerReader, ContainerState, InfraError, MutationResult, MutationVerification};
11
12const MAX_CONTAINER_ID_CHARS: usize = 256;
13const MAX_VERIFY_ATTEMPTS: u8 = 20;
14const MAX_VERIFY_INTERVAL: Duration = Duration::from_secs(5);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum ContainerLifecycleAction {
20 Start,
22 Stop,
24 Restart,
26 Pause,
28 Resume,
30}
31
32impl ContainerLifecycleAction {
33 #[must_use]
35 pub const fn operation_name(self) -> &'static str {
36 match self {
37 Self::Start => "container.start",
38 Self::Stop => "container.stop",
39 Self::Restart => "container.restart",
40 Self::Pause => "container.pause",
41 Self::Resume => "container.resume",
42 }
43 }
44
45 #[must_use]
47 pub const fn action_label(self) -> &'static str {
48 match self {
49 Self::Start => "start",
50 Self::Stop => "stop",
51 Self::Restart => "restart",
52 Self::Pause => "pause",
53 Self::Resume => "resume",
54 }
55 }
56
57 pub(crate) fn already_satisfied(self, state: &ContainerState) -> bool {
58 match self {
59 Self::Start | Self::Resume => matches!(state, ContainerState::Running),
60 Self::Stop => matches!(state, ContainerState::Exited | ContainerState::Dead),
61 Self::Pause => matches!(state, ContainerState::Paused),
62 Self::Restart => false,
63 }
64 }
65
66 pub(crate) fn verified(self, state: &ContainerState) -> bool {
67 match self {
68 Self::Start | Self::Restart | Self::Resume => {
69 matches!(state, ContainerState::Running)
70 }
71 Self::Stop => matches!(state, ContainerState::Exited | ContainerState::Dead),
72 Self::Pause => matches!(state, ContainerState::Paused),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct ContainerLifecycleRequest {
80 container: String,
81 action: ContainerLifecycleAction,
82 deadline: Timestamp,
83}
84
85impl ContainerLifecycleRequest {
86 pub fn new(
88 container: impl Into<String>,
89 action: ContainerLifecycleAction,
90 deadline: Timestamp,
91 ) -> Result<Self, InfraError> {
92 let container = container.into();
93 let count = container.chars().count();
94 if count == 0 || count > MAX_CONTAINER_ID_CHARS || container.chars().any(char::is_control) {
95 return Err(InfraError::InvalidRequest {
96 domain: "container-mutation",
97 message: "invalid container identifier".into(),
98 });
99 }
100 Ok(Self {
101 container,
102 action,
103 deadline,
104 })
105 }
106
107 #[must_use]
109 pub fn container(&self) -> &str {
110 &self.container
111 }
112
113 #[must_use]
115 pub const fn action(&self) -> ContainerLifecycleAction {
116 self.action
117 }
118
119 #[must_use]
121 pub const fn deadline(&self) -> Timestamp {
122 self.deadline
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct MutationVerificationPolicy {
129 attempts: u8,
130 interval: Duration,
131}
132
133impl MutationVerificationPolicy {
134 pub fn new(attempts: u8, interval: Duration) -> Result<Self, InfraError> {
136 if attempts == 0 || attempts > MAX_VERIFY_ATTEMPTS || interval > MAX_VERIFY_INTERVAL {
137 return Err(InfraError::InvalidRequest {
138 domain: "container-mutation",
139 message: format!(
140 "verification requires 1-{MAX_VERIFY_ATTEMPTS} attempts and an interval no greater than {} ms",
141 MAX_VERIFY_INTERVAL.as_millis()
142 ),
143 });
144 }
145 Ok(Self { attempts, interval })
146 }
147
148 #[must_use]
150 pub const fn attempts(self) -> u8 {
151 self.attempts
152 }
153
154 #[must_use]
156 pub const fn interval(self) -> Duration {
157 self.interval
158 }
159}
160
161impl Default for MutationVerificationPolicy {
162 fn default() -> Self {
163 Self {
164 attempts: 5,
165 interval: Duration::from_millis(200),
166 }
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ContainerMutationReceipt {
173 pub host: HostId,
175 pub topology_revision: TopologyRevision,
177 pub container: String,
179 pub action: ContainerLifecycleAction,
181 pub send_state: MutationSendState,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ContainerLifecycleOutcome {
188 pub host: HostId,
190 pub topology_revision: TopologyRevision,
192 pub container: String,
194 pub action: ContainerLifecycleAction,
196 pub changed: bool,
198 pub send_state: MutationSendState,
200 pub before: ContainerState,
202 pub after: Option<ContainerState>,
204 pub verification_status: VerificationStatus,
206 pub verification: MutationVerification,
208}
209
210#[async_trait]
212pub trait ContainerLifecycleMutator: Send + Sync {
213 async fn mutate_container(
215 &self,
216 host: &HostRecord,
217 request: &ContainerLifecycleRequest,
218 cancellation: &CancellationToken,
219 ) -> MutationResult<ContainerMutationReceipt>;
220}
221
222pub trait DockerMutationClient: ContainerReader + ContainerLifecycleMutator {}
224
225impl<T> DockerMutationClient for T where T: ContainerReader + ContainerLifecycleMutator {}
226
227#[async_trait]
229pub trait DockerMutationClientProvider: Send + Sync {
230 async fn mutation_client(
232 &self,
233 host: &HostRecord,
234 cancellation: &CancellationToken,
235 ) -> Result<Arc<dyn DockerMutationClient>, InfraError>;
236}
237
238#[cfg(test)]
239#[path = "container_mutation_tests.rs"]
240mod tests;