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 ContainerReader, ImageReader, InfraError, MutationProgressReporter, MutationResult,
11 MutationVerification,
12};
13
14const MAX_IMAGE_REFERENCE_CHARS: usize = 512;
15#[cfg(feature = "bollard-driver")]
16const MAX_PROGRESS_FRAMES: usize = 256;
17#[cfg(feature = "bollard-driver")]
18const MAX_PROGRESS_DELIVERY_ERRORS: usize = 16;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct ImagePullRequest {
23 operation_id: OperationId,
24 operation: OperationName,
25 image: String,
26 deadline: Timestamp,
27}
28
29impl ImagePullRequest {
30 pub fn new(
32 operation_id: OperationId,
33 operation: OperationName,
34 image: impl Into<String>,
35 deadline: Timestamp,
36 ) -> Result<Self, InfraError> {
37 let image = image.into();
38 validate_image_reference(&image)?;
39 Ok(Self {
40 operation_id,
41 operation,
42 image,
43 deadline,
44 })
45 }
46
47 #[must_use]
49 pub fn operation_id(&self) -> &OperationId {
50 &self.operation_id
51 }
52
53 #[must_use]
55 pub fn operation(&self) -> &OperationName {
56 &self.operation
57 }
58
59 #[must_use]
61 pub fn image(&self) -> &str {
62 &self.image
63 }
64
65 #[must_use]
67 pub const fn deadline(&self) -> Timestamp {
68 self.deadline
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ImageIdentity {
75 pub id: String,
77 pub repo_tags: Vec<String>,
79 pub repo_digests: Vec<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct ImagePullProgressFrame {
86 pub sequence: u64,
88 pub status: Option<String>,
90 pub id: Option<String>,
92 pub current: Option<u64>,
94 pub total: Option<u64>,
96 pub message: Option<String>,
98 pub error: Option<String>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct ImagePullReceipt {
105 pub host: HostId,
107 pub topology_revision: TopologyRevision,
109 pub image: String,
111 pub send_state: MutationSendState,
113 pub total_events: u64,
115 pub progress: Vec<ImagePullProgressFrame>,
117 pub progress_truncated: bool,
119 pub progress_delivery_errors: Vec<String>,
121}
122
123#[cfg(feature = "bollard-driver")]
124impl ImagePullReceipt {
125 pub(crate) fn retain_frame(&mut self, frame: ImagePullProgressFrame) {
126 self.total_events = self.total_events.saturating_add(1);
127 if self.progress.len() < MAX_PROGRESS_FRAMES {
128 self.progress.push(frame);
129 } else {
130 self.progress_truncated = true;
131 }
132 }
133
134 pub(crate) fn retain_delivery_error(&mut self, error: String) {
135 if self.progress_delivery_errors.len() < MAX_PROGRESS_DELIVERY_ERRORS {
136 self.progress_delivery_errors.push(error);
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct ImagePullOutcome {
144 pub host: HostId,
146 pub topology_revision: TopologyRevision,
148 pub image: String,
150 pub changed: bool,
152 pub send_state: MutationSendState,
154 pub before: Option<ImageIdentity>,
156 pub after: Option<ImageIdentity>,
158 pub total_events: u64,
160 pub progress: Vec<ImagePullProgressFrame>,
162 pub progress_truncated: bool,
164 pub progress_delivery_errors: Vec<String>,
166 pub verification_status: VerificationStatus,
168 pub verification: MutationVerification,
170}
171
172#[async_trait]
174pub trait ImagePullMutator: Send + Sync {
175 async fn pull_image(
177 &self,
178 host: &HostRecord,
179 request: &ImagePullRequest,
180 progress: &dyn MutationProgressReporter,
181 cancellation: &CancellationToken,
182 ) -> MutationResult<ImagePullReceipt>;
183}
184
185pub trait DockerArtifactClient: ContainerReader + ImageReader + ImagePullMutator {}
187
188impl<T> DockerArtifactClient for T where T: ContainerReader + ImageReader + ImagePullMutator {}
189
190#[async_trait]
192pub trait DockerArtifactClientProvider: Send + Sync {
193 async fn artifact_client(
195 &self,
196 host: &HostRecord,
197 cancellation: &CancellationToken,
198 ) -> Result<Arc<dyn DockerArtifactClient>, InfraError>;
199}
200
201pub(crate) fn validate_image_reference(image: &str) -> Result<(), InfraError> {
202 let chars = image.chars().count();
203 if chars == 0
204 || chars > MAX_IMAGE_REFERENCE_CHARS
205 || image.chars().any(char::is_control)
206 || image.chars().any(char::is_whitespace)
207 || image.starts_with('-')
208 {
209 return Err(InfraError::InvalidRequest {
210 domain: "image-pull",
211 message: "invalid image reference".into(),
212 });
213 }
214 Ok(())
215}
216
217#[must_use]
219pub fn canonical_image_reference(image: &str) -> String {
220 if image.contains('@') {
221 return image.to_owned();
222 }
223 let last_slash = image.rfind('/');
224 let last_colon = image.rfind(':');
225 if last_colon.is_some_and(|colon| last_slash.is_none_or(|slash| colon > slash)) {
226 image.to_owned()
227 } else {
228 format!("{image}:latest")
229 }
230}
231
232#[cfg(test)]
233#[path = "image_pull_tests.rs"]
234mod tests;