diff --git a/crates/composefs-ctl/src/lib.rs b/crates/composefs-ctl/src/lib.rs index d6783da2..e5f910d3 100644 --- a/crates/composefs-ctl/src/lib.rs +++ b/crates/composefs-ctl/src/lib.rs @@ -48,7 +48,6 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; use clap::{Parser, Subcommand, ValueEnum}; use clap_complete::engine::ArgValueCompleter; -#[cfg(any(feature = "oci", feature = "ostree"))] use comfy_table::{Table, presets::UTF8_FULL}; #[cfg(feature = "ostree")] use complete::complete_ostree_refs; @@ -726,6 +725,16 @@ enum Command { }, /// Imports a composefs image (unsafe!) ImportImage { reference: String }, + /// List all named image references in the repository + #[clap(name = "images", alias = "list-images")] + Images { + /// Output as JSON array + #[clap(long)] + json: bool, + /// Show full digest instead of truncated form + #[clap(long)] + no_trunc: bool, + }, /// Commands for dealing with OCI images and layers #[cfg(feature = "oci")] Oci { @@ -1904,6 +1913,31 @@ where } => { mount_opts.mount_image(&repo, &name, &mountpoint)?; } + Command::Images { json, no_trunc } => { + let reply = + varlink::run_list_image_refs(&repo).map_err(|e| anyhow::anyhow!("{e:?}"))?; + + if json { + serde_json::to_writer_pretty(std::io::stdout().lock(), &reply)?; + println!(); + } else if reply.images.is_empty() { + println!("No images found"); + } else { + let mut table = Table::new(); + table.load_preset(UTF8_FULL); + table.set_header(["NAME", "DIGEST"]); + + for entry in &reply.images { + let digest_display = if !no_trunc && entry.digest.len() > 12 { + &entry.digest[..12] + } else { + &entry.digest + }; + table.add_row([entry.name.as_str(), digest_display]); + } + println!("{table}"); + } + } Command::ImageObjects { name } => { let objects = repo.objects_for_image(&name)?; for object in objects { diff --git a/crates/composefs-ctl/src/varlink.rs b/crates/composefs-ctl/src/varlink.rs index fedff358..62c0e69c 100644 --- a/crates/composefs-ctl/src/varlink.rs +++ b/crates/composefs-ctl/src/varlink.rs @@ -407,6 +407,41 @@ async fn run_image_objects( Ok(ImageObjectsReply { object_ids }) } +/// A single image reference entry. +#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)] +pub struct ImageRefEntry { + /// The reference name. + pub name: String, + /// The fs-verity digest the reference points to. + pub digest: String, +} + +/// Reply listing all named image references in the repository. +#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)] +pub struct ListImageRefsReply { + /// The image references. + pub images: Vec, +} + +/// Collect all named image references from the repository. +pub fn run_list_image_refs( + repo: &Repository, +) -> std::result::Result { + let refs = repo + .list_image_refs("") + .map_err(|e| RepositoryError::InternalError { + message: format!("{e:#}"), + })?; + let images = refs + .into_iter() + .map(|(name, target)| { + let digest = target.rsplit('/').next().unwrap_or(&target).to_string(); + ImageRefEntry { name, digest } + }) + .collect(); + Ok(ListImageRefsReply { images }) +} + /// Options for a `Mount` call. All fields are optional for forward /// compatibility — new mount options can be added without breaking the /// wire format. @@ -743,9 +778,10 @@ mod service_impl { #![allow(missing_docs)] use super::{ - CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, MountParams, - MountReply, OpenRepo, OpenRepositoryReply, RepositoryError, run_fsck, run_gc, - run_image_objects, run_init_repository, run_mount, + CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, + ListImageRefsReply, MountParams, MountReply, OpenRepo, OpenRepositoryReply, + RepositoryError, run_fsck, run_gc, run_image_objects, run_init_repository, + run_list_image_refs, run_mount, }; use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue}; @@ -852,6 +888,17 @@ mod service_impl { } } + /// List all named image references in the repository. + async fn list_image_refs( + &self, + handle: u64, + ) -> std::result::Result { + match self.lookup_repo(handle)? { + OpenRepo::Sha256(ref r) => run_list_image_refs::(r), + OpenRepo::Sha512(ref r) => run_list_image_refs::(r), + } + } + /// Create a detached mount of an image and return the mount fd. /// /// If overlay upper/work directories are needed, pass them as two fds @@ -898,10 +945,11 @@ mod service_impl { parse_local_fetch, pull_stream, }; use super::{ - CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, MountParams, - MountReply, OpenRepo, OpenRepositoryReply, RepositoryError, run_compute_id, run_fsck, - run_gc, run_image_objects, run_init_repository, run_inspect, run_list_images, run_mount, - run_oci_fsck, run_oci_mount, run_tag, run_untag, + CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, + ListImageRefsReply, MountParams, MountReply, OpenRepo, OpenRepositoryReply, + RepositoryError, run_compute_id, run_fsck, run_gc, run_image_objects, run_init_repository, + run_inspect, run_list_image_refs, run_list_images, run_mount, run_oci_fsck, run_oci_mount, + run_tag, run_untag, }; use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue}; @@ -1010,6 +1058,17 @@ mod service_impl { } } + /// List all named image references in the repository. + async fn list_image_refs( + &self, + handle: u64, + ) -> std::result::Result { + match self.lookup_repo(handle)? { + OpenRepo::Sha256(ref r) => run_list_image_refs::(r), + OpenRepo::Sha512(ref r) => run_list_image_refs::(r), + } + } + /// Create a detached mount of an image and return the mount fd. /// /// If overlay upper/work directories are needed, pass them as two fds