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#[derive(Debug, Clone)]
12pub struct RegistryConfig {
13 pub server: String,
15 pub username: Option<String>,
17 pub password: Option<String>,
19 pub email: Option<String>,
21}
22
23impl RegistryConfig {
24 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 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 pub fn custom(server: String) -> Self {
46 Self {
47 server,
48 username: None,
49 password: None,
50 email: None,
51 }
52 }
53
54 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 pub fn with_email(mut self, email: String) -> Self {
63 self.email = Some(email);
64 self
65 }
66
67 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#[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
89pub 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 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 debug!("Registry authentication configured for {}", config.server);
117
118 Ok(())
119 }
120
121 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 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 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 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 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 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 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 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 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}