#![warn(missing_docs)]
use bollard::query_parameters::{
BuildImageOptions, InspectImageOptions, ListImagesOptions, RemoveImageOptions, CreateImageOptions,
};
use bollard::Docker as BollardDocker;
use clap::{Arg, ArgAction, Command};
use docker_types::{DockerError, ImageInfo};
use futures_util::stream::StreamExt;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::time::SystemTime;
#[derive(Debug, Serialize, Deserialize)]
pub struct ToolConfig {
pub log_level: String,
pub debug: bool,
pub config_path: String,
}
pub type Result<T> = std::result::Result<T, DockerError>;
pub fn create_base_command(name: &'static str, about: &'static str) -> Command {
Command::new(name)
.about(about)
.version("0.1.0")
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::Count)
.help("增加日志详细程度"),
)
.arg(
Arg::new("config")
.short('c')
.long("config")
.value_name("CONFIG")
.help("指定配置文件路径"),
)
.arg(
Arg::new("debug")
.long("debug")
.action(ArgAction::SetTrue)
.help("启用调试模式"),
)
}
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<ToolConfig> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
if !path.exists() {
return Err(DockerError::config_missing(path_str.clone()));
}
let content = fs::read_to_string(path)
.map_err(|e| DockerError::config_invalid(path_str.clone(), e.to_string()))?;
serde_json::from_str(&content).map_err(|e| DockerError::config_invalid(path_str.clone(), e.to_string()))
}
pub fn get_default_config() -> ToolConfig {
ToolConfig {
log_level: "info".to_string(),
debug: false,
config_path: ".docker/config.json".to_string(),
}
}
pub fn init_logger(verbose: u8, debug: bool) {
let log_level = if debug {
"debug"
} else {
match verbose {
0 => "info",
1 => "debug",
_ => "trace",
}
};
tracing_subscriber::fmt()
.with_env_filter(format!("{}={}", env!("CARGO_PKG_NAME"), log_level))
.init();
}
pub fn handle_common_args(cmd: Command) -> Result<(ToolConfig, bool)> {
let matches = cmd.get_matches();
let verbose = matches.get_count("verbose");
let debug = matches.get_flag("debug");
let config_path = matches
.get_one::<String>("config")
.map(|s| s.to_string())
.unwrap_or_else(|| get_default_config().config_path);
let config = if Path::new(&config_path).exists() {
load_config(&config_path)?
} else {
get_default_config()
};
init_logger(verbose, debug);
Ok((config, debug))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Image {
pub id: String,
pub name: String,
pub tags: Vec<String>,
pub size: u64,
pub created_at: SystemTime,
pub architecture: String,
pub os: String,
}
pub struct ImageManager {
docker: BollardDocker,
}
impl ImageManager {
pub fn new() -> Self {
let docker = BollardDocker::connect_with_defaults().expect("Failed to connect to Docker");
Self { docker }
}
pub async fn build_image(
&self,
context: &str,
tag: &str,
dockerfile: Option<&str>,
no_cache: bool,
target: Option<&str>,
) -> Result<ImageInfo> {
use futures_util::StreamExt;
let options = BuildImageOptions {
t: Some(tag.to_string()),
dockerfile: dockerfile.unwrap_or_default().to_string(),
nocache: no_cache,
target: target.unwrap_or_default().to_string(),
..Default::default()
};
let context_dir = Path::new(context);
let mut tar = tar::Builder::new(Vec::new());
tar.append_dir_all(".", context_dir).map_err(|e| {
DockerError::container_error(format!("Failed to create build context: {:?}", e))
})?;
let _tar_data = tar.into_inner().map_err(|e| {
DockerError::container_error(format!("Failed to create build context: {:?}", e))
})?;
let mut stream = self.docker.build_image(options, None, None);
while let Some(chunk) = stream.next().await {
match chunk {
Ok(chunk) => {
if let Some(error_detail) = chunk.error_detail {
return Err(DockerError::container_error(error_detail.message.unwrap_or_default()));
}
}
Err(e) => {
return Err(DockerError::container_error(format!(
"Failed to build image: {:?}",
e
)));
}
}
}
let image_name = tag.split(':').next().unwrap_or("unknown");
let image_tag = tag.split(':').nth(1).unwrap_or("latest");
self.inspect_image(&format!("{}:{}", image_name, image_tag))
.await
}
pub async fn list_images(&self) -> Result<Vec<ImageInfo>> {
let options = ListImagesOptions {
all: true,
..Default::default()
};
let images =
self.docker.list_images(Some(options)).await.map_err(|e| {
DockerError::container_error(format!("Failed to list images: {:?}", e))
})?;
let images: Vec<ImageInfo> = images
.into_iter()
.map(|image| {
let repo_tags = image.repo_tags;
let default_tag = "<none>:<none>".to_string();
let first_tag = repo_tags.first().unwrap_or(&default_tag);
let parts: Vec<&str> = first_tag.split(':').collect();
let name = if parts.len() > 1 && parts[0] != "<none>" {
parts[0].to_string()
} else {
"<none>".to_string()
};
ImageInfo {
id: image.id,
name,
tags: repo_tags,
size: image.size as u64,
created_at: SystemTime::now(), architecture: "amd64".to_string(), os: "linux".to_string(), }
})
.collect();
Ok(images)
}
pub async fn remove_image(&self, image_id: &str) -> Result<()> {
let options = RemoveImageOptions {
force: true,
..Default::default()
};
self.docker
.remove_image(image_id, Some(options), None)
.await
.map_err(|e| {
DockerError::container_error(format!("Failed to remove image: {:?}", e))
})?;
Ok(())
}
pub async fn pull_image(&self, name: &str, tag: &str) -> Result<ImageInfo> {
use futures_util::StreamExt;
let image_ref = format!("{}:{}", name, tag);
let options = CreateImageOptions {
from_image: Some(image_ref.clone()),
..Default::default()
};
let mut stream = self.docker.create_image(Some(options), None, None);
while let Some(chunk) = stream.next().await {
match chunk {
Ok(chunk) => {
if let Some(error_detail) = chunk.error_detail {
return Err(DockerError::container_error(error_detail.message.unwrap_or_default()));
}
}
Err(e) => {
return Err(DockerError::container_error(format!(
"Failed to pull image: {:?}",
e
)));
}
}
}
self.inspect_image(&image_ref).await
}
pub async fn inspect_image(&self, image_id: &str) -> Result<ImageInfo> {
let _options = InspectImageOptions {
..Default::default()
};
let image = self
.docker
.inspect_image(image_id)
.await
.map_err(|e| {
DockerError::not_found("image", format!("Failed to inspect image: {:?}", e))
})?;
let repo_tags = image.repo_tags.unwrap_or_default();
let default_tag = "<none>:<none>".to_string();
let first_tag = repo_tags.first().unwrap_or(&default_tag);
let parts: Vec<&str> = first_tag.split(':').collect();
let name = if parts.len() > 1 && parts[0] != "<none>" {
parts[0].to_string()
} else {
"<none>".to_string()
};
Ok(ImageInfo {
id: image.id.unwrap_or_default(),
name,
tags: repo_tags,
size: image.size.unwrap_or(0) as u64,
created_at: SystemTime::now(), architecture: image.architecture.unwrap_or_default(),
os: image.os.unwrap_or_default(),
})
}
}