lmrc_docker/images/
mod.rs

1//! Image management operations.
2
3use crate::DockerClient;
4use crate::error::{DockerError, Result};
5use bollard::auth::DockerCredentials;
6use bollard::image::*;
7use bollard::models::*;
8use futures_util::StreamExt;
9use std::collections::HashMap;
10use tracing::{debug, info, warn};
11
12mod builder;
13pub use builder::*;
14
15/// Image operations manager.
16pub struct Images<'a> {
17    client: &'a DockerClient,
18}
19
20impl<'a> Images<'a> {
21    pub(crate) fn new(client: &'a DockerClient) -> Self {
22        Self { client }
23    }
24
25    /// Create a new image builder for building images.
26    ///
27    /// # Example
28    ///
29    /// ```no_run
30    /// use lmrc_docker::DockerClient;
31    /// use std::path::Path;
32    ///
33    /// #[tokio::main]
34    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
35    ///     let client = DockerClient::new()?;
36    ///
37    ///     client.images()
38    ///         .build("my-app:latest")
39    ///         .dockerfile("Dockerfile")
40    ///         .context(Path::new("."))
41    ///         .build_arg("RUNTIME_IMAGE", "alpine:latest")
42    ///         .execute()
43    ///         .await?;
44    ///
45    ///     Ok(())
46    /// }
47    /// ```
48    pub fn build(&self, tag: impl Into<String>) -> ImageBuilder<'a> {
49        ImageBuilder::new(self.client, tag)
50    }
51
52    /// Get a reference to a specific image.
53    pub fn get(&self, name_or_id: impl Into<String>) -> ImageRef<'a> {
54        ImageRef::new(self.client, name_or_id.into())
55    }
56
57    /// List all images.
58    ///
59    /// # Example
60    ///
61    /// ```no_run
62    /// use lmrc_docker::DockerClient;
63    ///
64    /// #[tokio::main]
65    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
66    ///     let client = DockerClient::new()?;
67    ///     let images = client.images().list(true).await?;
68    ///     for image in images {
69    ///         println!("{:?}", image.repo_tags);
70    ///     }
71    ///     Ok(())
72    /// }
73    /// ```
74    pub async fn list(&self, all: bool) -> Result<Vec<ImageSummary>> {
75        let options = Some(ListImagesOptions::<String> {
76            all,
77            ..Default::default()
78        });
79
80        self.client
81            .docker
82            .list_images(options)
83            .await
84            .map_err(|e| DockerError::Other(format!("Failed to list images: {}", e)))
85    }
86
87    /// Pull an image from a registry.
88    ///
89    /// # Arguments
90    ///
91    /// * `image` - Image name with optional tag (e.g., "nginx:latest")
92    /// * `credentials` - Optional Docker registry credentials
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// use lmrc_docker::DockerClient;
98    ///
99    /// #[tokio::main]
100    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
101    ///     let client = DockerClient::new()?;
102    ///     client.images().pull("nginx:latest", None).await?;
103    ///     Ok(())
104    /// }
105    /// ```
106    pub async fn pull(
107        &self,
108        image: impl Into<String>,
109        credentials: Option<DockerCredentials>,
110    ) -> Result<()> {
111        let image = image.into();
112        info!("Pulling image: {}", image);
113
114        let options = Some(CreateImageOptions::<String> {
115            from_image: image.clone(),
116            ..Default::default()
117        });
118
119        let mut stream = self.client.docker.create_image(options, None, credentials);
120
121        while let Some(result) = stream.next().await {
122            match result {
123                Ok(info) => {
124                    if let Some(status) = info.status {
125                        debug!("Pull status: {}", status);
126                    }
127                    if let Some(error) = info.error {
128                        return Err(DockerError::PullFailed(error));
129                    }
130                }
131                Err(e) => {
132                    return Err(DockerError::PullFailed(e.to_string()));
133                }
134            }
135        }
136
137        info!("Successfully pulled image: {}", image);
138        Ok(())
139    }
140
141    /// Search for images in a registry.
142    ///
143    /// # Example
144    ///
145    /// ```no_run
146    /// use lmrc_docker::DockerClient;
147    ///
148    /// #[tokio::main]
149    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
150    ///     let client = DockerClient::new()?;
151    ///     let results = client.images().search("nginx", None).await?;
152    ///     for result in results {
153    ///         println!("{}: {}", result.name.unwrap_or_default(), result.description.unwrap_or_default());
154    ///     }
155    ///     Ok(())
156    /// }
157    /// ```
158    pub async fn search(
159        &self,
160        term: impl Into<String>,
161        limit: Option<i64>,
162    ) -> Result<Vec<ImageSearchResponseItem>> {
163        let options = SearchImagesOptions {
164            term: term.into(),
165            limit: limit.map(|l| l as u64),
166            ..Default::default()
167        };
168
169        self.client
170            .docker
171            .search_images(options)
172            .await
173            .map_err(|e| DockerError::Other(format!("Failed to search images: {}", e)))
174    }
175
176    /// Prune unused images.
177    ///
178    /// # Arguments
179    ///
180    /// * `dangling_only` - If true, only remove dangling images (untagged)
181    pub async fn prune(&self, dangling_only: bool) -> Result<ImagePruneResponse> {
182        info!("Pruning unused images...");
183        let mut filters = HashMap::new();
184        if dangling_only {
185            filters.insert("dangling", vec!["true"]);
186        }
187
188        let options = Some(PruneImagesOptions { filters });
189
190        self.client
191            .docker
192            .prune_images(options)
193            .await
194            .map_err(|e| DockerError::Other(format!("Failed to prune images: {}", e)))
195    }
196}
197
198/// Reference to a specific image.
199pub struct ImageRef<'a> {
200    client: &'a DockerClient,
201    name: String,
202}
203
204impl<'a> ImageRef<'a> {
205    pub(crate) fn new(client: &'a DockerClient, name: String) -> Self {
206        Self { client, name }
207    }
208
209    /// Get the image name.
210    pub fn name(&self) -> &str {
211        &self.name
212    }
213
214    /// Inspect the image to get detailed information.
215    ///
216    /// # Example
217    ///
218    /// ```no_run
219    /// use lmrc_docker::DockerClient;
220    ///
221    /// #[tokio::main]
222    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
223    ///     let client = DockerClient::new()?;
224    ///     let info = client.images().get("nginx:latest").inspect().await?;
225    ///     println!("Image ID: {:?}", info.id);
226    ///     Ok(())
227    /// }
228    /// ```
229    pub async fn inspect(&self) -> Result<ImageInspect> {
230        debug!("Inspecting image: {}", self.name);
231        self.client
232            .docker
233            .inspect_image(&self.name)
234            .await
235            .map_err(|e| DockerError::ImageNotFound(format!("{}: {}", self.name, e)))
236    }
237
238    /// Get the history of the image (layer information).
239    pub async fn history(&self) -> Result<Vec<HistoryResponseItem>> {
240        debug!("Getting history for image: {}", self.name);
241        self.client
242            .docker
243            .image_history(&self.name)
244            .await
245            .map_err(|e| DockerError::ImageNotFound(format!("{}: {}", self.name, e)))
246    }
247
248    /// Tag the image with a new name/tag.
249    ///
250    /// # Example
251    ///
252    /// ```no_run
253    /// use lmrc_docker::DockerClient;
254    ///
255    /// #[tokio::main]
256    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
257    ///     let client = DockerClient::new()?;
258    ///     client.images()
259    ///         .get("nginx:latest")
260    ///         .tag("my-nginx:v1.0")
261    ///         .await?;
262    ///     Ok(())
263    /// }
264    /// ```
265    pub async fn tag(&self, new_tag: impl Into<String>) -> Result<()> {
266        let new_tag = new_tag.into();
267        info!("Tagging image {} as {}", self.name, new_tag);
268
269        // Parse the new tag to extract repo and tag
270        let parts: Vec<&str> = new_tag.rsplitn(2, ':').collect();
271        let (repo, tag) = if parts.len() == 2 {
272            (parts[1], Some(parts[0]))
273        } else {
274            (new_tag.as_str(), None)
275        };
276
277        let options = TagImageOptions {
278            repo,
279            tag: tag.unwrap_or("latest"),
280        };
281
282        self.client
283            .docker
284            .tag_image(&self.name, Some(options))
285            .await
286            .map_err(|e| DockerError::Other(format!("Failed to tag image: {}", e)))
287    }
288
289    /// Push the image to a registry.
290    ///
291    /// # Arguments
292    ///
293    /// * `credentials` - Optional Docker registry credentials
294    ///
295    /// # Example
296    ///
297    /// ```no_run
298    /// use lmrc_docker::DockerClient;
299    ///
300    /// #[tokio::main]
301    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
302    ///     let client = DockerClient::new()?;
303    ///     client.images()
304    ///         .get("myregistry.com/my-app:v1.0")
305    ///         .push(None)
306    ///         .await?;
307    ///     Ok(())
308    /// }
309    /// ```
310    pub async fn push(&self, credentials: Option<DockerCredentials>) -> Result<()> {
311        info!("Pushing image: {}", self.name);
312
313        // Parse tag from image name
314        let tag = self.name.split(':').nth(1).unwrap_or("latest");
315
316        let options = Some(PushImageOptions { tag });
317
318        let mut stream = self
319            .client
320            .docker
321            .push_image(&self.name, options, credentials);
322
323        while let Some(result) = stream.next().await {
324            match result {
325                Ok(info) => {
326                    if let Some(status) = info.status {
327                        if status.contains("error") || status.contains("Error") {
328                            warn!("Push error: {}", status);
329                        } else {
330                            debug!("Push status: {}", status);
331                        }
332                    }
333                    if let Some(error) = info.error {
334                        return Err(DockerError::PushFailed(error));
335                    }
336                }
337                Err(e) => {
338                    return Err(DockerError::PushFailed(e.to_string()));
339                }
340            }
341        }
342
343        info!("Successfully pushed image: {}", self.name);
344        Ok(())
345    }
346
347    /// Remove the image.
348    ///
349    /// # Arguments
350    ///
351    /// * `force` - Force removal even if image is in use
352    /// * `noprune` - Do not delete untagged parent images
353    pub async fn remove(&self, force: bool, noprune: bool) -> Result<Vec<ImageDeleteResponseItem>> {
354        info!("Removing image: {}", self.name);
355
356        let options = Some(RemoveImageOptions { force, noprune });
357
358        self.client
359            .docker
360            .remove_image(&self.name, options, None)
361            .await
362            .map_err(|e| DockerError::Other(format!("Failed to remove image: {}", e)))
363    }
364
365    /// Export the image as a tar archive.
366    pub async fn export(&self) -> Result<Vec<u8>> {
367        info!("Exporting image: {}", self.name);
368
369        let mut stream = self.client.docker.export_image(&self.name);
370        let mut data = Vec::new();
371
372        while let Some(chunk) = stream.next().await {
373            match chunk {
374                Ok(bytes) => data.extend_from_slice(&bytes),
375                Err(e) => {
376                    return Err(DockerError::Other(format!("Failed to export image: {}", e)));
377                }
378            }
379        }
380
381        Ok(data)
382    }
383}
384
385impl DockerClient {
386    /// Access image operations.
387    ///
388    /// # Example
389    ///
390    /// ```no_run
391    /// use lmrc_docker::DockerClient;
392    ///
393    /// #[tokio::main]
394    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
395    ///     let client = DockerClient::new()?;
396    ///     let images = client.images().list(false).await?;
397    ///     Ok(())
398    /// }
399    /// ```
400    pub fn images(&self) -> Images<'_> {
401        Images::new(self)
402    }
403}