Skip to main content

ironflow_ops_docker/images/
manage.rs

1//! Image management: list, pull, push, build, inspect, remove.
2
3use std::collections::HashMap;
4
5use async_trait::async_trait;
6use bollard::Docker;
7use bollard::query_parameters::{
8    CreateImageOptions, ListImagesOptions, PushImageOptions, RemoveImageOptions,
9};
10use ironflow_core::error::OperationError;
11use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use tokio_stream::StreamExt;
15
16use crate::containers::DockerRef;
17use crate::helpers::{docker_error, to_value};
18
19// ---------------------------------------------------------------------------
20// ImageList
21// ---------------------------------------------------------------------------
22
23/// A single entry in the image list.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ImageListEntry {
26    /// Image ID.
27    pub id: String,
28    /// Repository tags.
29    pub repo_tags: Vec<String>,
30    /// Image size in bytes.
31    pub size: i64,
32    /// Creation timestamp.
33    pub created: i64,
34}
35
36/// Output of an image list.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ImageListOutput {
39    /// The images.
40    pub images: Vec<ImageListEntry>,
41}
42
43/// List local images.
44///
45/// # Examples
46///
47/// ```no_run
48/// use ironflow_ops_docker::images::ImageList;
49/// use ironflow_ops_docker::DockerClient;
50/// use ironflow_core::operation::Operation;
51///
52/// let client = DockerClient::connect_local().unwrap();
53/// let op = ImageList::new(&client);
54/// assert_eq!(op.kind(), "docker");
55/// ```
56pub struct ImageList {
57    docker: Docker,
58    all: bool,
59    filters: HashMap<String, Vec<String>>,
60}
61
62impl ImageList {
63    /// Create a new image-list operation.
64    pub fn new(client: impl Into<DockerRef>) -> Self {
65        Self {
66            docker: client.into().0,
67            all: false,
68            filters: HashMap::new(),
69        }
70    }
71
72    /// Include intermediate images.
73    pub fn all(mut self) -> Self {
74        self.all = true;
75        self
76    }
77
78    /// Add a filter.
79    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80        self.filters
81            .entry(key.into())
82            .or_default()
83            .push(value.into());
84        self
85    }
86
87    /// Execute and return a typed result.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`OperationError::External`] if the Docker daemon is
92    /// unreachable.
93    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageListOutput, OperationError> {
94        let options = ListImagesOptions {
95            all: self.all,
96            filters: Some(self.filters.clone()),
97            ..Default::default()
98        };
99        let images = self
100            .docker
101            .list_images(Some(options))
102            .await
103            .map_err(docker_error)?;
104        let entries = images
105            .into_iter()
106            .map(|i| ImageListEntry {
107                id: i.id,
108                repo_tags: i.repo_tags,
109                size: i.size,
110                created: i.created,
111            })
112            .collect();
113        Ok(ImageListOutput { images: entries })
114    }
115}
116
117#[async_trait]
118impl Operation for ImageList {
119    fn kind(&self) -> &str {
120        "docker"
121    }
122
123    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
124        to_value(&self.run(ctx).await?)
125    }
126
127    fn input(&self) -> Option<Value> {
128        Some(serde_json::json!({
129            "operation": "image_list",
130            "all": self.all,
131        }))
132    }
133}
134
135impl TypedOperation for ImageList {
136    type Output = ImageListOutput;
137}
138
139// ---------------------------------------------------------------------------
140// ImagePull
141// ---------------------------------------------------------------------------
142
143/// Output of an image pull.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ImagePullOutput {
146    /// The pulled image reference.
147    pub image: String,
148}
149
150/// Pull an image from a registry.
151///
152/// # Examples
153///
154/// ```no_run
155/// use ironflow_ops_docker::images::ImagePull;
156/// use ironflow_ops_docker::DockerClient;
157/// use ironflow_core::operation::Operation;
158///
159/// let client = DockerClient::connect_local().unwrap();
160/// let op = ImagePull::new(&client, "alpine:latest");
161/// assert_eq!(op.kind(), "docker");
162/// ```
163pub struct ImagePull {
164    docker: Docker,
165    image: String,
166}
167
168impl ImagePull {
169    /// Create a new image-pull operation.
170    pub fn new(client: impl Into<DockerRef>, image: impl Into<String>) -> Self {
171        Self {
172            docker: client.into().0,
173            image: image.into(),
174        }
175    }
176
177    /// Execute and return a typed result.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`OperationError::External`] if the image cannot be pulled.
182    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImagePullOutput, OperationError> {
183        let options = CreateImageOptions {
184            from_image: Some(self.image.clone()),
185            ..Default::default()
186        };
187        let mut stream = self.docker.create_image(Some(options), None, None);
188        while let Some(result) = stream.next().await {
189            result.map_err(docker_error)?;
190        }
191        Ok(ImagePullOutput {
192            image: self.image.clone(),
193        })
194    }
195}
196
197#[async_trait]
198impl Operation for ImagePull {
199    fn kind(&self) -> &str {
200        "docker"
201    }
202
203    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
204        to_value(&self.run(ctx).await?)
205    }
206
207    fn input(&self) -> Option<Value> {
208        Some(serde_json::json!({
209            "operation": "image_pull",
210            "image": self.image,
211        }))
212    }
213}
214
215impl TypedOperation for ImagePull {
216    type Output = ImagePullOutput;
217}
218
219// ---------------------------------------------------------------------------
220// ImagePush
221// ---------------------------------------------------------------------------
222
223/// Output of an image push.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ImagePushOutput {
226    /// The pushed image reference.
227    pub image: String,
228}
229
230/// Push an image to a registry.
231///
232/// # Examples
233///
234/// ```no_run
235/// use ironflow_ops_docker::images::ImagePush;
236/// use ironflow_ops_docker::DockerClient;
237/// use ironflow_core::operation::Operation;
238///
239/// let client = DockerClient::connect_local().unwrap();
240/// let op = ImagePush::new(&client, "my-image:latest");
241/// assert_eq!(op.kind(), "docker");
242/// ```
243pub struct ImagePush {
244    docker: Docker,
245    image: String,
246    tag: Option<String>,
247}
248
249impl ImagePush {
250    /// Create a new image-push operation.
251    pub fn new(client: impl Into<DockerRef>, image: impl Into<String>) -> Self {
252        Self {
253            docker: client.into().0,
254            image: image.into(),
255            tag: None,
256        }
257    }
258
259    /// Set the tag to push.
260    pub fn tag(mut self, tag: impl Into<String>) -> Self {
261        self.tag = Some(tag.into());
262        self
263    }
264
265    /// Execute and return a typed result.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`OperationError::External`] if the push fails.
270    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImagePushOutput, OperationError> {
271        let options = PushImageOptions {
272            tag: Some(self.tag.clone().unwrap_or_else(|| "latest".to_string())),
273            platform: None,
274        };
275        let mut stream = self.docker.push_image(&self.image, Some(options), None);
276        while let Some(result) = stream.next().await {
277            result.map_err(docker_error)?;
278        }
279        Ok(ImagePushOutput {
280            image: self.image.clone(),
281        })
282    }
283}
284
285#[async_trait]
286impl Operation for ImagePush {
287    fn kind(&self) -> &str {
288        "docker"
289    }
290
291    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
292        to_value(&self.run(ctx).await?)
293    }
294
295    fn input(&self) -> Option<Value> {
296        Some(serde_json::json!({
297            "operation": "image_push",
298            "image": self.image,
299        }))
300    }
301}
302
303impl TypedOperation for ImagePush {
304    type Output = ImagePushOutput;
305}
306
307// ---------------------------------------------------------------------------
308// ImageInspect
309// ---------------------------------------------------------------------------
310
311/// Output of an image inspection.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct ImageInspectOutput {
314    /// The full inspection response as JSON.
315    pub data: Value,
316}
317
318/// Inspect an image.
319///
320/// # Examples
321///
322/// ```no_run
323/// use ironflow_ops_docker::images::ImageInspect;
324/// use ironflow_ops_docker::DockerClient;
325/// use ironflow_core::operation::Operation;
326///
327/// let client = DockerClient::connect_local().unwrap();
328/// let op = ImageInspect::new(&client, "alpine:latest");
329/// assert_eq!(op.kind(), "docker");
330/// ```
331pub struct ImageInspect {
332    docker: Docker,
333    image: String,
334}
335
336impl ImageInspect {
337    /// Create a new image-inspect operation.
338    pub fn new(client: impl Into<DockerRef>, image: impl Into<String>) -> Self {
339        Self {
340            docker: client.into().0,
341            image: image.into(),
342        }
343    }
344
345    /// Execute and return a typed result.
346    ///
347    /// # Errors
348    ///
349    /// Returns [`OperationError::External`] if the image does not exist.
350    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageInspectOutput, OperationError> {
351        let response = self
352            .docker
353            .inspect_image(&self.image)
354            .await
355            .map_err(docker_error)?;
356        let data = serde_json::to_value(&response).map_err(|e| OperationError::External {
357            origin: "docker".to_string(),
358            message: e.to_string(),
359        })?;
360        Ok(ImageInspectOutput { data })
361    }
362}
363
364#[async_trait]
365impl Operation for ImageInspect {
366    fn kind(&self) -> &str {
367        "docker"
368    }
369
370    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
371        to_value(&self.run(ctx).await?)
372    }
373
374    fn input(&self) -> Option<Value> {
375        Some(serde_json::json!({
376            "operation": "image_inspect",
377            "image": self.image,
378        }))
379    }
380}
381
382impl TypedOperation for ImageInspect {
383    type Output = ImageInspectOutput;
384}
385
386// ---------------------------------------------------------------------------
387// ImageRemove
388// ---------------------------------------------------------------------------
389
390/// Output of an image removal.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ImageRemoveOutput {
393    /// The removed image reference.
394    pub image: String,
395}
396
397/// Remove an image.
398///
399/// # Examples
400///
401/// ```no_run
402/// use ironflow_ops_docker::images::ImageRemove;
403/// use ironflow_ops_docker::DockerClient;
404/// use ironflow_core::operation::Operation;
405///
406/// let client = DockerClient::connect_local().unwrap();
407/// let op = ImageRemove::new(&client, "alpine:latest");
408/// assert_eq!(op.kind(), "docker");
409/// ```
410pub struct ImageRemove {
411    docker: Docker,
412    image: String,
413    force: bool,
414    no_prune: bool,
415}
416
417impl ImageRemove {
418    /// Create a new image-remove operation.
419    pub fn new(client: impl Into<DockerRef>, image: impl Into<String>) -> Self {
420        Self {
421            docker: client.into().0,
422            image: image.into(),
423            force: false,
424            no_prune: false,
425        }
426    }
427
428    /// Force-remove the image.
429    pub fn force(mut self) -> Self {
430        self.force = true;
431        self
432    }
433
434    /// Do not delete untagged parent images.
435    pub fn no_prune(mut self) -> Self {
436        self.no_prune = true;
437        self
438    }
439
440    /// Execute and return a typed result.
441    ///
442    /// # Errors
443    ///
444    /// Returns [`OperationError::External`] if the image does not exist.
445    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageRemoveOutput, OperationError> {
446        let options = RemoveImageOptions {
447            force: self.force,
448            noprune: self.no_prune,
449            platforms: None,
450        };
451        self.docker
452            .remove_image(&self.image, Some(options), None)
453            .await
454            .map_err(docker_error)?;
455        Ok(ImageRemoveOutput {
456            image: self.image.clone(),
457        })
458    }
459}
460
461#[async_trait]
462impl Operation for ImageRemove {
463    fn kind(&self) -> &str {
464        "docker"
465    }
466
467    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
468        to_value(&self.run(ctx).await?)
469    }
470
471    fn input(&self) -> Option<Value> {
472        Some(serde_json::json!({
473            "operation": "image_remove",
474            "image": self.image,
475        }))
476    }
477}
478
479impl TypedOperation for ImageRemove {
480    type Output = ImageRemoveOutput;
481}