ostree_ext/
commit.rs

1//! This module contains the functions to implement the commit
2//! procedures as part of building an ostree container image.
3//! <https://github.com/ostreedev/ostree-rs-ext/issues/159>
4
5use crate::container_utils::require_ostree_container;
6use crate::mountutil::is_mountpoint;
7use anyhow::Context;
8use anyhow::Result;
9use cap_std::fs::Dir;
10use cap_std::fs::MetadataExt;
11use cap_std_ext::cap_std;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use std::path::Path;
14use std::path::PathBuf;
15use tokio::task;
16
17/// Directories for which we will always remove all content.
18const FORCE_CLEAN_PATHS: &[&str] = &["run", "tmp", "var/tmp", "var/cache"];
19
20/// Recursively remove the target directory, but avoid traversing across mount points.
21fn remove_all_on_mount_recurse(root: &Dir, rootdev: u64, path: &Path) -> Result<bool> {
22    let mut skipped = false;
23    for entry in root
24        .read_dir(path)
25        .with_context(|| format!("Reading {path:?}"))?
26    {
27        let entry = entry?;
28        let metadata = entry.metadata()?;
29        if metadata.dev() != rootdev {
30            skipped = true;
31            continue;
32        }
33        let name = entry.file_name();
34        let path = &path.join(name);
35
36        if metadata.is_dir() {
37            skipped |= remove_all_on_mount_recurse(root, rootdev, path.as_path())?;
38        } else {
39            root.remove_file(path)
40                .with_context(|| format!("Removing {path:?}"))?;
41        }
42    }
43    if !skipped {
44        root.remove_dir(path)
45            .with_context(|| format!("Removing {path:?}"))?;
46    }
47    Ok(skipped)
48}
49
50fn clean_subdir(root: &Dir, rootdev: u64) -> Result<()> {
51    for entry in root.entries()? {
52        let entry = entry?;
53        let metadata = entry.metadata()?;
54        let dev = metadata.dev();
55        let path = PathBuf::from(entry.file_name());
56        // Ignore other filesystem mounts, e.g. podman injects /run/.containerenv
57        if dev != rootdev {
58            tracing::trace!("Skipping entry in foreign dev {path:?}");
59            continue;
60        }
61        // Also ignore bind mounts, if we have a new enough kernel with statx()
62        // that will tell us.
63        if is_mountpoint(root, &path)?.unwrap_or_default() {
64            tracing::trace!("Skipping mount point {path:?}");
65            continue;
66        }
67        if metadata.is_dir() {
68            remove_all_on_mount_recurse(root, rootdev, &path)?;
69        } else {
70            root.remove_file(&path)
71                .with_context(|| format!("Removing {path:?}"))?;
72        }
73    }
74    Ok(())
75}
76
77fn clean_paths_in(root: &Dir, rootdev: u64) -> Result<()> {
78    for path in FORCE_CLEAN_PATHS {
79        let subdir = if let Some(subdir) = root.open_dir_optional(path)? {
80            subdir
81        } else {
82            continue;
83        };
84        clean_subdir(&subdir, rootdev).with_context(|| format!("Cleaning {path}"))?;
85    }
86    Ok(())
87}
88
89/// Given a root filesystem, clean out empty directories and warn about
90/// files in /var.  /run, /tmp, and /var/tmp have their contents recursively cleaned.
91pub fn prepare_ostree_commit_in(root: &Dir) -> Result<()> {
92    let rootdev = root.dir_metadata()?.dev();
93    clean_paths_in(root, rootdev)
94}
95
96/// Like [`prepare_ostree_commit_in`] but only emits warnings about unsupported
97/// files in `/var` and will not error.
98pub fn prepare_ostree_commit_in_nonstrict(root: &Dir) -> Result<()> {
99    let rootdev = root.dir_metadata()?.dev();
100    clean_paths_in(root, rootdev)
101}
102
103/// Entrypoint to the commit procedures, initially we just
104/// have one validation but we expect more in the future.
105pub(crate) async fn container_commit() -> Result<()> {
106    task::spawn_blocking(move || {
107        require_ostree_container()?;
108        let rootdir = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
109        prepare_ostree_commit_in(&rootdir)
110    })
111    .await?
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use camino::Utf8Path;
118
119    use cap_std_ext::cap_tempfile;
120
121    #[test]
122    fn commit() -> Result<()> {
123        let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
124
125        // Handle the empty case
126        prepare_ostree_commit_in(td).unwrap();
127        prepare_ostree_commit_in_nonstrict(td).unwrap();
128
129        let var = Utf8Path::new("var");
130        let run = Utf8Path::new("run");
131        let tmp = Utf8Path::new("tmp");
132        let vartmp_foobar = &var.join("tmp/foo/bar");
133        let runsystemd = &run.join("systemd");
134        let resolvstub = &runsystemd.join("resolv.conf");
135
136        for p in [var, run, tmp] {
137            td.create_dir(p)?;
138        }
139
140        td.create_dir_all(vartmp_foobar)?;
141        td.write(vartmp_foobar.join("a"), "somefile")?;
142        td.write(vartmp_foobar.join("b"), "somefile2")?;
143        td.create_dir_all(runsystemd)?;
144        td.write(resolvstub, "stub resolv")?;
145        prepare_ostree_commit_in(td).unwrap();
146        assert!(td.try_exists(var)?);
147        assert!(td.try_exists(var.join("tmp"))?);
148        assert!(!td.try_exists(vartmp_foobar)?);
149        assert!(td.try_exists(run)?);
150        assert!(!td.try_exists(runsystemd)?);
151
152        let systemd = run.join("systemd");
153        td.create_dir_all(&systemd)?;
154        prepare_ostree_commit_in(td).unwrap();
155        assert!(td.try_exists(var)?);
156        assert!(!td.try_exists(&systemd)?);
157
158        td.remove_dir_all(var)?;
159        td.create_dir(var)?;
160        td.write(var.join("foo"), "somefile")?;
161        prepare_ostree_commit_in(td).unwrap();
162        // Right now we don't auto-create var/tmp if it didn't exist, but maybe
163        // we will in the future.
164        assert!(!td.try_exists(var.join("tmp"))?);
165        assert!(td.try_exists(var)?);
166
167        td.write(var.join("foo"), "somefile")?;
168        prepare_ostree_commit_in_nonstrict(td).unwrap();
169        assert!(td.try_exists(var)?);
170
171        let nested = Utf8Path::new("var/lib/nested");
172        td.create_dir_all(nested)?;
173        td.write(nested.join("foo"), "test1")?;
174        td.write(nested.join("foo2"), "test2")?;
175        prepare_ostree_commit_in(td).unwrap();
176        assert!(td.try_exists(var)?);
177        assert!(td.try_exists(nested)?);
178
179        Ok(())
180    }
181}