use anyhow::{bail, Context, Result};
use bollard::auth::DockerCredentials;
use bollard::models::CreateImageInfo;
use bollard::query_parameters::CreateImageOptions;
use bollard::Docker;
use futures_util::StreamExt;
use crate::config::model::RegistryAuth;
pub fn parse_image_ref(image: &str) -> (&str, &str) {
match image.rsplit_once(':') {
Some((name, tag)) if !name.is_empty() && !tag.is_empty() => (name, tag),
_ => (image, "latest"),
}
}
pub async fn check_image_exists(docker: &Docker, image: &str) -> bool {
docker.inspect_image(image).await.is_ok()
}
pub async fn pull_image(docker: &Docker, image: &str) -> Result<()> {
let (name, tag) = parse_image_ref(image);
tracing::debug!(image = %image, "pulling image");
let options = CreateImageOptions {
from_image: Some(name.to_string()),
tag: Some(tag.to_string()),
..Default::default()
};
let mut stream = docker.create_image(Some(options), None, None);
while let Some(result) = stream.next().await {
let info: CreateImageInfo = result.context("pulling image")?;
if let Some(err) = &info.error_detail {
bail!("image pull failed for {}: {:?}", image, err);
}
}
tracing::debug!(image = %image, "image pulled successfully");
Ok(())
}
pub async fn pull_image_with_auth(
docker: &Docker,
image: &str,
auth: Option<&RegistryAuth>,
) -> Result<()> {
let (name, tag) = parse_image_ref(image);
tracing::debug!(image = %image, "pulling image");
let options = CreateImageOptions {
from_image: Some(name.to_string()),
tag: Some(tag.to_string()),
..Default::default()
};
let credentials = auth.map(|a| DockerCredentials {
username: Some(a.username.clone()),
password: Some(a.password.clone()),
..Default::default()
});
let mut stream = docker.create_image(Some(options), None, credentials);
while let Some(result) = stream.next().await {
let info: CreateImageInfo = result.context("pulling image")?;
if let Some(err) = &info.error_detail {
bail!("image pull failed for {}: {:?}", image, err);
}
}
tracing::debug!(image = %image, "image pulled successfully");
Ok(())
}
pub async fn pull_images_if_needed(docker: &Docker, images: &[&str]) -> Result<()> {
let mut set = tokio::task::JoinSet::new();
for &image in images {
let docker = docker.clone();
let image = image.to_string();
set.spawn(async move {
if check_image_exists(&docker, &image).await {
tracing::debug!(image = %image, "image already present locally");
return Ok(());
}
pull_image(&docker, &image).await
});
}
while let Some(result) = set.join_next().await {
result.context("image pull task panicked")??;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_image_with_tag() {
assert_eq!(parse_image_ref("postgres:16"), ("postgres", "16"));
}
#[test]
fn parse_image_without_tag() {
assert_eq!(parse_image_ref("redis"), ("redis", "latest"));
}
#[test]
fn parse_image_with_org_and_tag() {
assert_eq!(
parse_image_ref("axllent/mailpit:latest"),
("axllent/mailpit", "latest")
);
}
#[test]
fn parse_image_alpine() {
assert_eq!(
parse_image_ref("postgres:16-alpine"),
("postgres", "16-alpine")
);
}
}