Skip to content

Commit

Permalink
container: Add manifest/config to image query, add image history CLI
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cgwalters committed Mar 25, 2022
1 parent 3c0993f commit 7535f82
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 15 deletions.
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
71 changes: 71 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 23 additions & 15 deletions lib/src/container/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImageConfiguration>,
}

impl LayeredImageState {
Expand Down Expand Up @@ -208,6 +212,16 @@ fn manifest_data_from_commitmeta(
Ok((r, digest))
}

fn image_config_from_commitmeta(
commit_meta: &glib::VariantDict,
) -> Result<Option<ImageConfiguration>> {
commit_meta
.lookup::<String>(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:<digest>`.
///
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -598,10 +610,11 @@ pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
.collect()
}

fn query_image_impl(
/// Query metadata for a pulled image.
pub fn query_image(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
) -> Result<Option<(ImageManifest, LayeredImageState)>> {
) -> Result<Option<LayeredImageState>> {
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 {
Expand All @@ -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"))?;
Expand All @@ -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<Option<LayeredImageState>> {
Ok(query_image_impl(repo, imgref)?.map(|v| v.1))
Ok(Some(state))
}

/// Copy a downloaded image from one repository to another.
Expand Down

0 comments on commit 7535f82

Please sign in to comment.