use crate::compose::types::ComposeFile;
use crate::error::{ComposeError, Result};
use crate::libpod::types::image::ImageInspect;
use crate::libpod::{urlencoded, API_PREFIX};
use crate::units::{format_bytes, format_duration, DurationFormat, SizeFormat};
use super::inspect_util::split_repo_tag;
use super::Engine;
const SIZE_FORMAT: SizeFormat = SizeFormat::decimal().with_significant(3);
const AGE_FORMAT: DurationFormat = DurationFormat::default_parts();
struct ImageRow {
service: String,
repository: String,
tag: String,
id: String,
size: u64,
created: String,
}
impl Engine {
pub async fn images(&self, file: &ComposeFile) -> Result<()> {
self.images_with_options(file, super::ImagesOptions::default())
.await
}
pub async fn images_with_options(
&self,
file: &ComposeFile,
opts: super::ImagesOptions,
) -> Result<()> {
self.images_with_services(file, &[], opts).await
}
pub async fn images_with_services(
&self,
file: &ComposeFile,
target_services: &[String],
opts: super::ImagesOptions,
) -> Result<()> {
for name in target_services {
if !file.services.contains_key(name) {
return Err(ComposeError::ServiceNotFound(name.clone()));
}
}
let mut rows: Vec<ImageRow> = Vec::new();
for (name, service) in &file.services {
if !target_services.is_empty() && !target_services.iter().any(|t| t == name) {
continue;
}
let image_ref = match (&service.image, &service.build) {
(Some(img), _) => img.clone(),
(None, Some(build)) => {
super::super::build::primary_build_tag(&self.project, name, None, build.tags())
}
(None, None) => continue,
};
let (repository, tag) = split_repo_tag(&image_ref);
let path = format!("{API_PREFIX}/images/{}/json", urlencoded(&image_ref));
match self.client.get_json::<ImageInspect>(&path).await {
Ok(img) => {
let id = img.id.trim_start_matches("sha256:").get(..12).unwrap_or("");
rows.push(ImageRow {
service: name.clone(),
repository,
tag,
id: id.to_string(),
size: img.size,
created: img.created,
});
}
Err(e) if e.is_status(404) => {
tracing::debug!("images {name}: not present ({e})");
rows.push(ImageRow {
service: name.clone(),
repository,
tag,
id: String::new(),
size: 0,
created: String::new(),
});
}
Err(e) => return Err(ComposeError::Podman(e)),
}
}
if opts.quiet {
let mut seen = std::collections::HashSet::new();
for row in &rows {
if !row.id.is_empty() && seen.insert(row.id.as_str()) {
println!("{}", row.id);
}
}
return Ok(());
}
if opts.json {
let json: Vec<_> = rows
.iter()
.map(|row| {
serde_json::json!({
"Service": row.service,
"Repository": row.repository,
"Tag": row.tag,
"ID": row.id,
"Size": row.size,
"Created": row.created,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
return Ok(());
}
let now = super::ps::now_unix();
let mut table = crate::ui::Table::new(&[
"SERVICE",
"REPOSITORY",
"TAG",
"IMAGE ID",
"SIZE",
"CREATED",
])
.cap(0, 48)
.cap(1, 48)
.cap(2, 24)
.identity_col(0);
for row in &rows {
table.push(vec![
row.service.clone(),
row.repository.clone(),
row.tag.clone(),
row.id.clone(),
size_cell(row.size),
age_cell(&row.created, now),
]);
}
table.print();
Ok(())
}
}
fn size_cell(size: u64) -> String {
if size == 0 {
return String::new();
}
format_bytes(size, &SIZE_FORMAT)
}
fn age_cell(created: &str, now: i64) -> String {
let Some(built) = crate::timestamp::parse_rfc3339(created) else {
return String::new();
};
let elapsed = now.saturating_sub(built).max(0);
format_duration(std::time::Duration::from_secs(elapsed as u64), &AGE_FORMAT)
}
#[cfg(test)]
#[path = "images_tests.rs"]
mod tests;