From d0747541a71ce303209d886266a5cb4a105e9e97 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 3 Feb 2022 11:03:07 -0500 Subject: [PATCH] container/encapsulate: Support copying commit metadata keys For https://github.com/coreos/coreos-assembler/issues/2685 we want to copy e.g. `rpmostree.input-hash` into the container image. Extend the `ExportOpts` struct to support this, and also expose it via the CLI, e.g. `ostree container encapsulate --copymeta=rpmostree.input-hash ...`. And while I was thinking about this...we should by default copy some core ostree keys, such as `ostree.bootable` and `ostree.linux` since they are key pieces of metadata. --- lib/src/cli.rs | 15 +++++++++--- lib/src/container/encapsulate.rs | 39 +++++++++++++++++++++++++++++--- lib/tests/it/main.rs | 13 +++++++++-- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index dbb24a3d..596e708f 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -110,6 +110,10 @@ enum ContainerOpts { #[structopt(name = "label", long, short)] labels: Vec, + /// Propagate an OSTree commit metadata key to container label + #[structopt(name = "copymeta", long)] + copy_meta_keys: Vec, + /// Corresponds to the Dockerfile `CMD` instruction. #[structopt(long)] cmd: Option>, @@ -365,6 +369,7 @@ async fn container_export( rev: &str, imgref: &ImageReference, labels: BTreeMap, + copy_meta_keys: Vec, cmd: Option>, ) -> Result<()> { let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repo, gio::NONE_CANCELLABLE)?; @@ -372,8 +377,11 @@ async fn container_export( labels: Some(labels), cmd, }; - let opts = Some(Default::default()); - let pushed = crate::container::encapsulate(repo, rev, &config, opts, imgref).await?; + let opts = crate::container::ExportOpts { + copy_meta_keys, + ..Default::default() + }; + let pushed = crate::container::encapsulate(repo, rev, &config, Some(opts), imgref).await?; println!("{}", pushed); Ok(()) } @@ -492,6 +500,7 @@ where rev, imgref, labels, + copy_meta_keys, cmd, } => { let labels: Result> = labels @@ -503,7 +512,7 @@ where Ok((k.to_string(), v.to_string())) }) .collect(); - container_export(&repo, &rev, &imgref, labels?, cmd).await + container_export(&repo, &rev, &imgref, labels?, copy_meta_keys, cmd).await } ContainerOpts::Image(opts) => match opts { ContainerImageOpts::List { repo } => { diff --git a/lib/src/container/encapsulate.rs b/lib/src/container/encapsulate.rs index 33728aa7..e4055f3c 100644 --- a/lib/src/container/encapsulate.rs +++ b/lib/src/container/encapsulate.rs @@ -5,8 +5,7 @@ use super::{ocidir, OstreeImageReference, Transport}; use super::{ImageReference, SignatureSource, OSTREE_COMMIT_LABEL}; use crate::container::skopeo; use crate::tar as ostree_tar; -use anyhow::Context; -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use fn_error_context::context; use gio::glib; use oci_spec::image as oci_image; @@ -22,7 +21,6 @@ use tracing::{instrument, Level}; /// schema, it's not actually useful today. But, we keep it /// out of principle. const BLOB_OSTREE_ANNOTATION: &str = "ostree.encapsulated"; - /// Configuration for the generated container. #[derive(Debug, Default)] pub struct Config { @@ -46,6 +44,32 @@ fn export_ostree_ref( w.complete() } +fn commit_meta_to_labels<'a>( + meta: &glib::VariantDict, + keys: impl IntoIterator, + labels: &mut HashMap, +) -> Result<()> { + for k in keys { + let v = meta + .lookup::(k) + .context("Expected string for commit metadata value")? + .ok_or_else(|| anyhow!("Could not find commit metadata key: {}", k))?; + labels.insert(k.to_string(), v); + } + // Copy standard metadata keys `ostree.bootable` and `ostree.linux`. + // Bootable is an odd one out in being a boolean. + if let Some(v) = meta.lookup::(*ostree::METADATA_KEY_BOOTABLE)? { + labels.insert(ostree::METADATA_KEY_BOOTABLE.to_string(), v.to_string()); + } + // Handle any other string-typed values here. + for k in &[&ostree::METADATA_KEY_LINUX] { + if let Some(v) = meta.lookup::(k)? { + labels.insert(k.to_string(), v); + } + } + Ok(()) +} + /// Generate an OCI image from a given ostree root #[context("Building oci")] fn build_oci( @@ -76,6 +100,13 @@ fn build_oci( let mut ctrcfg = oci_image::Config::default(); let mut imgcfg = oci_image::ImageConfiguration::default(); let labels = ctrcfg.labels_mut().get_or_insert_with(Default::default); + + commit_meta_to_labels( + &commit_meta, + opts.copy_meta_keys.iter().map(|k| k.as_str()), + labels, + )?; + let mut manifest = ocidir::new_empty_manifest().build().unwrap(); if let Some(version) = @@ -198,6 +229,8 @@ async fn build_impl( pub struct ExportOpts { /// If true, perform gzip compression of the tar layers. pub compress: bool, + /// A set of commit metadata keys to copy as image labels. + pub copy_meta_keys: Vec, } /// Given an OSTree repository and ref, generate a container image. diff --git a/lib/tests/it/main.rs b/lib/tests/it/main.rs index fd0d5698..a7e0581e 100644 --- a/lib/tests/it/main.rs +++ b/lib/tests/it/main.rs @@ -54,7 +54,9 @@ fn generate_test_repo(dir: &Utf8Path) -> Result { indoc! {" cd {dir} ostree --repo=repo init --mode=archive - ostree --repo=repo commit -b {testref} --bootable --no-bindings --add-metadata-string=version=42.0 --gpg-homedir={gpghome} --gpg-sign={keyid} \ + ostree --repo=repo commit -b {testref} --bootable --no-bindings --add-metadata-string=version=42.0 \ + --add-metadata-string=buildsys.checksum=41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3 \ + --gpg-homedir={gpghome} --gpg-sign={keyid} \ --add-detached-metadata-string=my-detached-key=my-detached-value --tree=tar=exampleos.tar.zst >/dev/null ostree --repo=repo show {testref} >/dev/null "}, @@ -464,11 +466,15 @@ async fn test_container_import_export() -> Result<()> { ), cmd: Some(vec!["/bin/bash".to_string()]), }; + let opts = ostree_ext::container::ExportOpts { + copy_meta_keys: vec!["buildsys.checksum".to_string()], + ..Default::default() + }; let digest = ostree_ext::container::encapsulate( &fixture.srcrepo, TESTREF, &config, - None, + Some(opts), &srcoci_imgref, ) .await @@ -479,6 +485,9 @@ async fn test_container_import_export() -> Result<()> { assert!(inspect.contains(r#""version": "42.0""#)); assert!(inspect.contains(r#""foo": "bar""#)); assert!(inspect.contains(r#""test": "value""#)); + assert!(inspect.contains( + r#""buildsys.checksum": "41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3""# + )); let srcoci_unverified = OstreeImageReference { sigverify: SignatureSource::ContainerPolicyAllowInsecure,