Skip to content

Commit ea78d29

Browse files
committed
Initialize a containers-storage: owned by bootc, use for bound images
Initial work for: #721 - Initialize a containers-storage: instance at install time (that defaults to empty) - "Open" it (but do nothing with it) as part of the core CLI operations Further APIs and work will build on top of this. Signed-off-by: Colin Walters <[email protected]>
1 parent c39b664 commit ea78d29

File tree

13 files changed

+349
-76
lines changed

13 files changed

+349
-76
lines changed

Makefile

+1
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/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ liboverdrop = "0.1.0"
3030
libsystemd = "0.7"
3131
openssl = "^0.10.64"
3232
regex = "1.10.4"
33-
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] }
33+
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process", "mount"] }
3434
schemars = { version = "0.8.17", features = ["chrono"] }
3535
serde = { workspace = true, features = ["derive"] }
3636
serde_ignored = "0.1.10"

lib/src/boundimage.rs

+16-13
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
//! pre-pulled (and in the future, pinned) before a new image root
66
//! is considered ready.
77
8-
use crate::task::Task;
98
use anyhow::{Context, Result};
109
use camino::Utf8Path;
1110
use cap_std_ext::cap_std::fs::Dir;
1211
use cap_std_ext::dirext::CapStdExtDirExt;
1312
use fn_error_context::context;
1413
use ostree_ext::containers_image_proxy;
1514
use ostree_ext::ostree::Deployment;
16-
use ostree_ext::sysroot::SysrootLock;
15+
16+
use crate::imgstorage::PullMode;
17+
use crate::store::Storage;
1718

1819
/// The path in a root for bound images; this directory should only contain
1920
/// symbolic links to `.container` or `.image` files.
@@ -37,10 +38,10 @@ pub(crate) struct ResolvedBoundImage {
3738
}
3839

3940
/// Given a deployment, pull all container images it references.
40-
pub(crate) fn pull_bound_images(sysroot: &SysrootLock, deployment: &Deployment) -> Result<()> {
41+
pub(crate) async fn pull_bound_images(sysroot: &Storage, deployment: &Deployment) -> Result<()> {
4142
let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
4243
let bound_images = query_bound_images(deployment_root)?;
43-
pull_images(deployment_root, bound_images)
44+
pull_images(sysroot, bound_images).await
4445
}
4546

4647
#[context("Querying bound images")]
@@ -133,18 +134,20 @@ fn parse_container_file(file_contents: &tini::Ini) -> Result<BoundImage> {
133134
Ok(bound_image)
134135
}
135136

136-
#[context("pull bound images")]
137-
pub(crate) fn pull_images(_deployment_root: &Dir, bound_images: Vec<BoundImage>) -> Result<()> {
137+
#[context("Pulling bound images")]
138+
pub(crate) async fn pull_images(sysroot: &Storage, bound_images: Vec<BoundImage>) -> Result<()> {
138139
tracing::debug!("Pulling bound images: {}", bound_images.len());
139140
//TODO: do this in parallel
140141
for bound_image in bound_images {
141-
let mut task = Task::new("Pulling bound image", "/usr/bin/podman")
142-
.arg("pull")
143-
.arg(&bound_image.image);
144-
if let Some(auth_file) = &bound_image.auth_file {
145-
task = task.arg("--authfile").arg(auth_file);
146-
}
147-
task.run()?;
142+
let image = &bound_image.image;
143+
let desc = format!("Updating bound image: {image}");
144+
crate::utils::async_task_with_spinner(&desc, async move {
145+
sysroot
146+
.imgstore
147+
.pull(&bound_image.image, PullMode::IfNotExists)
148+
.await
149+
})
150+
.await?;
148151
}
149152

150153
Ok(())

lib/src/cli.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,12 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
430430
Ok(sysroot)
431431
}
432432

433+
/// Load global storage state, expecting that we're booted into a bootc system.
433434
#[context("Initializing storage")]
434435
pub(crate) async fn get_storage() -> Result<crate::store::Storage> {
436+
let global_run = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
435437
let sysroot = get_locked_sysroot().await?;
436-
crate::store::Storage::new(sysroot)
438+
crate::store::Storage::new(sysroot, &global_run)
437439
}
438440

439441
#[context("Querying root privilege")]

lib/src/deploy.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ pub(crate) async fn stage(
399399
)
400400
.await?;
401401

402-
crate::boundimage::pull_bound_images(sysroot, &deployment)?;
402+
crate::boundimage::pull_bound_images(sysroot, &deployment).await?;
403403

404404
crate::deploy::cleanup(sysroot).await?;
405405
println!("Queued for next boot: {:#}", spec.image);

lib/src/image.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,29 @@ use anyhow::{Context, Result};
66
use fn_error_context::context;
77
use ostree_ext::container::{ImageReference, Transport};
88

9+
use crate::utils::CommandRunExt;
10+
911
/// The name of the image we push to containers-storage if nothing is specified.
1012
const IMAGE_DEFAULT: &str = "localhost/bootc";
1113

1214
#[context("Listing images")]
1315
pub(crate) async fn list_entrypoint() -> Result<()> {
14-
let sysroot = crate::cli::get_locked_sysroot().await?;
16+
let sysroot = crate::cli::get_storage().await?;
1517
let repo = &sysroot.repo();
1618

1719
let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;
1820

21+
println!("# Host images");
1922
for image in images {
2023
println!("{image}");
2124
}
25+
println!("");
26+
27+
println!("# Logically bound images");
28+
let mut listcmd = sysroot.imgstore.new_image_cmd()?;
29+
listcmd.arg("list");
30+
listcmd.run()?;
31+
2232
Ok(())
2333
}
2434

lib/src/imgstorage.rs

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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::io::{Read, Seek};
9+
use std::os::unix::process::CommandExt;
10+
use std::process::Command;
11+
use std::sync::Arc;
12+
13+
use anyhow::{Context, Result};
14+
use camino::Utf8Path;
15+
use cap_std_ext::cap_std;
16+
use cap_std_ext::cap_std::fs::Dir;
17+
use cap_std_ext::cap_tempfile::TempDir;
18+
use cap_std_ext::dirext::CapStdExtDirExt;
19+
use fn_error_context::context;
20+
use rustix::io::DupFlags;
21+
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
22+
use tokio::process::Command as AsyncCommand;
23+
24+
use crate::utils::{AsyncCommandRunExt, CommandRunExt};
25+
26+
/// Global directory path which we use for podman to point
27+
/// it at our storage.
28+
pub(crate) const STORAGE_ALIAS_DIR: &str = "/run/bootc/storage";
29+
/// And a similar alias for the runtime state. The 3 here is hardcoded,
30+
/// and set up in a fork below too.
31+
pub(crate) const STORAGE_RUN_ALIAS_DIR: &str = "/proc/self/fd/3";
32+
const STORAGE_RUN_FD: i32 = 3;
33+
34+
/// The path to the storage, relative to the physical system root.
35+
pub(crate) const SUBPATH: &str = "ostree/bootc/storage";
36+
/// The path to the "runroot" with transient runtime state; this is
37+
/// relative to the /run directory
38+
const RUNROOT: &str = "bootc/storage";
39+
pub(crate) struct Storage {
40+
/// The root directory
41+
sysroot: Dir,
42+
/// The location of container storage
43+
storage_root: Dir,
44+
#[allow(dead_code)]
45+
/// Our runtime state
46+
run: Dir,
47+
}
48+
49+
#[derive(Debug, PartialEq, Eq)]
50+
pub(crate) enum PullMode {
51+
/// Pull only if the image is not present
52+
IfNotExists,
53+
/// Always check for an update
54+
#[allow(dead_code)]
55+
Always,
56+
}
57+
58+
async fn run_cmd_async(cmd: Command) -> Result<()> {
59+
let mut cmd = tokio::process::Command::from(cmd);
60+
cmd.kill_on_drop(true);
61+
let mut stderr = tempfile::tempfile()?;
62+
cmd.stderr(stderr.try_clone()?);
63+
if let Err(e) = cmd.run().await {
64+
stderr.seek(std::io::SeekFrom::Start(0))?;
65+
let mut stderr_buf = String::new();
66+
// Ignore errors
67+
let _ = stderr.read_to_string(&mut stderr_buf);
68+
return Err(anyhow::anyhow!("{e}: {stderr_buf}"));
69+
}
70+
Ok(())
71+
}
72+
73+
#[allow(unsafe_code)]
74+
#[context("Binding storage roots")]
75+
fn bind_storage_roots(cmd: &mut Command, storage_root: &Dir, run_root: &Dir) -> Result<()> {
76+
// podman requires an absolute path, for two reasons right now:
77+
// - It writes the file paths into `db.sql`, a sqlite database for unknown reasons
78+
// - It forks helper binaries, so just giving it /proc/self/fd won't work as
79+
// those helpers may not get the fd passed. (which is also true of skopeo)
80+
// We create a new mount namespace, which also has the helpful side effect
81+
// of automatically cleaning up the global bind mount that the storage stack
82+
// creates.
83+
84+
let storage_root = Arc::new(storage_root.try_clone().context("Cloning storage root")?);
85+
let run_root = Arc::new(run_root.try_clone().context("Cloning runroot")?);
86+
// SAFETY: All the APIs we call here are safe to invoke between fork and exec.
87+
unsafe {
88+
cmd.pre_exec(move || {
89+
// Set our working directory here, because this is the only way I could
90+
// get the mount() below to work.
91+
rustix::process::fchdir(&storage_root)?;
92+
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWNS)?;
93+
rustix::mount::mount_bind(".", STORAGE_ALIAS_DIR)?;
94+
// Set up the runtime dir via /proc/self/fd, which works because it's
95+
// not passed to any child processes. Note that we dup it without
96+
// setting O_CLOEXEC intentionally
97+
let mut ofd = OwnedFd::from_raw_fd(STORAGE_RUN_FD);
98+
rustix::io::dup3(run_root.as_fd(), &mut ofd, DupFlags::empty())?;
99+
// We didn't actually "own" this fd to start with
100+
std::mem::forget(ofd);
101+
Ok(())
102+
})
103+
};
104+
Ok(())
105+
}
106+
107+
fn new_podman_cmd_in(storage_root: &Dir, run_root: &Dir) -> Result<Command> {
108+
let mut cmd = Command::new("podman");
109+
bind_storage_roots(&mut cmd, storage_root, run_root)?;
110+
cmd.args([
111+
"--root",
112+
STORAGE_ALIAS_DIR,
113+
"--runroot",
114+
STORAGE_RUN_ALIAS_DIR,
115+
]);
116+
Ok(cmd)
117+
}
118+
119+
impl Storage {
120+
/// Create a `podman image` Command instance prepared to operate on our alternative
121+
/// root.
122+
pub(crate) fn new_image_cmd(&self) -> Result<Command> {
123+
let mut r = new_podman_cmd_in(&self.storage_root, &self.run)?;
124+
// We want to limit things to only manipulating images by default.
125+
r.arg("image");
126+
Ok(r)
127+
}
128+
129+
fn init_globals() -> Result<()> {
130+
// Ensure our global storage alias dirs exist
131+
for d in [STORAGE_ALIAS_DIR] {
132+
std::fs::create_dir_all(d).with_context(|| format!("Creating {d}"))?;
133+
}
134+
Ok(())
135+
}
136+
137+
#[context("Creating imgstorage")]
138+
pub(crate) fn create(sysroot: &Dir, run: &Dir) -> Result<Self> {
139+
Self::init_globals()?;
140+
let subpath = Utf8Path::new(SUBPATH);
141+
// SAFETY: We know there's a parent
142+
let parent = subpath.parent().unwrap();
143+
if !sysroot
144+
.try_exists(subpath)
145+
.with_context(|| format!("Querying {subpath}"))?
146+
{
147+
let tmp = format!("{SUBPATH}.tmp");
148+
sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
149+
sysroot
150+
.create_dir_all(parent)
151+
.with_context(|| format!("Creating {parent}"))?;
152+
sysroot.create_dir_all(&tmp).context("Creating tmpdir")?;
153+
let storage_root = sysroot.open_dir(&tmp).context("Open tmp")?;
154+
// There's no explicit API to initialize a containers-storage:
155+
// root, simply passing a path will attempt to auto-create it.
156+
// We run "podman images" in the new root.
157+
new_podman_cmd_in(&storage_root, &run)?
158+
.arg("images")
159+
.run()
160+
.context("Initializing images")?;
161+
drop(storage_root);
162+
sysroot
163+
.rename(&tmp, sysroot, subpath)
164+
.context("Renaming tmpdir")?;
165+
}
166+
Self::open(sysroot, run)
167+
}
168+
169+
#[context("Opening imgstorage")]
170+
pub(crate) fn open(sysroot: &Dir, run: &Dir) -> Result<Self> {
171+
Self::init_globals()?;
172+
let storage_root = sysroot
173+
.open_dir(SUBPATH)
174+
.with_context(|| format!("Opening {SUBPATH}"))?;
175+
// Always auto-create this if missing
176+
run.create_dir_all(RUNROOT)
177+
.with_context(|| format!("Creating {RUNROOT}"))?;
178+
let run = run.open_dir(RUNROOT)?;
179+
Ok(Self {
180+
sysroot: sysroot.try_clone()?,
181+
storage_root,
182+
run,
183+
})
184+
}
185+
186+
/// Fetch the image if it is not already present; return whether
187+
/// or not the image was fetched.
188+
pub(crate) async fn pull(&self, image: &str, mode: PullMode) -> Result<bool> {
189+
match mode {
190+
PullMode::IfNotExists => {
191+
// Sadly https://docs.rs/containers-image-proxy/latest/containers_image_proxy/struct.ImageProxy.html#method.open_image_optional
192+
// doesn't work with containers-storage yet
193+
let mut cmd = AsyncCommand::from(self.new_image_cmd()?);
194+
cmd.args(["exists", image]);
195+
let exists = cmd.status().await?.success();
196+
if exists {
197+
return Ok(false);
198+
}
199+
}
200+
PullMode::Always => {}
201+
};
202+
let mut cmd = self.new_image_cmd()?;
203+
cmd.args(["pull", image]);
204+
let authfile = ostree_ext::globals::get_global_authfile(&self.sysroot)?
205+
.map(|(authfile, _fd)| authfile);
206+
if let Some(authfile) = authfile {
207+
cmd.args(["--authfile", authfile.as_str()]);
208+
}
209+
run_cmd_async(cmd).await.context("Failed to pull image")?;
210+
Ok(true)
211+
}
212+
213+
pub(crate) async fn pull_from_host_storage(&self, image: &str) -> Result<()> {
214+
let mut cmd = Command::new("podman");
215+
// An ephemeral place for the transient state
216+
let temp_runroot = TempDir::new(cap_std::ambient_authority())?;
217+
bind_storage_roots(&mut cmd, &self.storage_root, &temp_runroot)?;
218+
219+
// The destination (target stateroot) + container storage dest
220+
let storage_dest =
221+
&format!("containers-storage:[overlay@{STORAGE_ALIAS_DIR}+{STORAGE_RUN_ALIAS_DIR}]");
222+
cmd.args(["image", "push", image])
223+
.arg(format!("{storage_dest}{image}"));
224+
run_cmd_async(cmd).await?;
225+
temp_runroot.close()?;
226+
Ok(())
227+
}
228+
}

0 commit comments

Comments
 (0)