use anyhow::{anyhow, Context, Result};
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
use super::{naming, Source};
use crate::notifier::Notifier;
pub struct DockerSource;
impl DockerSource {
pub fn new() -> Result<Self> {
Ok(Self)
}
fn run_command(&self, args: &[&str]) -> Result<String> {
let output = Command::new("docker")
.args(args)
.output()
.context(format!("Failed to execute docker command: {args:?}"))?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Docker command failed: {error}"));
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
Ok(stdout)
}
fn image_exists(&self, image_name: &str) -> bool {
Command::new("docker")
.args(["image", "inspect", image_name])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn pull_image(&self, image_name: &str, notifier: &Notifier) -> Result<()> {
notifier.info(&format!("Pulling Docker image '{image_name}'..."));
let output = Command::new("docker")
.args(["pull", image_name])
.output()
.context("Failed to execute docker pull command")?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Docker pull failed: {error}"));
}
notifier.info(&format!("Successfully pulled Docker image '{image_name}'"));
Ok(())
}
}
impl Source for DockerSource {
fn name(&self) -> &str {
"docker"
}
fn get_image_tarball(
&self,
image_name: &str,
notifier: &Notifier,
) -> Result<(PathBuf, Option<TempDir>)> {
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
let tarball_path = temp_dir.path().join("image.tar");
notifier.info(&format!(
"Exporting Docker image '{image_name}' to tarball..."
));
let save_result =
self.run_command(&["save", "-o", tarball_path.to_str().unwrap(), image_name]);
match save_result {
Ok(_) => {
Ok((tarball_path, Some(temp_dir)))
}
Err(e) => {
if !self.image_exists(image_name) {
notifier.info(&format!(
"Image '{image_name}' not found locally, attempting to pull..."
));
self.pull_image(image_name, notifier)
.context(format!("Failed to pull image '{image_name}'"))?;
notifier.info(&format!(
"Retrying export of Docker image '{image_name}' to tarball..."
));
self.run_command(&["save", "-o", tarball_path.to_str().unwrap(), image_name])
.context(format!("Failed to save image '{image_name}' after pull"))?;
Ok((tarball_path, Some(temp_dir)))
} else {
Err(e)
}
}
}
}
fn branch_name(&self, image_name: &str, os_arch: &str, image_digest: &str) -> String {
let base_branch = naming::container_image_to_branch(image_name);
naming::combine_branch_with_digest(&base_branch, os_arch, image_digest)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_docker_source_branch_name() {
let source = DockerSource::new().unwrap();
assert_eq!(
source.branch_name(
"hello-world:latest",
"linux-amd64",
"sha256:1234567890abcdef"
),
"hello-world#latest#linux-amd64#1234567890ab"
);
assert_eq!(
source.branch_name("hello-world", "linux-arm64", "sha256:1234567890abcdef"),
"hello-world#latest#linux-arm64#1234567890ab"
);
assert_eq!(
source.branch_name("nginx/nginx:1.21", "linux-amd64", "sha256:9876543210fedcba"),
"nginx-nginx#1.21#linux-amd64#9876543210fe"
);
assert_eq!(
source.branch_name("nginx", "windows-amd64", "sha256:abcdef123456789"),
"nginx#latest#windows-amd64#abcdef123456"
);
assert_eq!(
source.branch_name("nginx", "linux-amd64", "abcdef123456789"),
"nginx#latest#linux-amd64#abcdef123456789"
);
}
}