diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 1da7a95f..857dc9c1 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use camino::Utf8PathBuf; use clap::Parser; use fn_error_context::context; -use ostree::{gio, glib}; +use ostree::gio; use ostree_container::store::LayeredImageState; use ostree_container::store::PrepareResult; use ostree_container::OstreeImageReference; @@ -14,14 +14,12 @@ use ostree_ext::container as ostree_container; use ostree_ext::container::SignatureSource; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; -use ostree_ext::sysroot::SysrootLock; use std::ffi::OsString; use std::io::Seek; use std::os::unix::process::CommandExt; use std::process::Command; use crate::spec::Host; -use crate::spec::HostSpec; use crate::spec::ImageReference; /// Perform an upgrade operation @@ -229,45 +227,6 @@ async fn pull( Ok(import) } -/// Stage (queue deployment of) a fetched container image. -#[context("Staging")] -async fn stage( - sysroot: &SysrootLock, - stateroot: &str, - image: Box, - spec: &HostSpec, -) -> Result<()> { - let cancellable = gio::Cancellable::NONE; - let stateroot = Some(stateroot); - let merge_deployment = sysroot.merge_deployment(stateroot); - let origin = glib::KeyFile::new(); - let ostree_imgref = spec - .image - .as_ref() - .map(|imgref| OstreeImageReference::from(imgref.clone())); - if let Some(imgref) = ostree_imgref.as_ref() { - origin.set_string( - "origin", - ostree_container::deploy::ORIGIN_CONTAINER, - imgref.to_string().as_str(), - ); - } - let _new_deployment = sysroot.stage_tree_with_options( - stateroot, - image.merge_commit.as_str(), - Some(&origin), - merge_deployment.as_ref(), - &Default::default(), - cancellable, - )?; - if let Some(imgref) = ostree_imgref.as_ref() { - println!("Queued for next boot: {imgref}"); - } - ostree_container::deploy::remove_undeployed_images(sysroot).context("Pruning images")?; - - Ok(()) -} - #[context("Querying root privilege")] pub(crate) fn require_root() -> Result<()> { let uid = rustix::process::getuid(); @@ -282,7 +241,7 @@ pub(crate) fn require_root() -> Result<()> { /// A few process changes that need to be made for writing. #[context("Preparing for write")] -async fn prepare_for_write() -> Result<()> { +pub(crate) async fn prepare_for_write() -> Result<()> { if ostree_ext::container_utils::is_ostree_container()? { anyhow::bail!( "Detected container (ostree base); this command requires a booted host system." @@ -351,7 +310,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { } let osname = booted_deployment.osname(); - stage(sysroot, &osname, fetched, &host.spec).await?; + crate::deploy::stage(sysroot, &osname, fetched, &host.spec).await?; } if let Some(path) = opts.touch_if_changed { std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?; @@ -410,7 +369,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> { } let stateroot = booted_deployment.osname(); - stage(sysroot, &stateroot, fetched, &new_spec).await?; + crate::deploy::stage(sysroot, &stateroot, fetched, &new_spec).await?; Ok(()) } @@ -448,7 +407,7 @@ async fn edit(opts: EditOpts) -> Result<()> { // TODO gc old layers here let stateroot = booted_deployment.osname(); - stage(sysroot, &stateroot, fetched, &new_host.spec).await?; + crate::deploy::stage(sysroot, &stateroot, fetched, &new_host.spec).await?; Ok(()) } diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs new file mode 100644 index 00000000..f4b9fd15 --- /dev/null +++ b/lib/src/deploy.rs @@ -0,0 +1,129 @@ +//! # Write deployments merging image with configmap +//! +//! Create a merged filesystem tree with the image and mounted configmaps. + +use anyhow::{Context, Result}; + +use fn_error_context::context; +use ostree::{gio, glib}; +use ostree_container::store::LayeredImageState; +use ostree_container::OstreeImageReference; +use ostree_ext::container as ostree_container; +use ostree_ext::ostree; +use ostree_ext::ostree::Deployment; +use ostree_ext::sysroot::SysrootLock; + +use crate::spec::HostSpec; + +// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a +const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc"; + +/// Set on an ostree commit if this is a derived commit +const BOOTC_DERIVED_KEY: &str = "bootc.derived"; + +pub(crate) async fn cleanup(sysroot: &SysrootLock) -> Result<()> { + let repo = sysroot.repo(); + let sysroot = sysroot.sysroot.clone(); + ostree_ext::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| { + let cancellable = Some(cancellable); + let repo = &repo; + let txn = repo.auto_transaction(cancellable)?; + let repo = txn.repo(); + + // Regenerate our base references. First, we delete the ones that exist + for ref_entry in repo + .list_refs_ext( + Some(BASE_IMAGE_PREFIX), + ostree::RepoListRefsExtFlags::NONE, + cancellable, + ) + .context("Listing refs")? + .keys() + { + repo.transaction_set_refspec(ref_entry, None); + } + + // Then, for each deployment which is derived (e.g. has configmaps) we synthesize + // a base ref to ensure that it's not GC'd. + for (i, deployment) in sysroot.deployments().into_iter().enumerate() { + let commit = deployment.csum(); + if let Some(base) = get_base_commit(repo, &commit)? { + repo.transaction_set_refspec(&format!("{BASE_IMAGE_PREFIX}/{i}"), Some(&base)); + } + } + + Ok(()) + }) + .await +} + +/// If commit is a bootc-derived commit (e.g. has configmaps), return its base. +#[context("Finding base commit")] +pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result> { + let commitv = repo.load_commit(commit)?.0; + let commitmeta = commitv.child_value(0); + let commitmeta = &glib::VariantDict::new(Some(&commitmeta)); + let r = commitmeta.lookup::(BOOTC_DERIVED_KEY)?; + Ok(r) +} + +#[context("Writing deployment")] +async fn deploy( + sysroot: &SysrootLock, + merge_deployment: Option<&Deployment>, + stateroot: &str, + image: Box, + origin: &glib::KeyFile, +) -> Result<()> { + let stateroot = Some(stateroot); + // Copy to move into thread + let base_commit = image.get_commit().to_owned(); + let cancellable = gio::Cancellable::NONE; + let _new_deployment = sysroot.stage_tree_with_options( + stateroot, + &base_commit, + Some(origin), + merge_deployment, + &Default::default(), + cancellable, + )?; + Ok(()) +} + +/// Stage (queue deployment of) a fetched container image. +#[context("Staging")] +pub(crate) async fn stage( + sysroot: &SysrootLock, + stateroot: &str, + image: Box, + spec: &HostSpec, +) -> Result<()> { + let merge_deployment = sysroot.merge_deployment(Some(stateroot)); + let origin = glib::KeyFile::new(); + let ostree_imgref = spec + .image + .as_ref() + .map(|imgref| OstreeImageReference::from(imgref.clone())); + if let Some(imgref) = ostree_imgref.as_ref() { + origin.set_string( + "origin", + ostree_container::deploy::ORIGIN_CONTAINER, + imgref.to_string().as_str(), + ); + } + crate::deploy::deploy( + sysroot, + merge_deployment.as_ref(), + stateroot, + image, + &origin, + ) + .await?; + crate::deploy::cleanup(sysroot).await?; + if let Some(imgref) = ostree_imgref.as_ref() { + println!("Queued for next boot: {imgref}"); + } + ostree_container::deploy::remove_undeployed_images(sysroot).context("Pruning images")?; + + Ok(()) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 58f27a3d..9e8f64cd 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ #![deny(clippy::todo)] pub mod cli; +pub(crate) mod deploy; mod lsm; mod reexec; mod status;