diff --git a/Makefile b/Makefile index d3e82e0c..0b8a8630 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ install: install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d + ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/ ln -f $(DESTDIR)$(prefix)/bin/bootc $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator install -d $(DESTDIR)$(prefix)/lib/bootc/install diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 31707cfe..6627f804 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -430,10 +430,12 @@ pub(crate) async fn get_locked_sysroot() -> Result Result { + let global_run = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?; let sysroot = get_locked_sysroot().await?; - crate::store::Storage::new(sysroot) + crate::store::Storage::new(sysroot, &global_run) } #[context("Querying root privilege")] diff --git a/lib/src/imgstorage.rs b/lib/src/imgstorage.rs new file mode 100644 index 00000000..8cabb30f --- /dev/null +++ b/lib/src/imgstorage.rs @@ -0,0 +1,85 @@ +//! # bootc-managed container storage +//! +//! The default storage for this project uses ostree, canonically storing all of its state in +//! `/sysroot/ostree`. +//! +//! This containers-storage: which canonically lives in `/sysroot/ostree/bootc`. + +use std::sync::Arc; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::cmdext::CapStdExtCommandExt; +use cap_std_ext::dirext::CapStdExtDirExt; +use fn_error_context::context; +use std::os::fd::OwnedFd; + +use crate::task::Task; + +/// The path to the storage, relative to the physical system root. +pub(crate) const SUBPATH: &str = "ostree/bootc/storage"; +/// The path to the "runroot" with transient runtime state; this is +/// relative to the /run directory +const RUNROOT: &str = "bootc/storage"; +pub(crate) struct Storage { + root: Dir, + #[allow(dead_code)] + run: Dir, +} + +impl Storage { + fn podman_task_in(sysroot: OwnedFd, run: OwnedFd) -> Result { + let mut t = Task::new_quiet("podman"); + // podman expects absolute paths for these, so use /proc/self/fd + { + let sysroot_fd: Arc = Arc::new(sysroot); + t.cmd.take_fd_n(sysroot_fd, 3); + } + { + let run_fd: Arc = Arc::new(run); + t.cmd.take_fd_n(run_fd, 4); + } + t = t.args(["--root=/proc/self/fd/3", "--runroot=/proc/self/fd/4"]); + Ok(t) + } + + #[allow(dead_code)] + fn podman_task(&self) -> Result { + let sysroot = self.root.try_clone()?.into_std_file().into(); + let run = self.run.try_clone()?.into_std_file().into(); + Self::podman_task_in(sysroot, run) + } + + #[context("Creating imgstorage")] + pub(crate) fn create(sysroot: &Dir, run: &Dir) -> Result { + let subpath = Utf8Path::new(SUBPATH); + // SAFETY: We know there's a parent + let parent = subpath.parent().unwrap(); + if !sysroot.try_exists(subpath)? { + let tmp = format!("{SUBPATH}.tmp"); + sysroot.remove_all_optional(&tmp)?; + sysroot.create_dir_all(parent)?; + sysroot.create_dir_all(&tmp).context("Creating tmpdir")?; + // There's no explicit API to initialize a containers-storage: + // root, simply passing a path will attempt to auto-create it. + // We run "podman images" in the new root. + Self::podman_task_in(sysroot.open_dir(&tmp)?.into(), run.try_clone()?.into())? + .arg("images") + .run()?; + sysroot + .rename(&tmp, sysroot, subpath) + .context("Renaming tmpdir")?; + } + Self::open(sysroot, run) + } + + #[context("Opening imgstorage")] + pub(crate) fn open(sysroot: &Dir, run: &Dir) -> Result { + let root = sysroot.open_dir(SUBPATH).context(SUBPATH)?; + // Always auto-create this if missing + run.create_dir_all(RUNROOT)?; + let run = run.open_dir(RUNROOT).context(RUNROOT)?; + Ok(Self { root, run }) + } +} diff --git a/lib/src/install.rs b/lib/src/install.rs index 633819d6..f2b8b4d1 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -598,6 +598,19 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result .cwd(rootfs_dir)? .run()?; + let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs))); + sysroot.load(cancellable)?; + let sysroot_dir = Dir::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?; + + state.tempdir.create_dir("temp-run")?; + let temp_run = state.tempdir.open_dir("temp-run")?; + sysroot_dir + .create_dir_all(Utf8Path::new(crate::imgstorage::SUBPATH).parent().unwrap()) + .context("creating bootc dir")?; + let imgstore = crate::imgstorage::Storage::create(&sysroot_dir, &temp_run)?; + // And drop it again - we'll reopen it after this + drop(imgstore); + // Bootstrap the initial labeling of the /ostree directory as usr_t if let Some(policy) = sepolicy { let ostree_dir = rootfs_dir.open_dir("ostree")?; @@ -613,7 +626,7 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs))); sysroot.load(cancellable)?; let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?; - Storage::new(sysroot) + Storage::new(sysroot, &temp_run) } #[context("Creating ostree deployment")] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6d05aa64..cf03412e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -44,3 +44,4 @@ pub mod spec; #[cfg(feature = "docgen")] mod docgen; +mod imgstorage; diff --git a/lib/src/store/mod.rs b/lib/src/store/mod.rs index 71334177..b501347a 100644 --- a/lib/src/store/mod.rs +++ b/lib/src/store/mod.rs @@ -2,6 +2,7 @@ use std::env; use std::ops::Deref; use anyhow::Result; +use cap_std_ext::cap_std::fs::Dir; use clap::ValueEnum; use ostree_ext::container::OstreeImageReference; @@ -15,6 +16,8 @@ mod ostree_container; pub(crate) struct Storage { pub sysroot: SysrootLock, + #[allow(dead_code)] + pub imgstore: crate::imgstorage::Storage, pub store: Box, } @@ -48,7 +51,7 @@ impl Deref for Storage { } impl Storage { - pub fn new(sysroot: SysrootLock) -> Result { + pub fn new(sysroot: SysrootLock, run: &Dir) -> Result { 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(); @@ -58,9 +61,16 @@ impl Storage { Err(_) => crate::spec::Store::default(), }; + let sysroot_dir = Dir::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?; + let imgstore = crate::imgstorage::Storage::open(&sysroot_dir, run)?; + let store = load(store); - Ok(Self { sysroot, store }) + Ok(Self { + sysroot, + store, + imgstore, + }) } } diff --git a/tests-integration/src/install.rs b/tests-integration/src/install.rs index 73b9f30e..7955528d 100644 --- a/tests-integration/src/install.rs +++ b/tests-integration/src/install.rs @@ -2,6 +2,7 @@ use std::path::Path; use std::{os::fd::AsRawFd, path::PathBuf}; use anyhow::Result; +use camino::Utf8Path; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; @@ -53,6 +54,12 @@ fn find_deployment_root() -> Result { anyhow::bail!("Failed to find deployment root") } +// Hook relatively cheap post-install tests here +fn generic_post_install_verification() -> Result<()> { + assert!(Utf8Path::new("/ostree/bootc/storage/overlay").try_exists()?); + Ok(()) +} + #[context("Install tests")] pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) -> Result<()> { // Force all of these tests to be serial because they mutate global state @@ -88,6 +95,8 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 testcase@example.com")?; cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {tmp_keys}:/test_authorized_keys {image} bootc install to-filesystem {generic_inst_args...} --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target").run()?; + generic_post_install_verification()?; + // Test kargs injected via CLI cmd!( sh, @@ -120,6 +129,7 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) let sh = &xshell::Shell::new()?; reset_root(sh)?; cmd!(sh, "sudo {BASE_ARGS...} {target_args...} {image} bootc install to-existing-root --acknowledge-destructive {generic_inst_args...}").run()?; + generic_post_install_verification()?; let root = &Dir::open_ambient_dir("/ostree", cap_std::ambient_authority()).unwrap(); let mut path = PathBuf::from("."); crate::selinux::verify_selinux_recurse(root, &mut path, false)?; @@ -131,6 +141,7 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) let empty = sh.create_temp_dir()?; let empty = empty.path().to_str().unwrap(); cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {empty}:/usr/lib/bootc/install {image} bootc install to-existing-root {generic_inst_args...}").run()?; + generic_post_install_verification()?; Ok(()) }), ]; diff --git a/tests/booted/010-bootc-container-store.nu b/tests/booted/010-bootc-container-store.nu new file mode 100644 index 00000000..036c5904 --- /dev/null +++ b/tests/booted/010-bootc-container-store.nu @@ -0,0 +1,8 @@ +use std assert +use tap.nu + +tap begin "verify bootc-owned container storage" + +# This should currently be empty by default... +podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images +tap ok