use crate::DockerClient;
use crate::error::{DockerError, Result};
use bollard::auth::DockerCredentials;
use bollard::image::{ListImagesOptions, SearchImagesOptions};
use bollard::models::ImageSummary;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct RegistryConfig {
pub server: String,
pub username: Option<String>,
pub password: Option<String>,
pub email: Option<String>,
}
impl RegistryConfig {
pub fn docker_hub(username: String, password: String) -> Self {
Self {
server: "docker.io".to_string(),
username: Some(username),
password: Some(password),
email: None,
}
}
pub fn github(username: String, token: String) -> Self {
Self {
server: "ghcr.io".to_string(),
username: Some(username),
password: Some(token),
email: None,
}
}
pub fn custom(server: String) -> Self {
Self {
server,
username: None,
password: None,
email: None,
}
}
pub fn with_auth(mut self, username: String, password: String) -> Self {
self.username = Some(username);
self.password = Some(password);
self
}
pub fn with_email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
pub(crate) fn to_credentials(&self) -> DockerCredentials {
DockerCredentials {
username: self.username.clone(),
password: self.password.clone(),
email: self.email.clone(),
serveraddress: Some(self.server.clone()),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageSearchResult {
pub name: String,
pub description: Option<String>,
pub star_count: Option<i64>,
pub is_official: bool,
pub is_automated: bool,
}
pub struct Registry<'a> {
client: &'a DockerClient,
}
impl<'a> Registry<'a> {
pub(crate) fn new(client: &'a DockerClient) -> Self {
Self { client }
}
pub async fn login(&self, config: &RegistryConfig) -> Result<()> {
info!("Logging in to registry: {}", config.server);
let _credentials = config.to_credentials();
debug!("Registry authentication configured for {}", config.server);
Ok(())
}
pub async fn search(&self, term: &str, limit: Option<i64>) -> Result<Vec<ImageSearchResult>> {
info!("Searching for images: {}", term);
let mut filters = HashMap::new();
filters.insert("term".to_string(), vec![term.to_string()]);
let options = SearchImagesOptions {
term: term.to_string(),
limit: Some(limit.unwrap_or(25) as u64),
filters,
};
let results = self
.client
.docker
.search_images(options)
.await
.map_err(|e| DockerError::ImageOperationFailed(format!("Search failed: {}", e)))?;
Ok(results
.into_iter()
.map(|r| ImageSearchResult {
name: r.name.unwrap_or_default(),
description: r.description,
star_count: r.star_count,
is_official: r.is_official.unwrap_or(false),
is_automated: r.is_automated.unwrap_or(false),
})
.collect())
}
pub async fn image_exists_locally(&self, image: &str) -> Result<bool> {
debug!("Checking if image exists locally: {}", image);
let mut filters = HashMap::new();
filters.insert("reference".to_string(), vec![image.to_string()]);
let options = ListImagesOptions {
filters,
..Default::default()
};
let images = self
.client
.docker
.list_images(Some(options))
.await
.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to list images: {}", e))
})?;
Ok(!images.is_empty())
}
pub async fn list_local_images(&self, all: bool) -> Result<Vec<ImageSummary>> {
debug!("Listing local images (all: {})", all);
let options = ListImagesOptions::<String> {
all,
..Default::default()
};
self.client
.docker
.list_images(Some(options))
.await
.map_err(|e| DockerError::ImageOperationFailed(format!("Failed to list images: {}", e)))
}
pub async fn inspect_image(&self, image: &str) -> Result<bollard::models::ImageInspect> {
info!("Inspecting image: {}", image);
self.client
.docker
.inspect_image(image)
.await
.map_err(|_e| DockerError::ImageNotFound(image.to_string()))
}
pub async fn remove_image(&self, image: &str, force: bool, noprune: bool) -> Result<()> {
info!(
"Removing image: {} (force: {}, noprune: {})",
image, force, noprune
);
use bollard::image::RemoveImageOptions;
let options = RemoveImageOptions { force, noprune };
self.client
.docker
.remove_image(image, Some(options), None)
.await
.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to remove image: {}", e))
})?;
Ok(())
}
pub async fn tag_image(
&self,
source: &str,
target_repo: &str,
target_tag: Option<&str>,
) -> Result<()> {
let tag = target_tag.unwrap_or("latest");
info!("Tagging image {} as {}:{}", source, target_repo, tag);
use bollard::image::TagImageOptions;
let options = TagImageOptions {
repo: target_repo.to_string(),
tag: tag.to_string(),
};
self.client
.docker
.tag_image(source, Some(options))
.await
.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to tag image: {}", e))
})?;
Ok(())
}
pub async fn prune_images(&self, dangling_only: bool) -> Result<u64> {
info!("Pruning images (dangling_only: {})", dangling_only);
use bollard::image::PruneImagesOptions;
let mut filters = HashMap::new();
if dangling_only {
filters.insert("dangling".to_string(), vec!["true".to_string()]);
}
let options = PruneImagesOptions { filters };
let result = self
.client
.docker
.prune_images(Some(options))
.await
.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to prune images: {}", e))
})?;
let space_reclaimed = result.space_reclaimed.unwrap_or(0) as u64;
info!("Reclaimed {} bytes", space_reclaimed);
Ok(space_reclaimed)
}
pub async fn export_image(&self, image: &str) -> Result<Vec<u8>> {
info!("Exporting image: {}", image);
use futures_util::TryStreamExt;
let mut stream = self.client.docker.export_image(image);
let mut data = Vec::new();
while let Some(chunk) = stream.try_next().await.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to export image: {}", e))
})? {
data.extend_from_slice(chunk.as_ref());
}
info!("Exported {} bytes", data.len());
Ok(data)
}
pub async fn import_image(&self, tar_data: Vec<u8>, tag: Option<&str>) -> Result<String> {
info!("Importing image (tag: {:?})", tag);
use bollard::image::ImportImageOptions;
use bytes::Bytes;
use futures_util::TryStreamExt;
use http_body_util::Full;
let options = ImportImageOptions {
..Default::default()
};
let body = Full::new(Bytes::from(tar_data));
let body = http_body_util::Either::Left(body);
let mut stream = self.client.docker.import_image(options, body, None);
let mut image_id = String::new();
while let Some(info) = stream.try_next().await.map_err(|e| {
DockerError::ImageOperationFailed(format!("Failed to import image: {}", e))
})? {
if let Some(id) = info.id {
image_id = id;
}
}
info!("Imported image: {}", image_id);
Ok(image_id)
}
}