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};
7use tokio_util::sync::CancellationToken;
8
9use crate::{
10 ContainerReader, DockerTelemetryReader, ImageIdentity, ImageReader, InfraError, InfraResult,
11 MutationResult, NetworkReader, VolumeReader,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum DockerPruneTarget {
18 Containers,
20 Images,
22 Volumes,
24 Networks,
26 BuildCache,
28 All,
30}
31
32impl DockerPruneTarget {
33 pub fn parse(value: &str) -> InfraResult<Self> {
35 match value {
36 "containers" => Ok(Self::Containers),
37 "images" => Ok(Self::Images),
38 "volumes" => Ok(Self::Volumes),
39 "networks" => Ok(Self::Networks),
40 "buildcache" => Ok(Self::BuildCache),
41 "all" => Ok(Self::All),
42 _ => Err(InfraError::InvalidRequest {
43 domain: "docker-cleanup",
44 message: format!("unsupported prune target: {value}"),
45 }),
46 }
47 }
48
49 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::Containers => "containers",
54 Self::Images => "images",
55 Self::Volumes => "volumes",
56 Self::Networks => "networks",
57 Self::BuildCache => "buildcache",
58 Self::All => "all",
59 }
60 }
61
62 #[cfg(any(feature = "bollard-driver", test))]
63 pub(crate) fn expanded(self) -> &'static [Self] {
64 match self {
65 Self::All => &[
66 Self::Containers,
67 Self::Images,
68 Self::Volumes,
69 Self::Networks,
70 Self::BuildCache,
71 ],
72 Self::Containers => &[Self::Containers],
73 Self::Images => &[Self::Images],
74 Self::Volumes => &[Self::Volumes],
75 Self::Networks => &[Self::Networks],
76 Self::BuildCache => &[Self::BuildCache],
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct ImageRemovalFingerprint {
84 pub reference: String,
86 pub identity: ImageIdentity,
88 pub sha256: String,
90}
91
92impl ImageRemovalFingerprint {
93 pub fn new(reference: impl Into<String>, mut identity: ImageIdentity) -> InfraResult<Self> {
95 let reference = validate_text("image reference", reference.into(), 256)?;
96 identity.repo_tags.sort();
97 identity.repo_tags.dedup();
98 identity.repo_digests.sort();
99 identity.repo_digests.dedup();
100 let material = serde_json::to_vec(&(reference.as_str(), &identity)).map_err(|error| {
101 InfraError::Parse {
102 domain: "docker-cleanup",
103 message: error.to_string(),
104 }
105 })?;
106 let sha256 = crate::mutation::sha256_hex(&material);
107 Ok(Self {
108 reference,
109 identity,
110 sha256,
111 })
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct DockerPruneFingerprint {
118 pub host: HostId,
120 pub topology_revision: TopologyRevision,
122 pub target: DockerPruneTarget,
124 pub containers: Vec<String>,
126 pub images: Vec<String>,
128 pub volumes: Vec<String>,
130 pub networks: Vec<String>,
132 pub build_cache_bytes: u64,
134 pub sha256: String,
136}
137
138impl DockerPruneFingerprint {
139 pub(crate) fn finalize(mut self) -> InfraResult<Self> {
140 for values in [
141 &mut self.containers,
142 &mut self.images,
143 &mut self.volumes,
144 &mut self.networks,
145 ] {
146 values.sort();
147 values.dedup();
148 }
149 let material = serde_json::to_vec(&(
150 &self.host,
151 &self.topology_revision,
152 self.target,
153 &self.containers,
154 &self.images,
155 &self.volumes,
156 &self.networks,
157 self.build_cache_bytes,
158 ))
159 .map_err(|error| InfraError::Parse {
160 domain: "docker-cleanup",
161 message: error.to_string(),
162 })?;
163 self.sha256 = crate::mutation::sha256_hex(&material);
164 Ok(self)
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ImageRemovalRequest {
171 pub operation_id: OperationId,
173 pub operation: OperationName,
175 pub fingerprint: ImageRemovalFingerprint,
177 pub force: bool,
179 pub deadline: Timestamp,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct DockerPruneRequest {
186 pub operation_id: OperationId,
188 pub operation: OperationName,
190 pub fingerprint: DockerPruneFingerprint,
192 pub force: bool,
194 pub deadline: Timestamp,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct ImageRemovalReceipt {
201 pub send_state: MutationSendState,
203 pub deleted: Vec<String>,
205 pub untagged: Vec<String>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct DockerPruneScopeReceipt {
212 pub target: DockerPruneTarget,
214 pub deleted: Vec<String>,
216 pub space_reclaimed: u64,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct DockerPruneReceipt {
223 pub send_state: MutationSendState,
225 pub scopes: Vec<DockerPruneScopeReceipt>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct ImageRemovalOutcome {
232 pub before: ImageRemovalFingerprint,
234 pub removed: bool,
236 pub receipt: ImageRemovalReceipt,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct DockerPruneOutcome {
243 pub before: DockerPruneFingerprint,
245 pub after: DockerPruneFingerprint,
247 pub receipt: DockerPruneReceipt,
249 pub changed: bool,
251}
252
253#[async_trait]
255pub trait DockerCleanupMutator: Send + Sync {
256 async fn remove_image(
258 &self,
259 host: &HostRecord,
260 request: &ImageRemovalRequest,
261 cancellation: &CancellationToken,
262 ) -> MutationResult<ImageRemovalReceipt>;
263
264 async fn prune(
266 &self,
267 host: &HostRecord,
268 request: &DockerPruneRequest,
269 cancellation: &CancellationToken,
270 ) -> MutationResult<DockerPruneReceipt>;
271}
272
273pub trait DockerCleanupClient:
275 ImageReader
276 + ContainerReader
277 + NetworkReader
278 + VolumeReader
279 + DockerTelemetryReader
280 + DockerCleanupMutator
281{
282}
283
284impl<T> DockerCleanupClient for T where
285 T: ImageReader
286 + ContainerReader
287 + NetworkReader
288 + VolumeReader
289 + DockerTelemetryReader
290 + DockerCleanupMutator
291{
292}
293
294#[async_trait]
296pub trait DockerCleanupClientProvider: Send + Sync {
297 async fn cleanup_client(
299 &self,
300 host: &HostRecord,
301 cancellation: &CancellationToken,
302 ) -> InfraResult<Arc<dyn DockerCleanupClient>>;
303}
304
305fn validate_text(field: &'static str, value: String, max: usize) -> InfraResult<String> {
306 let count = value.chars().count();
307 if count == 0 || count > max || value.chars().any(char::is_control) {
308 Err(InfraError::InvalidRequest {
309 domain: "docker-cleanup",
310 message: format!("invalid {field}"),
311 })
312 } else {
313 Ok(value)
314 }
315}
316
317#[cfg(test)]
318#[path = "docker_cleanup_tests.rs"]
319mod tests;