From 7535f821a7749370ce086bc4582007da925f3e72 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 22 Mar 2022 20:54:51 -0400 Subject: [PATCH] container: Add manifest/config to image query, add `image history` CLI Extend our image state struct to include the manifest and image configuration (if available, only in v1). Add a `container image history` CLI verb which prints it. --- lib/Cargo.toml | 1 + lib/src/cli.rs | 71 ++++++++++++++++++++++++++++++++++++++ lib/src/container/store.rs | 38 ++++++++++++-------- 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index dc24fdde..f7c763e0 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -39,6 +39,7 @@ serde_json = "1.0.64" structopt = "0.3.21" tar = "0.4.38" tempfile = "3.2.0" +term_size = "0.3.2" tokio = { features = ["full"], version = "1" } tokio-util = { features = ["io-util"], version = "0.6.9" } tokio-stream = { features = ["sync"], version = "0.1.8" } diff --git a/lib/src/cli.rs b/lib/src/cli.rs index fffa2a56..a7651a8b 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -188,6 +188,17 @@ enum ContainerImageOpts { proxyopts: ContainerProxyOpts, }, + /// Pull (or update) a container image. + History { + /// Path to the repository + #[structopt(long, parse(try_from_str = parse_repo))] + repo: ostree::Repo, + + /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest + #[structopt(parse(try_from_str = parse_imgref))] + imgref: OstreeImageReference, + }, + /// Copy a pulled container image from one repo to another. Copy { /// Path to the source repository @@ -467,6 +478,63 @@ async fn container_store( Ok(()) } +fn print_column(s: &str, clen: usize, remaining: &mut usize) { + let l = s.len().min(*remaining); + print!("{}", &s[0..l]); + if clen > 0 { + // We always want two trailing spaces + let pad = clen.saturating_sub(l) + 2; + for _ in 0..pad { + print!(" "); + } + *remaining = remaining.checked_sub(l + pad).unwrap(); + } +} + +/// Output the container image history +async fn container_history(repo: &ostree::Repo, imgref: &OstreeImageReference) -> Result<()> { + let img = crate::container::store::query_image(repo, imgref)? + .ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?; + let columns = [("ID", 20), ("SIZE", 10), ("CREATED BY", 0usize)]; + let width = term_size::dimensions().map(|x| x.0).unwrap_or(80); + if let Some(config) = img.configuration.as_ref() { + let mut history = config.history().iter(); + let mut layers = img.manifest.layers().iter(); + + { + let mut remaining = width; + for (name, width) in columns.iter() { + print_column(name, *width as usize, &mut remaining); + } + println!(); + } + + while let Some(v) = layers.next() { + let histent = history.next(); + let created_by = histent + .map(|s| s.created_by().as_deref()) + .flatten() + .unwrap_or(""); + + let mut remaining = width; + + let digest = v.digest().as_str(); + // Verify it's OK to slice, this should all be ASCII + assert!(digest.chars().all(|c| c.is_ascii())); + let digest_max = columns[0].1; + let digest = &digest[0..digest_max]; + print_column(digest, digest_max, &mut remaining); + let size = glib::format_size(v.size() as u64); + print_column(size.as_str(), columns[1].1, &mut remaining); + print_column(created_by, columns[2].1, &mut remaining); + println!(); + } + Ok(()) + } else { + anyhow::bail!("v0 image does not have fetched configuration"); + } +} + /// Add IMA signatures to an ostree commit, generating a new commit. fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> { let signopts = crate::ima::ImaOpts { @@ -550,6 +618,9 @@ where imgref, proxyopts, } => container_store(&repo, &imgref, proxyopts).await, + ContainerImageOpts::History { repo, imgref } => { + container_history(&repo, &imgref).await + } ContainerImageOpts::Copy { src_repo, dest_repo, diff --git a/lib/src/container/store.rs b/lib/src/container/store.rs index 0eff1688..9da84901 100644 --- a/lib/src/container/store.rs +++ b/lib/src/container/store.rs @@ -65,6 +65,10 @@ pub struct LayeredImageState { pub is_layered: bool, /// The digest of the original manifest pub manifest_digest: String, + /// The image manfiest + pub manifest: ImageManifest, + /// The image configuration; for v0 images, may not be available. + pub configuration: Option, } impl LayeredImageState { @@ -208,6 +212,16 @@ fn manifest_data_from_commitmeta( Ok((r, digest)) } +fn image_config_from_commitmeta( + commit_meta: &glib::VariantDict, +) -> Result> { + commit_meta + .lookup::(META_CONFIG)? + .filter(|v| v != "null") // Format v0 apparently old versions injected `null` here sadly... + .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg)) + .transpose() +} + /// Return the original digest of the manifest stored in the commit metadata. /// This will be a string of the form e.g. `sha256:`. /// @@ -293,15 +307,13 @@ impl ImageImporter { // Query for previous stored state let (previous_manifest_digest, previous_imageid) = - if let Some((previous_manifest, previous_state)) = - query_image_impl(&self.repo, &self.imgref)? - { + if let Some(previous_state) = query_image(&self.repo, &self.imgref)? { // If the manifest digests match, we're done. if previous_state.manifest_digest == manifest_digest { return Ok(PrepareResult::AlreadyPresent(previous_state)); } // Failing that, if they have the same imageID, we're also done. - let previous_imageid = previous_manifest.config().digest().as_str(); + let previous_imageid = previous_state.manifest.config().digest().as_str(); if previous_imageid == new_imageid { return Ok(PrepareResult::AlreadyPresent(previous_state)); } @@ -598,10 +610,11 @@ pub fn list_images(repo: &ostree::Repo) -> Result> { .collect() } -fn query_image_impl( +/// Query metadata for a pulled image. +pub fn query_image( repo: &ostree::Repo, imgref: &OstreeImageReference, -) -> Result> { +) -> Result> { let ostree_ref = &ref_for_image(&imgref.imgref)?; let merge_rev = repo.resolve_rev(ostree_ref, true)?; let (merge_commit, merge_commit_obj) = if let Some(r) = merge_rev { @@ -612,6 +625,7 @@ fn query_image_impl( let commit_meta = &merge_commit_obj.child_value(0); let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta)); let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?; + let configuration = image_config_from_commitmeta(commit_meta)?; let mut layers = manifest.layers().iter().cloned(); // We require a base layer. let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?; @@ -626,17 +640,11 @@ fn query_image_impl( merge_commit, is_layered, manifest_digest, + manifest, + configuration, }; tracing::debug!(state = ?state); - Ok(Some((manifest, state))) -} - -/// Query metadata for a pulled image. -pub fn query_image( - repo: &ostree::Repo, - imgref: &OstreeImageReference, -) -> Result> { - Ok(query_image_impl(repo, imgref)?.map(|v| v.1)) + Ok(Some(state)) } /// Copy a downloaded image from one repository to another.