1use std::collections::HashSet;
15use std::io::{Seek, Write};
16use std::os::unix::process::CommandExt;
17use std::process::{Command, Stdio};
18use std::sync::Arc;
19
20use anyhow::{Context, Result};
21use bootc_utils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt};
22use camino::{Utf8Path, Utf8PathBuf};
23use cap_std_ext::cap_std::fs::Dir;
24use cap_std_ext::cap_tempfile::TempDir;
25use cap_std_ext::cmdext::{CapStdExtCommandExt, CmdFds};
26use cap_std_ext::dirext::CapStdExtDirExt;
27use cap_std_ext::{cap_std, cap_tempfile};
28use fn_error_context::context;
29use ostree_ext::ostree::{self};
30use std::os::fd::OwnedFd;
31use tokio::process::Command as AsyncCommand;
32
33const SUBCMD_ARGV_CHUNKING: usize = 100;
36
37pub(crate) const STORAGE_ALIAS_DIR: &str = "/run/bootc/storage";
42pub(crate) const STORAGE_RUN_FD: i32 = 3;
44
45const LABELED: &str = ".bootc_labeled";
46
47const SYS_CSTOR_PATH: &str = "/var/lib/containers/storage";
50
51pub(crate) const SUBPATH: &str = "storage";
53const RUNROOT: &str = "bootc/storage";
56
57pub(crate) struct CStorage {
59 sysroot: Dir,
61 storage_root: Dir,
63 run: Dir,
65 sepolicy: Option<ostree::SePolicy>,
67 _unsync: std::cell::Cell<()>,
72}
73
74#[derive(Debug, PartialEq, Eq)]
75pub(crate) enum PullMode {
76 IfNotExists,
78 #[allow(dead_code)]
80 Always,
81}
82
83#[allow(unsafe_code)]
84#[context("Binding storage roots")]
85pub(crate) fn bind_storage_roots(
86 cmd: &mut Command,
87 fds: &mut CmdFds,
88 storage_root: &Dir,
89 run_root: &Dir,
90) -> Result<()> {
91 let storage_root = Arc::new(storage_root.try_clone().context("Cloning storage root")?);
100 let run_root: Arc<OwnedFd> = Arc::new(run_root.try_clone().context("Cloning runroot")?.into());
101 unsafe {
103 cmd.pre_exec(move || {
104 use rustix::fs::{Mode, OFlags};
105 let oldwd = rustix::fs::open(
120 ".",
121 OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::RDONLY,
122 Mode::empty(),
123 )?;
124 rustix::process::fchdir(&storage_root)?;
125 rustix::thread::unshare_unsafe(rustix::thread::UnshareFlags::NEWNS)?;
126 rustix::mount::mount_bind(".", STORAGE_ALIAS_DIR)?;
127 rustix::process::fchdir(&oldwd)?;
128 Ok(())
129 })
130 };
131 fds.take_fd_n(run_root, STORAGE_RUN_FD);
132 Ok(())
133}
134
135pub(crate) fn setup_auth(cmd: &mut Command, fds: &mut CmdFds, sysroot: &Dir) -> Result<()> {
141 let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?;
142 let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd).map(std::io::BufWriter::new)?;
143
144 let authfile_fd = ostree_ext::globals::get_global_authfile(sysroot)?.map(|v| v.1);
147 if let Some(mut fd) = authfile_fd {
148 std::io::copy(&mut fd, &mut tempfile)?;
149 } else {
150 tempfile.write_all(b"{}")?;
153 }
154
155 let tempfile = tempfile
156 .into_inner()
157 .map_err(|e| e.into_error())?
158 .into_std();
159 let fd: Arc<OwnedFd> = std::sync::Arc::new(tempfile.into());
160 let target_fd = fds.take_fd(fd);
161 cmd.env("REGISTRY_AUTH_FILE", format!("/proc/self/fd/{target_fd}"));
162
163 Ok(())
164}
165
166fn new_podman_cmd_in(sysroot: &Dir, storage_root: &Dir, run_root: &Dir) -> Result<Command> {
170 let mut cmd = Command::new(bootc_utils::podman_bin());
171 let mut fds = CmdFds::new();
172 bind_storage_roots(&mut cmd, &mut fds, storage_root, run_root)?;
173 let run_root = format!("/proc/self/fd/{STORAGE_RUN_FD}");
174 cmd.args(["--root", STORAGE_ALIAS_DIR, "--runroot", run_root.as_str()]);
175 setup_auth(&mut cmd, &mut fds, sysroot)?;
176 cmd.take_fds(fds);
177 Ok(cmd)
178}
179
180pub fn set_additional_image_store<'c>(
183 cmd: &'c mut Command,
184 ais: impl AsRef<Utf8Path>,
185) -> &'c mut Command {
186 let ais = ais.as_ref();
187 let storage_opt = format!("additionalimagestore={ais}");
188 cmd.env("STORAGE_OPTS", storage_opt)
189}
190
191pub(crate) fn ensure_floating_c_storage_initialized() {
204 if let Err(e) = Command::new(bootc_utils::podman_bin())
205 .args(["system", "info"])
206 .stdout(Stdio::null())
207 .run_capture_stderr()
208 {
209 tracing::warn!("Failed to query podman system info: {e}");
213 }
214}
215
216impl CStorage {
217 pub(crate) fn new_image_cmd(&self) -> Result<Command> {
220 let mut r = new_podman_cmd_in(&self.sysroot, &self.storage_root, &self.run)?;
221 r.arg("image");
223 Ok(r)
224 }
225
226 fn init_globals() -> Result<()> {
227 std::fs::create_dir_all(STORAGE_ALIAS_DIR)
229 .with_context(|| format!("Creating {STORAGE_ALIAS_DIR}"))?;
230 Ok(())
231 }
232
233 #[context("Labeling imgstorage dirs")]
237 pub(crate) fn ensure_labeled(&self) -> Result<()> {
238 if self.storage_root.try_exists(LABELED)? {
239 return Ok(());
240 }
241 let Some(sepolicy) = self.sepolicy.as_ref() else {
242 return Ok(());
243 };
244
245 crate::lsm::relabel_recurse(
248 &self.storage_root,
249 ".",
250 Some(Utf8Path::new(SYS_CSTOR_PATH)),
251 sepolicy,
252 )
253 .context("labeling storage root")?;
254
255 rustix::fs::fsync(
257 self.storage_root
258 .reopen_as_ownedfd()
259 .context("Reopening as owned fd")?,
260 )
261 .context("fsync")?;
262
263 self.storage_root.create(LABELED)?;
264
265 crate::lsm::relabel(
267 &self.storage_root,
268 &self.storage_root.symlink_metadata(LABELED)?,
269 LABELED.into(),
270 Some(&Utf8Path::new(SYS_CSTOR_PATH).join(LABELED)),
271 sepolicy,
272 )
273 .context("labeling stamp file")?;
274
275 rustix::fs::fsync(
277 self.storage_root
278 .reopen_as_ownedfd()
279 .context("Reopening as owned fd")?,
280 )
281 .context("fsync")?;
282
283 Ok(())
284 }
285
286 #[context("Creating imgstorage")]
287 pub(crate) fn create(
288 sysroot: &Dir,
289 run: &Dir,
290 sepolicy: Option<&ostree::SePolicy>,
291 ) -> Result<Self> {
292 Self::init_globals()?;
293 let subpath = &Self::subpath();
294
295 let parent = subpath.parent().unwrap();
297 let tmp = format!("{subpath}.tmp");
298 let existed = sysroot
299 .try_exists(subpath)
300 .with_context(|| format!("Querying {subpath}"))?;
301 if !existed {
302 sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
303 sysroot
304 .create_dir_all(parent)
305 .with_context(|| format!("Creating {parent}"))?;
306 sysroot.create_dir_all(&tmp).context("Creating tmpdir")?;
307 let storage_root = sysroot.open_dir(&tmp).context("Open tmp")?;
308
309 new_podman_cmd_in(&sysroot, &storage_root, &run)?
313 .stdout(Stdio::null())
314 .arg("images")
315 .run_capture_stderr()
316 .context("Initializing images")?;
317 drop(storage_root);
318 sysroot
319 .rename(&tmp, sysroot, subpath)
320 .context("Renaming tmpdir")?;
321 tracing::debug!("Created image store");
322 }
323
324 let s = Self::open(sysroot, run, sepolicy.cloned())?;
325 if existed {
326 s.ensure_labeled()?;
331 }
332 Ok(s)
333 }
334
335 #[context("Opening imgstorage")]
336 pub(crate) fn open(
337 sysroot: &Dir,
338 run: &Dir,
339 sepolicy: Option<ostree::SePolicy>,
340 ) -> Result<Self> {
341 tracing::trace!("Opening container image store");
342 Self::init_globals()?;
343 let subpath = &Self::subpath();
344 let storage_root = sysroot
345 .open_dir(subpath)
346 .with_context(|| format!("Opening {subpath}"))?;
347 run.create_dir_all(RUNROOT)
349 .with_context(|| format!("Creating {RUNROOT}"))?;
350 let run = run.open_dir(RUNROOT)?;
351 Ok(Self {
352 sysroot: sysroot.try_clone()?,
353 storage_root,
354 run,
355 sepolicy,
356 _unsync: Default::default(),
357 })
358 }
359
360 #[context("Listing images")]
361 pub(crate) async fn list_images(&self) -> Result<Vec<crate::podman::ImageListEntry>> {
362 let mut cmd = self.new_image_cmd()?;
363 cmd.args(["list", "--format=json"]);
364 cmd.stdin(Stdio::null());
365 let mut stdout = tempfile::tempfile()?;
367 cmd.stdout(stdout.try_clone()?);
368 let stderr = tempfile::tempfile()?;
370 cmd.stderr(stderr.try_clone()?);
371
372 AsyncCommand::from(cmd)
374 .status()
375 .await?
376 .check_status_with_stderr(stderr)?;
377 tokio::task::spawn_blocking(move || -> Result<_> {
380 stdout.seek(std::io::SeekFrom::Start(0))?;
381 let stdout = std::io::BufReader::new(stdout);
382 let r = serde_json::from_reader(stdout)?;
383 Ok(r)
384 })
385 .await?
386 }
387
388 #[context("Pruning")]
389 pub(crate) async fn prune_except_roots(&self, roots: &HashSet<&str>) -> Result<Vec<String>> {
390 let all_images = self.list_images().await?;
391 tracing::debug!("Images total: {}", all_images.len(),);
392 let mut garbage = Vec::new();
393 for image in all_images {
394 if image
395 .names
396 .iter()
397 .flatten()
398 .all(|name| !roots.contains(name.as_str()))
399 {
400 garbage.push(image.id);
401 }
402 }
403 tracing::debug!("Images to prune: {}", garbage.len());
404 for garbage in garbage.chunks(SUBCMD_ARGV_CHUNKING) {
405 let mut cmd = self.new_image_cmd()?;
406 cmd.stdin(Stdio::null());
407 cmd.stdout(Stdio::null());
408 cmd.arg("rm");
409 cmd.args(garbage);
410 AsyncCommand::from(cmd).run().await?;
411 }
412 Ok(garbage)
413 }
414
415 pub(crate) async fn exists(&self, image: &str) -> Result<bool> {
417 let mut cmd = AsyncCommand::from(self.new_image_cmd()?);
420 cmd.args(["exists", image]);
421 Ok(cmd.status().await?.success())
422 }
423
424 pub(crate) async fn pull(&self, image: &str, mode: PullMode) -> Result<bool> {
427 match mode {
428 PullMode::IfNotExists => {
429 if self.exists(image).await? {
430 tracing::debug!("Image is already present: {image}");
431 return Ok(false);
432 }
433 }
434 PullMode::Always => {}
435 };
436 let mut cmd = self.new_image_cmd()?;
437 cmd.stdin(Stdio::null());
438 cmd.stdout(Stdio::null());
439 cmd.args(["pull", image]);
440 tracing::debug!("Pulling image: {image}");
441 let mut cmd = AsyncCommand::from(cmd);
442 cmd.run().await.context("Failed to pull image")?;
443 Ok(true)
444 }
445
446 #[context("Pulling from host storage: {image}")]
449 pub(crate) async fn pull_from_host_storage(&self, image: &str) -> Result<()> {
450 let mut cmd = Command::new(bootc_utils::podman_bin());
451 cmd.stdin(Stdio::null());
452 cmd.stdout(Stdio::null());
453 let temp_runroot = TempDir::new(cap_std::ambient_authority())?;
455 let mut fds = CmdFds::new();
456 bind_storage_roots(&mut cmd, &mut fds, &self.storage_root, &temp_runroot)?;
457 cmd.take_fds(fds);
458
459 let storage_dest = &format!(
461 "containers-storage:[overlay@{STORAGE_ALIAS_DIR}+/proc/self/fd/{STORAGE_RUN_FD}]"
462 );
463 cmd.args(["image", "push", "--remove-signatures", image])
464 .arg(format!("{storage_dest}{image}"));
465 let mut cmd = AsyncCommand::from(cmd);
466 cmd.run().await?;
467 temp_runroot.close()?;
468 Ok(())
469 }
470
471 pub(crate) async fn pull_with_progress(&self, image: &str) -> Result<()> {
480 let client = crate::podman_client::PodmanClient::connect(
481 &self.sysroot,
482 &self.storage_root,
483 &self.run,
484 )
485 .await?;
486 client.pull_with_progress(image).await
487 }
488
489 pub(crate) fn subpath() -> Utf8PathBuf {
490 Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 static_assertions::assert_not_impl_any!(CStorage: Sync);
498}