Skip to content

Commit

Permalink
Prep for multiple container image stores
Browse files Browse the repository at this point in the history
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 <jeckersb@redhat.com>
  • Loading branch information
jeckersb committed Jul 24, 2024
1 parent 1a171a6 commit 54dfc34
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 53 deletions.
14 changes: 10 additions & 4 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
Ok(sysroot)
}

#[context("Initializing storage")]
pub(crate) async fn get_storage() -> Result<crate::store::Storage> {
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();
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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) =
Expand Down
3 changes: 2 additions & 1 deletion lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)?;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub(crate) mod metadata;
mod reboot;
mod reexec;
mod status;
mod store;
mod task;
mod utils;

Expand Down
25 changes: 25 additions & 0 deletions lib/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Store>,
/// If this boot entry is ostree based, the corresponding state
pub ostree: Option<BootEntryOstree>,
}
Expand Down Expand Up @@ -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());
}
}
74 changes: 27 additions & 47 deletions lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ostree_container::SignatureSource> for ImageSignature {
fn from(sig: ostree_container::SignatureSource) -> Self {
Expand Down Expand Up @@ -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<BootEntry> {
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(),
Expand Down Expand Up @@ -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))?;
Expand All @@ -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());
Expand Down Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions lib/src/store/mod.rs
Original file line number Diff line number Diff line change
@@ -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<dyn ContainerImageStoreImpl>,
}

#[derive(Default)]
pub(crate) struct CachedImageStatus {
pub image: Option<ImageStatus>,
pub cached_update: Option<ImageStatus>,
}

pub(crate) trait ContainerImageStore {
fn store(&self) -> Result<Option<Box<dyn ContainerImageStoreImpl>>>;
}

pub(crate) trait ContainerImageStoreImpl {
fn spec(&self) -> crate::spec::Store;

fn imagestatus(
&self,
sysroot: &SysrootLock,
deployment: &ostree::Deployment,
image: OstreeImageReference,
) -> Result<CachedImageStatus>;
}

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<Option<Box<dyn ContainerImageStoreImpl>>> {
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<dyn ContainerImageStoreImpl> {
match ty {
crate::spec::Store::OstreeContainer => Box::new(ostree_container::OstreeContainerStore),
}
}
Loading

0 comments on commit 54dfc34

Please sign in to comment.