lmrc_docker/registry/
mod.rs

1use crate::DockerClient;
2use crate::error::{DockerError, Result};
3use bollard::auth::DockerCredentials;
4use bollard::image::{ListImagesOptions, SearchImagesOptions};
5use bollard::models::ImageSummary;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use tracing::{debug, info};
9
10/// Registry configuration and authentication
11#[derive(Debug, Clone)]
12pub struct RegistryConfig {
13    /// Registry server address (e.g., "docker.io", "ghcr.io")
14    pub server: String,
15    /// Username for authentication
16    pub username: Option<String>,
17    /// Password or token for authentication
18    pub password: Option<String>,
19    /// Email (optional, for some registries)
20    pub email: Option<String>,
21}
22
23impl RegistryConfig {
24    /// Create a new registry config for Docker Hub
25    pub fn docker_hub(username: String, password: String) -> Self {
26        Self {
27            server: "docker.io".to_string(),
28            username: Some(username),
29            password: Some(password),
30            email: None,
31        }
32    }
33
34    /// Create a new registry config for GitHub Container Registry
35    pub fn github(username: String, token: String) -> Self {
36        Self {
37            server: "ghcr.io".to_string(),
38            username: Some(username),
39            password: Some(token),
40            email: None,
41        }
42    }
43
44    /// Create a custom registry config
45    pub fn custom(server: String) -> Self {
46        Self {
47            server,
48            username: None,
49            password: None,
50            email: None,
51        }
52    }
53
54    /// Set authentication credentials
55    pub fn with_auth(mut self, username: String, password: String) -> Self {
56        self.username = Some(username);
57        self.password = Some(password);
58        self
59    }
60
61    /// Set email
62    pub fn with_email(mut self, email: String) -> Self {
63        self.email = Some(email);
64        self
65    }
66
67    /// Convert to DockerCredentials for bollard
68    pub(crate) fn to_credentials(&self) -> DockerCredentials {
69        DockerCredentials {
70            username: self.username.clone(),
71            password: self.password.clone(),
72            email: self.email.clone(),
73            serveraddress: Some(self.server.clone()),
74            ..Default::default()
75        }
76    }
77}
78
79/// Image search result
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ImageSearchResult {
82    pub name: String,
83    pub description: Option<String>,
84    pub star_count: Option<i64>,
85    pub is_official: bool,
86    pub is_automated: bool,
87}
88
89/// Registry operations
90pub struct Registry<'a> {
91    client: &'a DockerClient,
92}
93
94impl<'a> Registry<'a> {
95    pub(crate) fn new(client: &'a DockerClient) -> Self {
96        Self { client }
97    }
98
99    /// Authenticate with a registry
100    ///
101    /// This is typically not needed for public pulls, but required for:
102    /// - Pushing images
103    /// - Pulling private images
104    /// - Accessing registries with rate limits
105    pub async fn login(&self, config: &RegistryConfig) -> Result<()> {
106        info!("Logging in to registry: {}", config.server);
107
108        let _credentials = config.to_credentials();
109
110        // Verify credentials by attempting to access the registry
111        // Docker doesn't have a dedicated "login" endpoint in the API
112        // Instead, credentials are stored and used with subsequent operations
113
114        // We can verify the login by attempting to search (works for Docker Hub)
115        // For other registries, the credentials will be validated on first use
116        debug!("Registry authentication configured for {}", config.server);
117
118        Ok(())
119    }
120
121    /// Search for images in a registry
122    ///
123    /// # Arguments
124    /// * `term` - Search term
125    /// * `limit` - Maximum number of results (default: 25)
126    ///
127    /// # Example
128    /// ```no_run
129    /// # use lmrc_docker::DockerClient;
130    /// # #[tokio::main]
131    /// # async fn main() -> lmrc_docker::Result<()> {
132    /// let client = DockerClient::new()?;
133    /// let results = client.registry().search("nginx", Some(10)).await?;
134    /// for result in results {
135    ///     println!("{}: {}", result.name, result.description.unwrap_or_default());
136    /// }
137    /// # Ok(())
138    /// # }
139    /// ```
140    pub async fn search(&self, term: &str, limit: Option<i64>) -> Result<Vec<ImageSearchResult>> {
141        info!("Searching for images: {}", term);
142
143        let mut filters = HashMap::new();
144        filters.insert("term".to_string(), vec![term.to_string()]);
145
146        let options = SearchImagesOptions {
147            term: term.to_string(),
148            limit: Some(limit.unwrap_or(25) as u64),
149            filters,
150        };
151
152        let results = self
153            .client
154            .docker
155            .search_images(options)
156            .await
157            .map_err(|e| DockerError::ImageOperationFailed(format!("Search failed: {}", e)))?;
158
159        Ok(results
160            .into_iter()
161            .map(|r| ImageSearchResult {
162                name: r.name.unwrap_or_default(),
163                description: r.description,
164                star_count: r.star_count,
165                is_official: r.is_official.unwrap_or(false),
166                is_automated: r.is_automated.unwrap_or(false),
167            })
168            .collect())
169    }
170
171    /// Check if an image exists in the local registry cache
172    pub async fn image_exists_locally(&self, image: &str) -> Result<bool> {
173        debug!("Checking if image exists locally: {}", image);
174
175        let mut filters = HashMap::new();
176        filters.insert("reference".to_string(), vec![image.to_string()]);
177
178        let options = ListImagesOptions {
179            filters,
180            ..Default::default()
181        };
182
183        let images = self
184            .client
185            .docker
186            .list_images(Some(options))
187            .await
188            .map_err(|e| {
189                DockerError::ImageOperationFailed(format!("Failed to list images: {}", e))
190            })?;
191
192        Ok(!images.is_empty())
193    }
194
195    /// List all local images
196    pub async fn list_local_images(&self, all: bool) -> Result<Vec<ImageSummary>> {
197        debug!("Listing local images (all: {})", all);
198
199        let options = ListImagesOptions::<String> {
200            all,
201            ..Default::default()
202        };
203
204        self.client
205            .docker
206            .list_images(Some(options))
207            .await
208            .map_err(|e| DockerError::ImageOperationFailed(format!("Failed to list images: {}", e)))
209    }
210
211    /// Get detailed information about a local image
212    pub async fn inspect_image(&self, image: &str) -> Result<bollard::models::ImageInspect> {
213        info!("Inspecting image: {}", image);
214
215        self.client
216            .docker
217            .inspect_image(image)
218            .await
219            .map_err(|_e| DockerError::ImageNotFound(image.to_string()))
220    }
221
222    /// Remove an image from local cache
223    ///
224    /// # Arguments
225    /// * `image` - Image name or ID
226    /// * `force` - Force removal even if image is in use
227    /// * `noprune` - Do not delete untagged parents
228    pub async fn remove_image(&self, image: &str, force: bool, noprune: bool) -> Result<()> {
229        info!(
230            "Removing image: {} (force: {}, noprune: {})",
231            image, force, noprune
232        );
233
234        use bollard::image::RemoveImageOptions;
235
236        let options = RemoveImageOptions { force, noprune };
237
238        self.client
239            .docker
240            .remove_image(image, Some(options), None)
241            .await
242            .map_err(|e| {
243                DockerError::ImageOperationFailed(format!("Failed to remove image: {}", e))
244            })?;
245
246        Ok(())
247    }
248
249    /// Tag an image with a new name/tag
250    ///
251    /// # Arguments
252    /// * `source` - Source image name
253    /// * `target_repo` - Target repository name
254    /// * `target_tag` - Target tag (default: "latest")
255    pub async fn tag_image(
256        &self,
257        source: &str,
258        target_repo: &str,
259        target_tag: Option<&str>,
260    ) -> Result<()> {
261        let tag = target_tag.unwrap_or("latest");
262        info!("Tagging image {} as {}:{}", source, target_repo, tag);
263
264        use bollard::image::TagImageOptions;
265
266        let options = TagImageOptions {
267            repo: target_repo.to_string(),
268            tag: tag.to_string(),
269        };
270
271        self.client
272            .docker
273            .tag_image(source, Some(options))
274            .await
275            .map_err(|e| {
276                DockerError::ImageOperationFailed(format!("Failed to tag image: {}", e))
277            })?;
278
279        Ok(())
280    }
281
282    /// Prune unused images
283    ///
284    /// # Arguments
285    /// * `dangling_only` - Only remove dangling images (untagged)
286    ///
287    /// # Returns
288    /// Number of bytes reclaimed
289    pub async fn prune_images(&self, dangling_only: bool) -> Result<u64> {
290        info!("Pruning images (dangling_only: {})", dangling_only);
291
292        use bollard::image::PruneImagesOptions;
293
294        let mut filters = HashMap::new();
295        if dangling_only {
296            filters.insert("dangling".to_string(), vec!["true".to_string()]);
297        }
298
299        let options = PruneImagesOptions { filters };
300
301        let result = self
302            .client
303            .docker
304            .prune_images(Some(options))
305            .await
306            .map_err(|e| {
307                DockerError::ImageOperationFailed(format!("Failed to prune images: {}", e))
308            })?;
309
310        let space_reclaimed = result.space_reclaimed.unwrap_or(0) as u64;
311        info!("Reclaimed {} bytes", space_reclaimed);
312
313        Ok(space_reclaimed)
314    }
315
316    /// Export an image to a tar archive
317    pub async fn export_image(&self, image: &str) -> Result<Vec<u8>> {
318        info!("Exporting image: {}", image);
319
320        use futures_util::TryStreamExt;
321
322        let mut stream = self.client.docker.export_image(image);
323        let mut data = Vec::new();
324
325        while let Some(chunk) = stream.try_next().await.map_err(|e| {
326            DockerError::ImageOperationFailed(format!("Failed to export image: {}", e))
327        })? {
328            data.extend_from_slice(chunk.as_ref());
329        }
330
331        info!("Exported {} bytes", data.len());
332        Ok(data)
333    }
334
335    /// Import an image from a tar archive
336    pub async fn import_image(&self, tar_data: Vec<u8>, tag: Option<&str>) -> Result<String> {
337        info!("Importing image (tag: {:?})", tag);
338
339        use bollard::image::ImportImageOptions;
340        use bytes::Bytes;
341        use futures_util::TryStreamExt;
342        use http_body_util::Full;
343
344        let options = ImportImageOptions {
345            ..Default::default()
346        };
347
348        let body = Full::new(Bytes::from(tar_data));
349        let body = http_body_util::Either::Left(body);
350
351        let mut stream = self.client.docker.import_image(options, body, None);
352
353        let mut image_id = String::new();
354
355        while let Some(info) = stream.try_next().await.map_err(|e| {
356            DockerError::ImageOperationFailed(format!("Failed to import image: {}", e))
357        })? {
358            if let Some(id) = info.id {
359                image_id = id;
360            }
361        }
362
363        info!("Imported image: {}", image_id);
364        Ok(image_id)
365    }
366}