Skip to content

Commit bbb3fc1

Browse files
committed
Introduce bootc-owned container store, use for bound images
WIP for #721 Signed-off-by: Colin Walters <[email protected]>
1 parent fe5225b commit bbb3fc1

File tree

4 files changed

+200
-59
lines changed

4 files changed

+200
-59
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ install:
1111
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
1212
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d
1313
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d
14+
ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage
1415
install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/
1516
ln -f $(DESTDIR)$(prefix)/bin/bootc $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
1617
install -d $(DESTDIR)$(prefix)/lib/bootc/install

lib/src/imgstorage.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! # bootc-managed container storage
2+
//!
3+
//! The default storage for this project uses ostree, canonically storing all of its state in
4+
//! `/sysroot/ostree`.
5+
//!
6+
//! This containers-storage: which canonically lives in `/sysroot/ostree/bootc`.
7+
8+
use std::sync::Arc;
9+
10+
use anyhow::{Context, Result};
11+
use camino::Utf8Path;
12+
use cap_std_ext::{
13+
cap_std::fs_utf8::Dir, cmdext::CapStdExtCommandExt, dirext::CapStdExtDirExtUtf8,
14+
};
15+
use fn_error_context::context;
16+
use std::os::fd::OwnedFd;
17+
18+
use crate::task::Task;
19+
20+
struct TempMount {
21+
dir: Option<tempfile::TempDir>,
22+
}
23+
24+
impl TempMount {
25+
#[context("Creating temp mount")]
26+
fn new(srcd: &Dir) -> Result<Self> {
27+
let dir = tempfile::tempdir()?;
28+
Task::new_quiet("mount")
29+
.args(["--bind", "."])
30+
.arg(dir.path())
31+
.cwd(srcd.as_cap_std())?
32+
.run()?;
33+
Ok(Self { dir: Some(dir) })
34+
}
35+
36+
fn path(&self) -> &Utf8Path {
37+
// SAFETY: We don't expose an unset value except on drop
38+
let dir = self.dir.as_ref().unwrap();
39+
// SAFETY: We really expect utf-8 paths
40+
dir.path().try_into().unwrap()
41+
}
42+
43+
#[context("Closing temp mount")]
44+
fn impl_close(&mut self) -> Result<()> {
45+
let Some(dir) = self.dir.take() else {
46+
return Ok(());
47+
};
48+
// We must recursively unmount because the storage stack
49+
// creates a bind mount at the target by default.
50+
Task::new_quiet("umount")
51+
.args(["-R"])
52+
.arg(dir.path())
53+
.run()?;
54+
dir.close()?;
55+
Ok(())
56+
}
57+
58+
// We expect users to pass this to close() which checks errors
59+
fn close(mut self) -> Result<()> {
60+
self.impl_close()
61+
}
62+
}
63+
64+
impl Drop for TempMount {
65+
// But our drop is a last ditch effort
66+
fn drop(&mut self) {
67+
let _ = self.impl_close();
68+
}
69+
}
70+
71+
/// The path to the storage, relative to the physical system root.
72+
pub(crate) const SUBPATH: &str = "ostree/bootc/storage";
73+
/// The path to the "runroot" with transient runtime state; this is
74+
/// relative to the /run directory
75+
const RUNROOT: &str = "bootc/storage";
76+
pub(crate) struct Storage {
77+
root: Dir,
78+
#[allow(dead_code)]
79+
run: Dir,
80+
}
81+
82+
impl Storage {
83+
fn podman_task_in(sysroot: OwnedFd, run: OwnedFd) -> Result<crate::task::Task> {
84+
let mut t = Task::new_quiet("podman");
85+
// podman expects absolute paths for these, so use /proc/self/fd
86+
{
87+
let sysroot_fd: Arc<OwnedFd> = Arc::new(sysroot);
88+
t.cmd.take_fd_n(sysroot_fd, 3);
89+
}
90+
{
91+
let run_fd: Arc<OwnedFd> = Arc::new(run);
92+
t.cmd.take_fd_n(run_fd, 4);
93+
}
94+
t = t.args(["--root=/proc/self/fd/3", "--runroot=/proc/self/fd/4"]);
95+
Ok(t)
96+
}
97+
98+
#[allow(dead_code)]
99+
fn podman_task(&self) -> Result<crate::task::Task> {
100+
let sysroot = self.root.as_cap_std().try_clone()?.into_std_file().into();
101+
let run = self.run.as_cap_std().try_clone()?.into_std_file().into();
102+
Self::podman_task_in(sysroot, run)
103+
}
104+
105+
#[context("Creating imgstorage")]
106+
pub(crate) fn create(sysroot: &Dir, run: &Dir) -> Result<Self> {
107+
let subpath = Utf8Path::new(SUBPATH);
108+
// SAFETY: We know there's a parent
109+
let parent = subpath.parent().unwrap();
110+
if !sysroot.try_exists(subpath)? {
111+
let tmp = format!("{SUBPATH}.tmp");
112+
sysroot.remove_all_optional(&tmp)?;
113+
sysroot.create_dir_all(parent)?;
114+
sysroot.create_dir_all(&tmp).context("Creating tmpdir")?;
115+
// There's no explicit API to initialize a containers-storage:
116+
// root, simply passing a path will attempt to auto-create it.
117+
// We run "podman images" in the new root.
118+
Self::podman_task_in(sysroot.open_dir(&tmp)?.into(), run.try_clone()?.into())?
119+
.arg("images")
120+
.run()?;
121+
sysroot
122+
.rename(&tmp, sysroot, subpath)
123+
.context("Renaming tmpdir")?;
124+
}
125+
Self::open(sysroot, run)
126+
}
127+
128+
#[context("Opening imgstorage")]
129+
pub(crate) fn open(sysroot: &Dir, run: &Dir) -> Result<Self> {
130+
let root = sysroot.open_dir(SUBPATH).context(SUBPATH)?;
131+
// Always auto-create this if missing
132+
run.create_dir_all(RUNROOT)?;
133+
let run = run.open_dir(RUNROOT).context(RUNROOT)?;
134+
Ok(Self { root, run })
135+
}
136+
137+
/// View this storage as a directory.
138+
#[allow(dead_code)]
139+
pub(crate) fn as_dir(&self) -> &Dir {
140+
&self.root
141+
}
142+
143+
pub(crate) fn pull_from_host_storage(&self, image: &str) -> Result<()> {
144+
// The skopeo API expects absolute paths, so we make a temporary bind
145+
let temp_mount = TempMount::new(&self.root)?;
146+
let temp_mount_path = temp_mount.path();
147+
// And an ephemeral place for the transient state
148+
let tmp_runroot = tempfile::tempdir()?;
149+
let tmp_runroot: &Utf8Path = tmp_runroot.path().try_into()?;
150+
151+
// The destination (target stateroot) + container storage dest
152+
let storage_dest = &format!("containers-storage:[overlay@{temp_mount_path}+{tmp_runroot}]");
153+
Task::new(format!("Copying image to target: {}", image), "skopeo")
154+
.arg("copy")
155+
.arg(format!("containers-storage:{image}"))
156+
.arg(format!("{storage_dest}{image}"))
157+
.run()?;
158+
temp_mount.close()?;
159+
Ok(())
160+
}
161+
}

lib/src/install.rs

Lines changed: 37 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub(crate) mod config;
1111
pub(crate) mod osconfig;
1212

1313
use std::io::Write;
14-
use std::os::fd::{AsFd, OwnedFd};
14+
use std::os::fd::AsFd;
1515
use std::os::unix::process::CommandExt;
1616
use std::path::Path;
1717
use std::process::Command;
@@ -24,9 +24,9 @@ use anyhow::{anyhow, Context, Result};
2424
use camino::Utf8Path;
2525
use camino::Utf8PathBuf;
2626
use cap_std::fs::{Dir, MetadataExt};
27+
use cap_std::fs_utf8::Dir as DirUtf8;
2728
use cap_std_ext::cap_std;
2829
use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
29-
use cap_std_ext::cmdext::CapStdExtCommandExt;
3030
use cap_std_ext::prelude::CapStdExtDirExt;
3131
use chrono::prelude::*;
3232
use clap::ValueEnum;
@@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize};
4242

4343
use self::baseline::InstallBlockDeviceOpts;
4444
use crate::containerenv::ContainerExecutionInfo;
45+
use crate::imgstorage::Storage;
4546
use crate::mount::Filesystem;
4647
use crate::spec::ImageReference;
4748
use crate::task::Task;
@@ -548,8 +549,11 @@ pub(crate) fn print_configuration() -> Result<()> {
548549
serde_json::to_writer(stdout, &install_config).map_err(Into::into)
549550
}
550551

551-
#[context("Creating ostree deployment")]
552-
async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<ostree::Sysroot> {
552+
#[context("Creating system root")]
553+
async fn initialize_ostree_root(
554+
state: &State,
555+
root_setup: &RootSetup,
556+
) -> Result<(ostree::Sysroot, crate::imgstorage::Storage)> {
553557
let sepolicy = state.load_policy()?;
554558
let sepolicy = sepolicy.as_ref();
555559
// Load a fd for the mounted target physical root
@@ -594,6 +598,16 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
594598
.cwd(rootfs_dir)?
595599
.run()?;
596600

601+
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
602+
sysroot.load(cancellable)?;
603+
let sysroot_dir = DirUtf8::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?;
604+
605+
let tmp_run = cap_std_ext::cap_tempfile::utf8::TempDir::new(cap_std::ambient_authority())?;
606+
sysroot_dir
607+
.create_dir_all(Utf8Path::new(crate::imgstorage::SUBPATH).parent().unwrap())
608+
.context("creating bootc dir")?;
609+
let imgstore = crate::imgstorage::Storage::create(&sysroot_dir, &*tmp_run)?;
610+
597611
// Bootstrap the initial labeling of the /ostree directory as usr_t
598612
if let Some(policy) = sepolicy {
599613
let ostree_dir = rootfs_dir.open_dir("ostree")?;
@@ -606,9 +620,7 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
606620
)?;
607621
}
608622

609-
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
610-
sysroot.load(cancellable)?;
611-
Ok(sysroot)
623+
Ok((sysroot, imgstore))
612624
}
613625

614626
#[context("Creating ostree deployment")]
@@ -1271,14 +1283,14 @@ async fn install_with_sysroot(
12711283
state: &State,
12721284
rootfs: &RootSetup,
12731285
sysroot: &ostree::Sysroot,
1286+
imgstore: &Storage,
12741287
boot_uuid: &str,
12751288
bound_images: &[crate::boundimage::ResolvedBoundImage],
12761289
) -> Result<()> {
12771290
let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
12781291
// And actually set up the container in that root, returning a deployment and
12791292
// the aleph state (see below).
1280-
let (deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
1281-
let stateroot = deployment.osname();
1293+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
12821294
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
12831295
rootfs
12841296
.rootfs_fd
@@ -1301,53 +1313,11 @@ async fn install_with_sysroot(
13011313
tracing::debug!("Installed bootloader");
13021314

13031315
tracing::debug!("Perfoming post-deployment operations");
1304-
if !bound_images.is_empty() {
1305-
// TODO: We shouldn't hardcode the overlay driver for source or
1306-
// target, but we currently need to in order to reference the location.
1307-
// For this one, containers-storage: is actually the *host*'s /var/lib/containers
1308-
// which we are accessing directly.
1309-
let storage_src = "containers-storage:";
1310-
// TODO: We only do this dance to initialize `/var` at install time if
1311-
// there are bound images today; it minimizes side effects.
1312-
// However going forward we really do need to handle a separate /var partition...
1313-
// and to do that we may in the general case need to run the `var.mount`
1314-
// target from the new root.
1315-
// Probably the best fix is for us to switch bound images to use the bootc storage.
1316-
let varpath = format!("ostree/deploy/{stateroot}/var");
1317-
let var = rootfs
1318-
.rootfs_fd
1319-
.open_dir(&varpath)
1320-
.with_context(|| format!("Opening {varpath}"))?;
1321-
1322-
// The skopeo API expects absolute paths, so we make a temporary bind
1323-
let tmp_dest_var_abs = tempfile::tempdir()?;
1324-
let tmp_dest_var_abs: &Utf8Path = tmp_dest_var_abs.path().try_into()?;
1325-
let mut t = Task::new("Mounting deployment /var", "mount")
1326-
.args(["--bind", "/proc/self/fd/3"])
1327-
.arg(tmp_dest_var_abs);
1328-
t.cmd.take_fd_n(Arc::new(OwnedFd::from(var)), 3);
1329-
t.run()?;
1330-
1331-
// And an ephemeral place for the transient state
1332-
let tmp_runroot = tempfile::tempdir()?;
1333-
let tmp_runroot: &Utf8Path = tmp_runroot.path().try_into()?;
1334-
1335-
// The destination (target stateroot) + container storage dest
1336-
let storage_dest = &format!(
1337-
"containers-storage:[overlay@{tmp_dest_var_abs}/lib/containers/storage+{tmp_runroot}]"
1338-
);
1339-
1340-
// Now copy each bound image from the host's container storage into the target.
1341-
for image in bound_images {
1342-
let image = image.image.as_str();
1343-
Task::new(format!("Copying image to target: {}", image), "skopeo")
1344-
.arg("copy")
1345-
.arg(format!("{storage_src}{image}"))
1346-
.arg(format!("{storage_dest}{image}"))
1347-
.run()?;
1348-
}
1316+
// Now copy each bound image from the host's container storage into the target.
1317+
for image in bound_images {
1318+
let image = image.image.as_str();
1319+
imgstore.pull_from_host_storage(image)?;
13491320
}
1350-
13511321
Ok(())
13521322
}
13531323

@@ -1397,10 +1367,18 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13971367

13981368
// Initialize the ostree sysroot (repo, stateroot, etc.)
13991369
{
1400-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1401-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1402-
// We must drop the sysroot here in order to close any open file
1403-
// descriptors.
1370+
let (sysroot, imgstore) = initialize_ostree_root(state, rootfs).await?;
1371+
install_with_sysroot(
1372+
state,
1373+
rootfs,
1374+
&sysroot,
1375+
&imgstore,
1376+
&boot_uuid,
1377+
&bound_images,
1378+
)
1379+
.await?;
1380+
// We must drop the sysroot and imgstore here in order to close any
1381+
// open file descriptors.
14041382
}
14051383

14061384
// Finalize mounted filesystems

lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ pub mod spec;
4545

4646
#[cfg(feature = "docgen")]
4747
mod docgen;
48+
mod imgstorage;

0 commit comments

Comments
 (0)