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}