Skip to content

Commit

Permalink
Introduce bootc-owned container store, use for bound images
Browse files Browse the repository at this point in the history
WIP for containers#721

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Jul 25, 2024
1 parent fe5225b commit bbb3fc1
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 59 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions lib/src/imgstorage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! # 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_utf8::Dir, cmdext::CapStdExtCommandExt, dirext::CapStdExtDirExtUtf8,
};
use fn_error_context::context;
use std::os::fd::OwnedFd;

use crate::task::Task;

struct TempMount {
dir: Option<tempfile::TempDir>,
}

impl TempMount {
#[context("Creating temp mount")]
fn new(srcd: &Dir) -> Result<Self> {
let dir = tempfile::tempdir()?;
Task::new_quiet("mount")
.args(["--bind", "."])
.arg(dir.path())
.cwd(srcd.as_cap_std())?
.run()?;
Ok(Self { dir: Some(dir) })
}

fn path(&self) -> &Utf8Path {
// SAFETY: We don't expose an unset value except on drop
let dir = self.dir.as_ref().unwrap();
// SAFETY: We really expect utf-8 paths
dir.path().try_into().unwrap()
}

#[context("Closing temp mount")]
fn impl_close(&mut self) -> Result<()> {
let Some(dir) = self.dir.take() else {
return Ok(());
};
// We must recursively unmount because the storage stack
// creates a bind mount at the target by default.
Task::new_quiet("umount")
.args(["-R"])
.arg(dir.path())
.run()?;
dir.close()?;
Ok(())
}

// We expect users to pass this to close() which checks errors
fn close(mut self) -> Result<()> {
self.impl_close()
}
}

impl Drop for TempMount {
// But our drop is a last ditch effort
fn drop(&mut self) {
let _ = self.impl_close();
}
}

/// 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<crate::task::Task> {
let mut t = Task::new_quiet("podman");
// podman expects absolute paths for these, so use /proc/self/fd
{
let sysroot_fd: Arc<OwnedFd> = Arc::new(sysroot);
t.cmd.take_fd_n(sysroot_fd, 3);
}
{
let run_fd: Arc<OwnedFd> = 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<crate::task::Task> {
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<Self> {
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<Self> {
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 })
}

/// View this storage as a directory.
#[allow(dead_code)]
pub(crate) fn as_dir(&self) -> &Dir {
&self.root
}

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 = TempMount::new(&self.root)?;
let temp_mount_path = temp_mount.path();
// 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_path}+{tmp_runroot}]");
Task::new(format!("Copying image to target: {}", image), "skopeo")
.arg("copy")
.arg(format!("containers-storage:{image}"))
.arg(format!("{storage_dest}{image}"))
.run()?;
temp_mount.close()?;
Ok(())
}
}
96 changes: 37 additions & 59 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ostree::Sysroot> {
#[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
Expand Down Expand Up @@ -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_all(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")?;
Expand All @@ -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")]
Expand Down Expand Up @@ -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
Expand All @@ -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(())
}

Expand Down Expand Up @@ -1397,10 +1367,18 @@ 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?;
// We must drop the sysroot here in order to close any open file
// descriptors.
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 and imgstore here in order to close any
// open file descriptors.
}

// Finalize mounted filesystems
Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ pub mod spec;

#[cfg(feature = "docgen")]
mod docgen;
mod imgstorage;

0 comments on commit bbb3fc1

Please sign in to comment.