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/imgstorage.rs b/lib/src/imgstorage.rs new file mode 100644 index 00000000..e072cd48 --- /dev/null +++ b/lib/src/imgstorage.rs @@ -0,0 +1,138 @@ +//! # 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::cell::OnceCell; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use cap_std_ext::{ + cap_std::fs_utf8::Dir, cmdext::CapStdExtCommandExt, dirext::CapStdExtDirExtUtf8, +}; +use fn_error_context::context; +use rustix::fd::AsFd; +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 name of the overlay backend, created under SUBPATH above. +/// Used to signal that the storage is fully initialized. +const OVERLAY: &str = "overlay"; +/// 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, + // A temporary mount point because skopeo wants absolute paths + temp_mount: OnceCell>, +} + +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.as_cap_std().try_clone()?.into_std_file().into(); + let run = self.run.as_cap_std().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.join(OVERLAY))? { + 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")?; + sysroot.remove_all_optional(&tmp)?; + } + 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, + temp_mount: OnceCell::new(), + }) + } + + /// View this storage as a directory. + #[allow(dead_code)] + pub(crate) fn as_dir(&self) -> &Dir { + &self.root + } + + fn get_temp_mount(&self) -> Result<&Utf8Path> { + match self.temp_mount.get_or_init(|| { + let td = tempfile::TempDir::new()?; + Task::new_quiet("mount") + .args(["--bind", "."]) + .arg(td.path()) + .cwd(self.root.as_cap_std())? + .run()?; + Ok(td) + }) { + Ok(r) => Ok(r.path().try_into()?), + Err(e) => Err(anyhow::Error::msg(e.to_string())), + } + } + + pub(crate) fn pull_from_host_storage(&self, image: &str) -> Result<()> { + // The skopeo API expects absolute paths, so we make a temporary bind + let temp_mount = self.get_temp_mount()?; + // And an ephemeral place for the transient state + let tmp_runroot = tempfile::tempdir()?; + let tmp_runroot: &Utf8Path = tmp_runroot.path().try_into()?; + + // The destination (target stateroot) + container storage dest + let storage_dest = &format!( + "containers-storage:[overlay@{temp_mount}+{tmp_runroot}]" + ); + Task::new(format!("Copying image to target: {}", image), "skopeo") + .arg("copy") + .arg(format!("containers-storage:{image}")) + .arg(format!("{storage_dest}{image}")) + .run()?; + Ok(()) + } +} diff --git a/lib/src/install.rs b/lib/src/install.rs index 66139ecd..0b57f570 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -11,7 +11,7 @@ pub(crate) mod config; pub(crate) mod osconfig; use std::io::Write; -use std::os::fd::{AsFd, OwnedFd}; +use std::os::fd::AsFd; use std::os::unix::process::CommandExt; use std::path::Path; use std::process::Command; @@ -24,9 +24,9 @@ use anyhow::{anyhow, Context, Result}; use camino::Utf8Path; use camino::Utf8PathBuf; use cap_std::fs::{Dir, MetadataExt}; +use cap_std::fs_utf8::Dir as DirUtf8; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8; -use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::prelude::CapStdExtDirExt; use chrono::prelude::*; use clap::ValueEnum; @@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize}; use self::baseline::InstallBlockDeviceOpts; use crate::containerenv::ContainerExecutionInfo; +use crate::imgstorage::Storage; use crate::mount::Filesystem; use crate::spec::ImageReference; use crate::task::Task; @@ -548,8 +549,11 @@ pub(crate) fn print_configuration() -> Result<()> { serde_json::to_writer(stdout, &install_config).map_err(Into::into) } -#[context("Creating ostree deployment")] -async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result { +#[context("Creating system root")] +async fn initialize_ostree_root( + state: &State, + root_setup: &RootSetup, +) -> Result<(ostree::Sysroot, crate::imgstorage::Storage)> { let sepolicy = state.load_policy()?; let sepolicy = sepolicy.as_ref(); // Load a fd for the mounted target physical root @@ -594,6 +598,16 @@ 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 = DirUtf8::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?; + + let tmp_run = cap_std_ext::cap_tempfile::utf8::TempDir::new(cap_std::ambient_authority())?; + sysroot_dir + .create_dir(Utf8Path::new(crate::imgstorage::SUBPATH).parent().unwrap()) + .context("creating bootc dir")?; + let imgstore = crate::imgstorage::Storage::create(&sysroot_dir, &*tmp_run)?; + // Bootstrap the initial labeling of the /ostree directory as usr_t if let Some(policy) = sepolicy { let ostree_dir = rootfs_dir.open_dir("ostree")?; @@ -606,9 +620,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)?; - Ok(sysroot) + Ok((sysroot, imgstore)) } #[context("Creating ostree deployment")] @@ -1271,14 +1283,14 @@ async fn install_with_sysroot( state: &State, rootfs: &RootSetup, sysroot: &ostree::Sysroot, + imgstore: &Storage, boot_uuid: &str, bound_images: &[crate::boundimage::ResolvedBoundImage], ) -> Result<()> { let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?; // And actually set up the container in that root, returning a deployment and // the aleph state (see below). - let (deployment, aleph) = install_container(state, rootfs, &sysroot).await?; - let stateroot = deployment.osname(); + let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?; // Write the aleph data that captures the system state at the time of provisioning for aid in future debugging. rootfs .rootfs_fd @@ -1301,53 +1313,11 @@ async fn install_with_sysroot( tracing::debug!("Installed bootloader"); tracing::debug!("Perfoming post-deployment operations"); - if !bound_images.is_empty() { - // TODO: We shouldn't hardcode the overlay driver for source or - // target, but we currently need to in order to reference the location. - // For this one, containers-storage: is actually the *host*'s /var/lib/containers - // which we are accessing directly. - let storage_src = "containers-storage:"; - // TODO: We only do this dance to initialize `/var` at install time if - // there are bound images today; it minimizes side effects. - // However going forward we really do need to handle a separate /var partition... - // and to do that we may in the general case need to run the `var.mount` - // target from the new root. - // Probably the best fix is for us to switch bound images to use the bootc storage. - let varpath = format!("ostree/deploy/{stateroot}/var"); - let var = rootfs - .rootfs_fd - .open_dir(&varpath) - .with_context(|| format!("Opening {varpath}"))?; - - // The skopeo API expects absolute paths, so we make a temporary bind - let tmp_dest_var_abs = tempfile::tempdir()?; - let tmp_dest_var_abs: &Utf8Path = tmp_dest_var_abs.path().try_into()?; - let mut t = Task::new("Mounting deployment /var", "mount") - .args(["--bind", "/proc/self/fd/3"]) - .arg(tmp_dest_var_abs); - t.cmd.take_fd_n(Arc::new(OwnedFd::from(var)), 3); - t.run()?; - - // And an ephemeral place for the transient state - let tmp_runroot = tempfile::tempdir()?; - let tmp_runroot: &Utf8Path = tmp_runroot.path().try_into()?; - - // The destination (target stateroot) + container storage dest - let storage_dest = &format!( - "containers-storage:[overlay@{tmp_dest_var_abs}/lib/containers/storage+{tmp_runroot}]" - ); - - // Now copy each bound image from the host's container storage into the target. - for image in bound_images { - let image = image.image.as_str(); - Task::new(format!("Copying image to target: {}", image), "skopeo") - .arg("copy") - .arg(format!("{storage_src}{image}")) - .arg(format!("{storage_dest}{image}")) - .run()?; - } + // Now copy each bound image from the host's container storage into the target. + for image in bound_images { + let image = image.image.as_str(); + imgstore.pull_from_host_storage(image)?; } - Ok(()) } @@ -1397,8 +1367,16 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re // Initialize the ostree sysroot (repo, stateroot, etc.) { - let sysroot = initialize_ostree_root(state, rootfs).await?; - install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?; + let (sysroot, imgstore) = initialize_ostree_root(state, rootfs).await?; + install_with_sysroot( + state, + rootfs, + &sysroot, + &imgstore, + &boot_uuid, + &bound_images, + ) + .await?; // We must drop the sysroot here in order to close any open file // descriptors. } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 7479f2a5..f69f5f75 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -45,3 +45,4 @@ pub mod spec; #[cfg(feature = "docgen")] mod docgen; +mod imgstorage;