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::container::{ImageReference, Transport};
30use ostree_ext::ostree::{self};
31use std::os::fd::OwnedFd;
32use tokio::process::Command as AsyncCommand;
33
34const SUBCMD_ARGV_CHUNKING: usize = 100;
37
38pub(crate) const STORAGE_ALIAS_DIR: &str = "/run/bootc/storage";
43pub(crate) const STORAGE_RUN_FD: i32 = 3;
45
46const LABELED: &str = ".bootc_labeled";
47
48const SYS_CSTOR_PATH: &str = "/var/lib/containers/storage";
51
52pub(crate) const SUBPATH: &str = "storage";
54const RUNROOT: &str = "bootc/storage";
57
58fn resolvable_in_storage(image: &str) -> bool {
60 match ImageReference::try_from(image) {
61 Ok(r) => r.transport == Transport::ContainerStorage,
62 Err(_) => true,
64 }
65}
66
67pub(crate) struct CStorage {
69 sysroot: Dir,
71 storage_root: Dir,
73 run: Dir,
75 sepolicy: Option<ostree::SePolicy>,
77 _unsync: std::cell::Cell<()>,
82}
83
84#[derive(Debug, PartialEq, Eq)]
85pub(crate) enum PullMode {
86 IfNotExists,
88 #[allow(dead_code)]
90 Always,
91}
92
93#[allow(unsafe_code)]
94#[context("Binding storage roots")]
95pub(crate) fn bind_storage_roots(
96 cmd: &mut Command,
97 fds: &mut CmdFds,
98 storage_root: &Dir,
99 run_root: &Dir,
100) -> Result<()> {
101 let storage_root = Arc::new(storage_root.try_clone().context("Cloning storage root")?);
110 let run_root: Arc<OwnedFd> = Arc::new(run_root.try_clone().context("Cloning runroot")?.into());
111 unsafe {
113 cmd.pre_exec(move || {
114 use rustix::fs::{Mode, OFlags};
115 let oldwd = rustix::fs::open(
131 ".",
132 OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::RDONLY,
133 Mode::empty(),
134 )?;
135 rustix::process::fchdir(&storage_root)?;
136 rustix::thread::unshare_unsafe(rustix::thread::UnshareFlags::NEWNS)?;
137 rustix::mount::mount_change(
143 c"/",
144 rustix::mount::MountPropagationFlags::PRIVATE
145 | rustix::mount::MountPropagationFlags::REC,
146 )?;
147 rustix::mount::mount_bind(".", STORAGE_ALIAS_DIR)?;
148 rustix::process::fchdir(&oldwd)?;
149 Ok(())
150 })
151 };
152 fds.take_fd_n(run_root, STORAGE_RUN_FD);
153 Ok(())
154}
155
156pub(crate) fn setup_auth(cmd: &mut Command, fds: &mut CmdFds, sysroot: &Dir) -> Result<()> {
162 let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?;
163 let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd).map(std::io::BufWriter::new)?;
164
165 let authfile_fd = ostree_ext::globals::get_global_authfile(sysroot)?.map(|v| v.1);
168 if let Some(mut fd) = authfile_fd {
169 std::io::copy(&mut fd, &mut tempfile)?;
170 } else {
171 tempfile.write_all(b"{}")?;
174 }
175
176 let tempfile = tempfile
177 .into_inner()
178 .map_err(|e| e.into_error())?
179 .into_std();
180 let fd: Arc<OwnedFd> = std::sync::Arc::new(tempfile.into());
181 let target_fd = fds.take_fd(fd);
182 cmd.env("REGISTRY_AUTH_FILE", format!("/proc/self/fd/{target_fd}"));
183
184 Ok(())
185}
186
187fn new_podman_cmd_in(sysroot: &Dir, storage_root: &Dir, run_root: &Dir) -> Result<Command> {
191 let mut cmd = Command::new(bootc_utils::podman_bin());
192 let mut fds = CmdFds::new();
193 bind_storage_roots(&mut cmd, &mut fds, storage_root, run_root)?;
194 let run_root = format!("/proc/self/fd/{STORAGE_RUN_FD}");
195 cmd.args(["--root", STORAGE_ALIAS_DIR, "--runroot", run_root.as_str()]);
196 setup_auth(&mut cmd, &mut fds, sysroot)?;
197 cmd.take_fds(fds);
198 Ok(cmd)
199}
200
201pub fn set_additional_image_store<'c>(
204 cmd: &'c mut Command,
205 ais: impl AsRef<Utf8Path>,
206) -> &'c mut Command {
207 let ais = ais.as_ref();
208 let storage_opt = format!("additionalimagestore={ais}");
209 cmd.env("STORAGE_OPTS", storage_opt)
210}
211
212pub(crate) fn ensure_floating_c_storage_initialized() {
225 if let Err(e) = Command::new(bootc_utils::podman_bin())
226 .args(["system", "info"])
227 .stdout(Stdio::null())
228 .run_capture_stderr()
229 {
230 tracing::warn!("Failed to query podman system info: {e}");
234 }
235}
236
237impl CStorage {
238 pub(crate) fn new_image_cmd(&self) -> Result<Command> {
241 let mut r = new_podman_cmd_in(&self.sysroot, &self.storage_root, &self.run)?;
242 r.arg("image");
244 Ok(r)
245 }
246
247 fn init_globals() -> Result<()> {
248 std::fs::create_dir_all(STORAGE_ALIAS_DIR)
250 .with_context(|| format!("Creating {STORAGE_ALIAS_DIR}"))?;
251 Ok(())
252 }
253
254 #[context("Labeling imgstorage dirs")]
258 pub(crate) fn ensure_labeled(&self) -> Result<()> {
259 if self.storage_root.try_exists(LABELED)? {
260 return Ok(());
261 }
262 let Some(sepolicy) = self.sepolicy.as_ref() else {
263 return Ok(());
264 };
265
266 crate::lsm::relabel_recurse(
269 &self.storage_root,
270 ".",
271 Some(Utf8Path::new(SYS_CSTOR_PATH)),
272 sepolicy,
273 )
274 .context("labeling storage root")?;
275
276 rustix::fs::fsync(
278 self.storage_root
279 .reopen_as_ownedfd()
280 .context("Reopening as owned fd")?,
281 )
282 .context("fsync")?;
283
284 self.storage_root.create(LABELED)?;
285
286 crate::lsm::relabel(
288 &self.storage_root,
289 &self.storage_root.symlink_metadata(LABELED)?,
290 LABELED.into(),
291 Some(&Utf8Path::new(SYS_CSTOR_PATH).join(LABELED)),
292 sepolicy,
293 )
294 .context("labeling stamp file")?;
295
296 rustix::fs::fsync(
298 self.storage_root
299 .reopen_as_ownedfd()
300 .context("Reopening as owned fd")?,
301 )
302 .context("fsync")?;
303
304 Ok(())
305 }
306
307 #[context("Creating imgstorage")]
308 pub(crate) fn create(
309 sysroot: &Dir,
310 run: &Dir,
311 sepolicy: Option<&ostree::SePolicy>,
312 ) -> Result<Self> {
313 Self::init_globals()?;
314
315 let bin = bootc_utils::podman_bin();
316 if !crate::utils::have_executable(bin)? {
317 anyhow::bail!(
318 "{bin} executable not found in PATH; it is required for container image storage operations"
319 );
320 }
321
322 let subpath = &Self::subpath();
323
324 let parent = subpath.parent().unwrap();
326 let tmp = format!("{subpath}.tmp");
327 let existed = sysroot
328 .try_exists(subpath)
329 .with_context(|| format!("Querying {subpath}"))?;
330 if !existed {
331 sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
332 sysroot
333 .create_dir_all(parent)
334 .with_context(|| format!("Creating {parent}"))?;
335 sysroot.create_dir_all(&tmp).context("Creating tmpdir")?;
336 let storage_root = sysroot.open_dir(&tmp).context("Open tmp")?;
337
338 new_podman_cmd_in(&sysroot, &storage_root, &run)?
342 .stdout(Stdio::null())
343 .arg("images")
344 .run_capture_stderr()
345 .context("Initializing images")?;
346 drop(storage_root);
347 sysroot
348 .rename(&tmp, sysroot, subpath)
349 .context("Renaming tmpdir")?;
350 tracing::debug!("Created image store");
351 }
352
353 let s = Self::open(sysroot, run, sepolicy.cloned())?;
354 if existed {
355 s.ensure_labeled()?;
360 }
361 Ok(s)
362 }
363
364 #[context("Opening imgstorage")]
365 pub(crate) fn open(
366 sysroot: &Dir,
367 run: &Dir,
368 sepolicy: Option<ostree::SePolicy>,
369 ) -> Result<Self> {
370 tracing::trace!("Opening container image store");
371 Self::init_globals()?;
372 let subpath = &Self::subpath();
373 let storage_root = sysroot
374 .open_dir(subpath)
375 .with_context(|| format!("Opening {subpath}"))?;
376 run.create_dir_all(RUNROOT)
378 .with_context(|| format!("Creating {RUNROOT}"))?;
379 let run = run.open_dir(RUNROOT)?;
380 Ok(Self {
381 sysroot: sysroot.try_clone()?,
382 storage_root,
383 run,
384 sepolicy,
385 _unsync: Default::default(),
386 })
387 }
388
389 #[context("Listing images")]
390 pub(crate) async fn list_images(&self) -> Result<Vec<crate::podman::ImageListEntry>> {
391 let mut cmd = self.new_image_cmd()?;
392 cmd.args(["list", "--format=json"]);
393 cmd.stdin(Stdio::null());
394 let mut stdout = tempfile::tempfile()?;
396 cmd.stdout(stdout.try_clone()?);
397 let stderr = tempfile::tempfile()?;
399 cmd.stderr(stderr.try_clone()?);
400
401 AsyncCommand::from(cmd)
403 .status()
404 .await?
405 .check_status_with_stderr(stderr)?;
406 tokio::task::spawn_blocking(move || -> Result<_> {
409 stdout.seek(std::io::SeekFrom::Start(0))?;
410 let stdout = std::io::BufReader::new(stdout);
411 let r = serde_json::from_reader(stdout)?;
412 Ok(r)
413 })
414 .await?
415 }
416
417 #[context("Pruning")]
418 pub(crate) async fn prune_except_roots(&self, roots: &HashSet<&str>) -> Result<Vec<String>> {
419 let all_images = self.list_images().await?;
420 tracing::debug!("Images total: {}", all_images.len(),);
421 let mut garbage = Vec::new();
422 for image in all_images {
423 if image
424 .names
425 .iter()
426 .flatten()
427 .all(|name| !roots.contains(name.as_str()))
428 {
429 garbage.push(image.id);
430 }
431 }
432 tracing::debug!("Images to prune: {}", garbage.len());
433 for garbage in garbage.chunks(SUBCMD_ARGV_CHUNKING) {
434 let mut cmd = self.new_image_cmd()?;
435 cmd.stdin(Stdio::null());
436 cmd.stdout(Stdio::null());
437 cmd.arg("rm");
438 cmd.args(garbage);
439 AsyncCommand::from(cmd).run().await?;
440 }
441 Ok(garbage)
442 }
443
444 pub(crate) async fn exists(&self, image: &str) -> Result<bool> {
446 if !resolvable_in_storage(image) {
449 tracing::debug!("Not a storage reference: {image}");
450 return Ok(false);
451 }
452 let mut cmd = AsyncCommand::from(self.new_image_cmd()?);
453 cmd.args(["exists", image]);
454 Ok(cmd.status().await?.success())
455 }
456
457 pub(crate) async fn pull(&self, image: &str, mode: PullMode) -> Result<bool> {
460 match mode {
461 PullMode::IfNotExists => {
462 if self.exists(image).await? {
463 tracing::debug!("Image is already present: {image}");
464 return Ok(false);
465 }
466 }
467 PullMode::Always => {}
468 };
469 let mut cmd = self.new_image_cmd()?;
470 cmd.stdin(Stdio::null());
471 cmd.stdout(Stdio::null());
472 cmd.args(["pull", image]);
473 tracing::debug!("Pulling image: {image}");
474 let mut cmd = AsyncCommand::from(cmd);
475 cmd.run().await.context("Failed to pull image")?;
476 Ok(true)
477 }
478
479 #[context("Pulling from host storage: {image}")]
482 pub(crate) async fn pull_from_host_storage(&self, image: &str) -> Result<()> {
483 let mut cmd = Command::new(bootc_utils::podman_bin());
484 cmd.stdin(Stdio::null());
485 cmd.stdout(Stdio::null());
486 let temp_runroot = TempDir::new(cap_std::ambient_authority())?;
488 let mut fds = CmdFds::new();
489 bind_storage_roots(&mut cmd, &mut fds, &self.storage_root, &temp_runroot)?;
490 cmd.take_fds(fds);
491
492 let storage_dest = &format!(
494 "containers-storage:[overlay@{STORAGE_ALIAS_DIR}+/proc/self/fd/{STORAGE_RUN_FD}]"
495 );
496 cmd.args(["image", "push", "--remove-signatures", image])
497 .arg(format!("{storage_dest}{image}"));
498 let mut cmd = AsyncCommand::from(cmd);
499 cmd.run().await?;
500 temp_runroot.close()?;
501 Ok(())
502 }
503
504 pub(crate) async fn pull_with_progress(&self, image: &str) -> Result<()> {
513 let client = crate::podman_client::PodmanClient::connect(
514 &self.sysroot,
515 &self.storage_root,
516 &self.run,
517 )
518 .await?;
519 client.pull_with_progress(image).await
520 }
521
522 pub(crate) fn subpath() -> Utf8PathBuf {
523 Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 static_assertions::assert_not_impl_any!(CStorage: Sync);
531
532 #[test]
533 fn test_resolvable_in_storage() {
534 assert!(resolvable_in_storage("quay.io/example/foo:latest"));
535 assert!(resolvable_in_storage("localhost:5000/example/foo:latest"));
536 assert!(resolvable_in_storage(
537 "containers-storage:localhost/foo:latest"
538 ));
539 assert!(!resolvable_in_storage(
540 "registry:quay.io/example/foo:latest"
541 ));
542 assert!(!resolvable_in_storage("oci:/var/tmp/image.oci:latest"));
543 assert!(!resolvable_in_storage("oci-archive:/var/tmp/foo.tar"));
544 assert!(!resolvable_in_storage("dir:/var/tmp/foo"));
545 assert!(!resolvable_in_storage(
546 "docker://quay.io/example/foo:latest"
547 ));
548 assert!(!resolvable_in_storage("docker-archive:/var/tmp/foo.tar"));
549 assert!(!resolvable_in_storage("docker-daemon:localhost/foo:latest"));
550 }
551}