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