1use std::collections::HashSet;
12use std::io::{Seek, Write};
13use std::os::unix::process::CommandExt;
14use std::process::{Command, Stdio};
15use std::sync::Arc;
16
17use anyhow::{Context, Result};
18use bootc_utils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt};
19use camino::{Utf8Path, Utf8PathBuf};
20use cap_std_ext::cap_std::fs::Dir;
21use cap_std_ext::cap_tempfile::TempDir;
22use cap_std_ext::cmdext::CapStdExtCommandExt;
23use cap_std_ext::dirext::CapStdExtDirExt;
24use cap_std_ext::{cap_std, cap_tempfile};
25use fn_error_context::context;
26use ostree_ext::ostree::{self};
27use std::os::fd::{AsFd, AsRawFd, OwnedFd};
28use tokio::process::Command as AsyncCommand;
29
30const SUBCMD_ARGV_CHUNKING: usize = 100;
33
34pub(crate) const STORAGE_ALIAS_DIR: &str = "/run/bootc/storage";
39const STORAGE_RUN_FD: i32 = 3;
41
42const LABELED: &str = ".bootc_labeled";
43
44const SYS_CSTOR_PATH: &str = "/var/lib/containers/storage";
47
48pub(crate) const SUBPATH: &str = "storage";
50const RUNROOT: &str = "bootc/storage";
53
54pub(crate) struct CStorage {
56 sysroot: Dir,
58 storage_root: Dir,
60 #[allow(dead_code)]
61 run: Dir,
63 sepolicy: Option<ostree::SePolicy>,
65 _unsync: std::cell::Cell<()>,
70}
71
72#[derive(Debug, PartialEq, Eq)]
73pub(crate) enum PullMode {
74 IfNotExists,
76 #[allow(dead_code)]
78 Always,
79}
80
81#[allow(unsafe_code)]
82#[context("Binding storage roots")]
83fn bind_storage_roots(cmd: &mut Command, storage_root: &Dir, run_root: &Dir) -> Result<()> {
84 let storage_root = Arc::new(storage_root.try_clone().context("Cloning storage root")?);
93 let run_root: Arc<OwnedFd> = Arc::new(run_root.try_clone().context("Cloning runroot")?.into());
94 unsafe {
96 cmd.pre_exec(move || {
97 use rustix::fs::{Mode, OFlags};
98 let oldwd = rustix::fs::open(
113 ".",
114 OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::RDONLY,
115 Mode::empty(),
116 )?;
117 rustix::process::fchdir(&storage_root)?;
118 rustix::thread::unshare_unsafe(rustix::thread::UnshareFlags::NEWNS)?;
119 rustix::mount::mount_bind(".", STORAGE_ALIAS_DIR)?;
120 rustix::process::fchdir(&oldwd)?;
121 Ok(())
122 })
123 };
124 cmd.take_fd_n(run_root, STORAGE_RUN_FD);
125 Ok(())
126}
127
128fn new_podman_cmd_in(sysroot: &Dir, storage_root: &Dir, run_root: &Dir) -> Result<Command> {
132 let mut cmd = Command::new("podman");
133 bind_storage_roots(&mut cmd, storage_root, run_root)?;
134 let run_root = format!("/proc/self/fd/{STORAGE_RUN_FD}");
135 cmd.args(["--root", STORAGE_ALIAS_DIR, "--runroot", run_root.as_str()]);
136
137 let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?;
138 let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd).map(std::io::BufWriter::new)?;
139
140 let authfile_fd = ostree_ext::globals::get_global_authfile(sysroot)?.map(|v| v.1);
143 if let Some(mut fd) = authfile_fd {
144 std::io::copy(&mut fd, &mut tempfile)?;
145 } else {
146 tempfile.write_all(b"{}")?;
149 }
150
151 let tempfile = tempfile
152 .into_inner()
153 .map_err(|e| e.into_error())?
154 .into_std();
155 let fd: Arc<OwnedFd> = std::sync::Arc::new(tempfile.into());
156 let target_fd = fd.as_fd().as_raw_fd();
157 cmd.take_fd_n(fd, target_fd);
158 cmd.env("REGISTRY_AUTH_FILE", format!("/proc/self/fd/{target_fd}"));
159
160 Ok(cmd)
161}
162
163pub fn set_additional_image_store<'c>(
166 cmd: &'c mut Command,
167 ais: impl AsRef<Utf8Path>,
168) -> &'c mut Command {
169 let ais = ais.as_ref();
170 let storage_opt = format!("additionalimagestore={ais}");
171 cmd.env("STORAGE_OPTS", storage_opt)
172}
173
174pub(crate) fn ensure_floating_c_storage_initialized() {
187 if let Err(e) = Command::new("podman")
188 .args(["system", "info"])
189 .stdout(Stdio::null())
190 .run_capture_stderr()
191 {
192 tracing::warn!("Failed to query podman system info: {e}");
196 }
197}
198
199impl CStorage {
200 pub(crate) fn new_image_cmd(&self) -> Result<Command> {
203 let mut r = new_podman_cmd_in(&self.sysroot, &self.storage_root, &self.run)?;
204 r.arg("image");
206 Ok(r)
207 }
208
209 fn init_globals() -> Result<()> {
210 std::fs::create_dir_all(STORAGE_ALIAS_DIR)
212 .with_context(|| format!("Creating {STORAGE_ALIAS_DIR}"))?;
213 Ok(())
214 }
215
216 #[context("Labeling imgstorage dirs")]
220 pub(crate) fn ensure_labeled(&self) -> Result<()> {
221 if self.storage_root.try_exists(LABELED)? {
222 return Ok(());
223 }
224 let Some(sepolicy) = self.sepolicy.as_ref() else {
225 return Ok(());
226 };
227
228 crate::lsm::relabel_recurse(
231 &self.storage_root,
232 ".",
233 Some(Utf8Path::new(SYS_CSTOR_PATH)),
234 sepolicy,
235 )
236 .context("labeling storage root")?;
237
238 rustix::fs::fsync(
240 self.storage_root
241 .reopen_as_ownedfd()
242 .context("Reopening as owned fd")?,
243 )
244 .context("fsync")?;
245
246 self.storage_root.create(LABELED)?;
247
248 crate::lsm::relabel(
250 &self.storage_root,
251 &self.storage_root.symlink_metadata(LABELED)?,
252 LABELED.into(),
253 Some(&Utf8Path::new(SYS_CSTOR_PATH).join(LABELED)),
254 sepolicy,
255 )
256 .context("labeling stamp file")?;
257
258 rustix::fs::fsync(
260 self.storage_root
261 .reopen_as_ownedfd()
262 .context("Reopening as owned fd")?,
263 )
264 .context("fsync")?;
265
266 Ok(())
267 }
268
269 #[context("Creating imgstorage")]
270 pub(crate) fn create(
271 sysroot: &Dir,
272 run: &Dir,
273 sepolicy: Option<&ostree::SePolicy>,
274 ) -> Result<Self> {
275 Self::init_globals()?;
276 let subpath = &Self::subpath();
277
278 let parent = subpath.parent().unwrap();
280 let tmp = format!("{subpath}.tmp");
281 let existed = sysroot
282 .try_exists(subpath)
283 .with_context(|| format!("Querying {subpath}"))?;
284 if !existed {
285 sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
286 sysroot
287 .create_dir_all(parent)
288 .with_context(|| format!("Creating {parent}"))?;
289 sysroot.create_dir_all(&tmp).context("Creating tmpdir")?;
290 let storage_root = sysroot.open_dir(&tmp).context("Open tmp")?;
291
292 new_podman_cmd_in(&sysroot, &storage_root, &run)?
296 .stdout(Stdio::null())
297 .arg("images")
298 .run_capture_stderr()
299 .context("Initializing images")?;
300 drop(storage_root);
301 sysroot
302 .rename(&tmp, sysroot, subpath)
303 .context("Renaming tmpdir")?;
304 tracing::debug!("Created image store");
305 }
306
307 let s = Self::open(sysroot, run, sepolicy.cloned())?;
308 if existed {
309 s.ensure_labeled()?;
314 }
315 Ok(s)
316 }
317
318 #[context("Opening imgstorage")]
319 pub(crate) fn open(
320 sysroot: &Dir,
321 run: &Dir,
322 sepolicy: Option<ostree::SePolicy>,
323 ) -> Result<Self> {
324 tracing::trace!("Opening container image store");
325 Self::init_globals()?;
326 let subpath = &Self::subpath();
327 let storage_root = sysroot
328 .open_dir(subpath)
329 .with_context(|| format!("Opening {subpath}"))?;
330 run.create_dir_all(RUNROOT)
332 .with_context(|| format!("Creating {RUNROOT}"))?;
333 let run = run.open_dir(RUNROOT)?;
334 Ok(Self {
335 sysroot: sysroot.try_clone()?,
336 storage_root,
337 run,
338 sepolicy,
339 _unsync: Default::default(),
340 })
341 }
342
343 #[context("Listing images")]
344 pub(crate) async fn list_images(&self) -> Result<Vec<crate::podman::ImageListEntry>> {
345 let mut cmd = self.new_image_cmd()?;
346 cmd.args(["list", "--format=json"]);
347 cmd.stdin(Stdio::null());
348 let mut stdout = tempfile::tempfile()?;
350 cmd.stdout(stdout.try_clone()?);
351 let stderr = tempfile::tempfile()?;
353 cmd.stderr(stderr.try_clone()?);
354
355 AsyncCommand::from(cmd)
357 .status()
358 .await?
359 .check_status_with_stderr(stderr)?;
360 tokio::task::spawn_blocking(move || -> Result<_> {
363 stdout.seek(std::io::SeekFrom::Start(0))?;
364 let stdout = std::io::BufReader::new(stdout);
365 let r = serde_json::from_reader(stdout)?;
366 Ok(r)
367 })
368 .await?
369 }
370
371 #[context("Pruning")]
372 pub(crate) async fn prune_except_roots(&self, roots: &HashSet<&str>) -> Result<Vec<String>> {
373 let all_images = self.list_images().await?;
374 tracing::debug!("Images total: {}", all_images.len(),);
375 let mut garbage = Vec::new();
376 for image in all_images {
377 if image
378 .names
379 .iter()
380 .flatten()
381 .all(|name| !roots.contains(name.as_str()))
382 {
383 garbage.push(image.id);
384 }
385 }
386 tracing::debug!("Images to prune: {}", garbage.len());
387 for garbage in garbage.chunks(SUBCMD_ARGV_CHUNKING) {
388 let mut cmd = self.new_image_cmd()?;
389 cmd.stdin(Stdio::null());
390 cmd.stdout(Stdio::null());
391 cmd.arg("rm");
392 cmd.args(garbage);
393 AsyncCommand::from(cmd).run().await?;
394 }
395 Ok(garbage)
396 }
397
398 pub(crate) async fn exists(&self, image: &str) -> Result<bool> {
400 let mut cmd = AsyncCommand::from(self.new_image_cmd()?);
403 cmd.args(["exists", image]);
404 Ok(cmd.status().await?.success())
405 }
406
407 pub(crate) async fn pull(&self, image: &str, mode: PullMode) -> Result<bool> {
410 match mode {
411 PullMode::IfNotExists => {
412 if self.exists(image).await? {
413 tracing::debug!("Image is already present: {image}");
414 return Ok(false);
415 }
416 }
417 PullMode::Always => {}
418 };
419 let mut cmd = self.new_image_cmd()?;
420 cmd.stdin(Stdio::null());
421 cmd.stdout(Stdio::null());
422 cmd.args(["pull", image]);
423 tracing::debug!("Pulling image: {image}");
424 let mut cmd = AsyncCommand::from(cmd);
425 cmd.run().await.context("Failed to pull image")?;
426 Ok(true)
427 }
428
429 #[context("Pulling from host storage: {image}")]
432 pub(crate) async fn pull_from_host_storage(&self, image: &str) -> Result<()> {
433 let mut cmd = Command::new("podman");
434 cmd.stdin(Stdio::null());
435 cmd.stdout(Stdio::null());
436 let temp_runroot = TempDir::new(cap_std::ambient_authority())?;
438 bind_storage_roots(&mut cmd, &self.storage_root, &temp_runroot)?;
439
440 let storage_dest = &format!(
442 "containers-storage:[overlay@{STORAGE_ALIAS_DIR}+/proc/self/fd/{STORAGE_RUN_FD}]"
443 );
444 cmd.args(["image", "push", "--remove-signatures", image])
445 .arg(format!("{storage_dest}{image}"));
446 let mut cmd = AsyncCommand::from(cmd);
447 cmd.run().await?;
448 temp_runroot.close()?;
449 Ok(())
450 }
451
452 pub(crate) fn subpath() -> Utf8PathBuf {
453 Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 static_assertions::assert_not_impl_any!(CStorage: Sync);
461}