From 54dfc3417f7ad9b4edf1b080f2bd01dec07eb7de Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Tue, 16 Jul 2024 16:23:38 -0400 Subject: [PATCH] Prep for multiple container image stores For now, this is effectively a no-op as it only implements a store for the current ostree container implementation. Beyond some minor code movement and plumbing, this also only handles the store implementation backing the `bootc status` command. Additional image store functionality (check, pull, delete, etc.) will be added in future changes in order to keep things smaller and more manageable. Signed-off-by: John Eckersberg --- lib/src/cli.rs | 14 +++-- lib/src/deploy.rs | 3 +- lib/src/image.rs | 2 +- lib/src/lib.rs | 1 + lib/src/spec.rs | 25 +++++++++ lib/src/status.rs | 74 ++++++++++---------------- lib/src/store/mod.rs | 87 +++++++++++++++++++++++++++++++ lib/src/store/ostree_container.rs | 79 ++++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 53 deletions(-) create mode 100644 lib/src/store/mod.rs create mode 100644 lib/src/store/ostree_container.rs diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 94a4013fe..97fa35d9c 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -430,6 +430,12 @@ pub(crate) async fn get_locked_sysroot() -> Result Result { + let sysroot = get_locked_sysroot().await?; + Ok(crate::store::Storage::new(sysroot)) +} + #[context("Querying root privilege")] pub(crate) fn require_root() -> Result<()> { let uid = rustix::process::getuid(); @@ -482,7 +488,7 @@ fn prepare_for_write() -> Result<()> { /// Implementation of the `bootc upgrade` CLI command. #[context("Upgrading")] async fn upgrade(opts: UpgradeOpts) -> Result<()> { - let sysroot = &get_locked_sysroot().await?; + let sysroot = &get_storage().await?; let repo = &sysroot.repo(); let (booted_deployment, _deployments, host) = crate::status::get_status_require_booted(sysroot)?; @@ -619,7 +625,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> { let cancellable = gio::Cancellable::NONE; - let sysroot = &get_locked_sysroot().await?; + let sysroot = &get_storage().await?; let repo = &sysroot.repo(); let (booted_deployment, _deployments, host) = crate::status::get_status_require_booted(sysroot)?; @@ -658,14 +664,14 @@ async fn switch(opts: SwitchOpts) -> Result<()> { /// Implementation of the `bootc rollback` CLI command. #[context("Rollback")] async fn rollback(_opts: RollbackOpts) -> Result<()> { - let sysroot = &get_locked_sysroot().await?; + let sysroot = &get_storage().await?; crate::deploy::rollback(sysroot).await } /// Implementation of the `bootc edit` CLI command. #[context("Editing spec")] async fn edit(opts: EditOpts) -> Result<()> { - let sysroot = &get_locked_sysroot().await?; + let sysroot = &get_storage().await?; let repo = &sysroot.repo(); let (booted_deployment, _deployments, host) = diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs index 6d8a640e8..59325b264 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -22,6 +22,7 @@ use ostree_ext::sysroot::SysrootLock; use crate::spec::ImageReference; use crate::spec::{BootOrder, HostSpec}; use crate::status::labels_of_config; +use crate::store::Storage; // TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc"; @@ -405,7 +406,7 @@ pub(crate) async fn stage( } /// Implementation of rollback functionality -pub(crate) async fn rollback(sysroot: &SysrootLock) -> Result<()> { +pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> { const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468"; let repo = &sysroot.repo(); let (booted_deployment, deployments, host) = crate::status::get_status_require_booted(sysroot)?; diff --git a/lib/src/image.rs b/lib/src/image.rs index 4fd7cd9b1..296f41e7d 100644 --- a/lib/src/image.rs +++ b/lib/src/image.rs @@ -26,7 +26,7 @@ pub(crate) async fn list_entrypoint() -> Result<()> { #[context("Pushing image")] pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>) -> Result<()> { let transport = Transport::ContainerStorage; - let sysroot = crate::cli::get_locked_sysroot().await?; + let sysroot = crate::cli::get_storage().await?; let repo = &sysroot.repo(); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index ed3a82d73..dbb214cd4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -26,6 +26,7 @@ pub(crate) mod metadata; mod reboot; mod reexec; mod status; +mod store; mod task; mod utils; diff --git a/lib/src/spec.rs b/lib/src/spec.rs index f6414cf80..415a5ebf8 100644 --- a/lib/src/spec.rs +++ b/lib/src/spec.rs @@ -40,6 +40,18 @@ pub enum BootOrder { Rollback, } +#[derive( + clap::ValueEnum, Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, Default, +)] +#[serde(rename_all = "camelCase")] +/// The container storage backend +pub enum Store { + /// Use the ostree-container storage backend. + #[default] + #[value(alias = "ostreecontainer")] // default is kebab-case + OstreeContainer, +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] /// The host specification @@ -112,6 +124,9 @@ pub struct BootEntry { pub incompatible: bool, /// Whether this entry will be subject to garbage collection pub pinned: bool, + /// The container storage backend + #[serde(default)] + pub store: Option, /// If this boot entry is ostree based, the corresponding state pub ostree: Option, } @@ -258,4 +273,14 @@ mod tests { assert_eq!(displayed.as_str(), src); assert_eq!(format!("{s:#}"), src); } + + #[test] + fn test_store_from_str() { + use clap::ValueEnum; + + // should be case-insensitive, kebab-case optional + assert!(Store::from_str("Ostree-Container", true).is_ok()); + assert!(Store::from_str("OstrEeContAiner", true).is_ok()); + assert!(Store::from_str("invalid", true).is_err()); + } } diff --git a/lib/src/status.rs b/lib/src/status.rs index 65386ff38..8f9a6999b 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -8,13 +8,12 @@ use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::oci_spec; -use ostree_ext::oci_spec::image::ImageConfiguration; use ostree_ext::ostree; -use ostree_ext::sysroot::SysrootLock; use crate::cli::OutputFormat; -use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType, ImageStatus}; +use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; +use crate::store::{CachedImageStatus, ContainerImageStore, Storage}; impl From for ImageSignature { fn from(sig: ostree_container::SignatureSource) -> Self { @@ -115,65 +114,46 @@ pub(crate) fn labels_of_config( config.config().as_ref().and_then(|c| c.labels().as_ref()) } -/// Convert between a subset of ostree-ext metadata and the exposed spec API. -pub(crate) fn create_imagestatus( - image: ImageReference, - manifest_digest: &str, - config: &ImageConfiguration, -) -> ImageStatus { - let labels = labels_of_config(config); - let timestamp = labels - .and_then(|l| { - l.get(oci_spec::image::ANNOTATION_CREATED) - .map(|s| s.as_str()) - }) - .and_then(try_deserialize_timestamp); - - let version = ostree_container::version_for_config(config).map(ToOwned::to_owned); - ImageStatus { - image, - version, - timestamp, - image_digest: manifest_digest.to_owned(), - } -} - /// Given an OSTree deployment, parse out metadata into our spec. #[context("Reading deployment metadata")] fn boot_entry_from_deployment( - sysroot: &SysrootLock, + sysroot: &Storage, deployment: &ostree::Deployment, ) -> Result { - let repo = &sysroot.repo(); - let (image, cached_update, incompatible) = if let Some(origin) = deployment.origin().as_ref() { + let ( + store, + CachedImageStatus { + image, + cached_update, + }, + incompatible, + ) = if let Some(origin) = deployment.origin().as_ref() { let incompatible = crate::utils::origin_has_rpmostree_stuff(origin); - let (image, cached) = if incompatible { + let (store, cached_imagestatus) = if incompatible { // If there are local changes, we can't represent it as a bootc compatible image. - (None, None) + (None, CachedImageStatus::default()) } else if let Some(image) = get_image_origin(origin)? { - let image = ImageReference::from(image); - let csum = deployment.csum(); - let imgstate = ostree_container::store::query_image_commit(repo, &csum)?; - let cached = imgstate.cached_update.map(|cached| { - create_imagestatus(image.clone(), &cached.manifest_digest, &cached.config) - }); - let imagestatus = - create_imagestatus(image, &imgstate.manifest_digest, &imgstate.configuration); - // We found a container-image based deployment - (Some(imagestatus), cached) + let store = deployment.store()?; + let store = store.as_ref().unwrap_or(&sysroot.store); + let spec = Some(store.spec()); + let status = store.imagestatus(sysroot, deployment, image)?; + + (spec, status) } else { // The deployment isn't using a container image - (None, None) + (None, CachedImageStatus::default()) }; - (image, cached, incompatible) + (store, cached_imagestatus, incompatible) } else { // The deployment has no origin at all (this generally shouldn't happen) - (None, None, false) + (None, CachedImageStatus::default(), false) }; + let r = BootEntry { image, cached_update, incompatible, + store, pinned: deployment.is_pinned(), ostree: Some(crate::spec::BootEntryOstree { checksum: deployment.csum().into(), @@ -203,7 +183,7 @@ impl BootEntry { /// A variant of [`get_status`] that requires a booted deployment. pub(crate) fn get_status_require_booted( - sysroot: &SysrootLock, + sysroot: &Storage, ) -> Result<(ostree::Deployment, Deployments, Host)> { let booted_deployment = sysroot.require_booted_deployment()?; let (deployments, host) = get_status(sysroot, Some(&booted_deployment))?; @@ -214,7 +194,7 @@ pub(crate) fn get_status_require_booted( /// a more native Rust structure. #[context("Computing status")] pub(crate) fn get_status( - sysroot: &SysrootLock, + sysroot: &Storage, booted_deployment: Option<&ostree::Deployment>, ) -> Result<(Deployments, Host)> { let stateroot = booted_deployment.as_ref().map(|d| d.osname()); @@ -311,7 +291,7 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { let host = if !Utf8Path::new("/run/ostree-booted").try_exists()? { Default::default() } else { - let sysroot = super::cli::get_locked_sysroot().await?; + let sysroot = super::cli::get_storage().await?; let booted_deployment = sysroot.booted_deployment(); let (_deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?; host diff --git a/lib/src/store/mod.rs b/lib/src/store/mod.rs new file mode 100644 index 000000000..8a30cedc8 --- /dev/null +++ b/lib/src/store/mod.rs @@ -0,0 +1,87 @@ +use std::env; +use std::ops::Deref; + +use anyhow::Result; +use clap::ValueEnum; + +use ostree_ext::container::OstreeImageReference; +use ostree_ext::keyfileext::KeyFileExt; +use ostree_ext::ostree; +use ostree_ext::sysroot::SysrootLock; + +use crate::spec::ImageStatus; + +mod ostree_container; + +pub(crate) struct Storage { + pub sysroot: SysrootLock, + pub store: Box, +} + +#[derive(Default)] +pub(crate) struct CachedImageStatus { + pub image: Option, + pub cached_update: Option, +} + +pub(crate) trait ContainerImageStore { + fn store(&self) -> Result>>; +} + +pub(crate) trait ContainerImageStoreImpl { + fn spec(&self) -> crate::spec::Store; + + fn imagestatus( + &self, + sysroot: &SysrootLock, + deployment: &ostree::Deployment, + image: OstreeImageReference, + ) -> Result; +} + +impl Deref for Storage { + type Target = SysrootLock; + + fn deref(&self) -> &Self::Target { + &self.sysroot + } +} + +impl Storage { + pub fn new(sysroot: SysrootLock) -> Self { + let store = match env::var("BOOTC_STORAGE") { + Ok(val) => crate::spec::Store::from_str(&val, true).unwrap_or_else(|_| { + let default = crate::spec::Store::default(); + tracing::warn!("Unknown BOOTC_STORAGE option {val}, falling back to {default:?}"); + default + }), + Err(_) => crate::spec::Store::default(), + }; + + let store = load(store); + + Self { sysroot, store } + } +} + +impl ContainerImageStore for ostree::Deployment { + fn store<'a>(&self) -> Result>> { + if let Some(origin) = self.origin().as_ref() { + if let Some(store) = origin.optional_string("bootc", "backend")? { + let store = + crate::spec::Store::from_str(&store, true).map_err(anyhow::Error::msg)?; + Ok(Some(load(store))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } +} + +pub(crate) fn load(ty: crate::spec::Store) -> Box { + match ty { + crate::spec::Store::OstreeContainer => Box::new(ostree_container::OstreeContainerStore), + } +} diff --git a/lib/src/store/ostree_container.rs b/lib/src/store/ostree_container.rs new file mode 100644 index 000000000..ca18000f5 --- /dev/null +++ b/lib/src/store/ostree_container.rs @@ -0,0 +1,79 @@ +use anyhow::{Context, Result}; + +use ostree_ext::container as ostree_container; +use ostree_ext::oci_spec; +use ostree_ext::oci_spec::image::ImageConfiguration; +use ostree_ext::ostree; +use ostree_ext::sysroot::SysrootLock; + +use super::CachedImageStatus; +use crate::spec::{ImageReference, ImageStatus}; + +pub(super) struct OstreeContainerStore; + +impl super::ContainerImageStoreImpl for OstreeContainerStore { + fn spec(&self) -> crate::spec::Store { + crate::spec::Store::OstreeContainer + } + + fn imagestatus( + &self, + sysroot: &SysrootLock, + deployment: &ostree::Deployment, + image: ostree_container::OstreeImageReference, + ) -> Result { + let repo = &sysroot.repo(); + let image = ImageReference::from(image); + let csum = deployment.csum(); + let imgstate = ostree_container::store::query_image_commit(repo, &csum)?; + let cached = imgstate.cached_update.map(|cached| { + create_imagestatus(image.clone(), &cached.manifest_digest, &cached.config) + }); + let imagestatus = + create_imagestatus(image, &imgstate.manifest_digest, &imgstate.configuration); + + Ok(CachedImageStatus { + image: Some(imagestatus), + cached_update: cached, + }) + } +} + +/// Convert between a subset of ostree-ext metadata and the exposed spec API. +fn create_imagestatus( + image: ImageReference, + manifest_digest: &str, + config: &ImageConfiguration, +) -> ImageStatus { + let labels = labels_of_config(config); + let timestamp = labels + .and_then(|l| { + l.get(oci_spec::image::ANNOTATION_CREATED) + .map(|s| s.as_str()) + }) + .and_then(try_deserialize_timestamp); + + let version = ostree_container::version_for_config(config).map(ToOwned::to_owned); + ImageStatus { + image, + version, + timestamp, + image_digest: manifest_digest.to_owned(), + } +} + +fn labels_of_config( + config: &oci_spec::image::ImageConfiguration, +) -> Option<&std::collections::HashMap> { + config.config().as_ref().and_then(|c| c.labels().as_ref()) +} + +fn try_deserialize_timestamp(t: &str) -> Option> { + match chrono::DateTime::parse_from_rfc3339(t).context("Parsing timestamp") { + Ok(t) => Some(t.into()), + Err(e) => { + tracing::warn!("Invalid timestamp in image: {:#}", e); + None + } + } +}