Skip to main content

ironflow_ops_docker/images/
cleanup.rs

1//! Image cleanup operations: prune.
2
3use std::collections::HashMap;
4
5use async_trait::async_trait;
6use bollard::Docker;
7use bollard::query_parameters::PruneImagesOptions;
8use ironflow_core::error::OperationError;
9use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::containers::DockerRef;
14use crate::helpers::{docker_error, to_value};
15
16// ---------------------------------------------------------------------------
17// ImagePrune
18// ---------------------------------------------------------------------------
19
20/// Output of pruning unused images.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ImagePruneOutput {
23    /// Deleted image IDs.
24    pub images_deleted: Vec<String>,
25    /// Disk space reclaimed in bytes.
26    pub space_reclaimed: u64,
27}
28
29/// Remove unused images.
30///
31/// # Examples
32///
33/// ```no_run
34/// use ironflow_ops_docker::images::ImagePrune;
35/// use ironflow_ops_docker::DockerClient;
36/// use ironflow_core::operation::Operation;
37///
38/// let client = DockerClient::connect_local().unwrap();
39/// let op = ImagePrune::new(&client);
40/// assert_eq!(op.kind(), "docker");
41/// ```
42pub struct ImagePrune {
43    docker: Docker,
44    filters: HashMap<String, Vec<String>>,
45}
46
47impl ImagePrune {
48    /// Create a new image-prune operation.
49    pub fn new(client: impl Into<DockerRef>) -> Self {
50        Self {
51            docker: client.into().0,
52            filters: HashMap::new(),
53        }
54    }
55
56    /// Add a filter.
57    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
58        self.filters
59            .entry(key.into())
60            .or_default()
61            .push(value.into());
62        self
63    }
64
65    /// Execute and return a typed result.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`OperationError::External`] if the Docker daemon is
70    /// unreachable.
71    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImagePruneOutput, OperationError> {
72        let options = PruneImagesOptions {
73            filters: Some(self.filters.clone()),
74        };
75        let response = self
76            .docker
77            .prune_images(Some(options))
78            .await
79            .map_err(docker_error)?;
80        let images_deleted = response
81            .images_deleted
82            .unwrap_or_default()
83            .into_iter()
84            .filter_map(|i| i.deleted.or(i.untagged))
85            .collect();
86        Ok(ImagePruneOutput {
87            images_deleted,
88            space_reclaimed: response.space_reclaimed.unwrap_or(0) as u64,
89        })
90    }
91}
92
93#[async_trait]
94impl Operation for ImagePrune {
95    fn kind(&self) -> &str {
96        "docker"
97    }
98
99    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
100        to_value(&self.run(ctx).await?)
101    }
102
103    fn input(&self) -> Option<Value> {
104        Some(serde_json::json!({
105            "operation": "image_prune",
106        }))
107    }
108}
109
110impl TypedOperation for ImagePrune {
111    type Output = ImagePruneOutput;
112}