Skip to main content

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 std::time::Duration;
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 root.is_mountpoint(&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/// Printed by `ostree container commit` to point users at its replacement.
104const CONTAINER_COMMIT_NOTICE: &str =
105    "notice: This command is deprecated, use `bootc container lint` instead.";
106
107/// How long to pause after [`CONTAINER_COMMIT_NOTICE`], so it stands out in build logs.
108const CONTAINER_COMMIT_NOTICE_DELAY: Duration = Duration::from_secs(5);
109
110/// Write the deprecation notice for `ostree container commit`.
111fn write_container_commit_notice(mut out: impl std::io::Write) -> Result<()> {
112    writeln!(out, "{CONTAINER_COMMIT_NOTICE}").context("Writing ostree container commit notice")?;
113    Ok(())
114}
115
116/// Entrypoint to the commit procedures, initially we just
117/// have one validation but we expect more in the future.
118pub(crate) async fn container_commit() -> Result<()> {
119    task::spawn_blocking(move || {
120        write_container_commit_notice(std::io::stderr().lock())?;
121        std::thread::sleep(CONTAINER_COMMIT_NOTICE_DELAY);
122        require_ostree_container()?;
123        let rootdir = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
124        prepare_ostree_commit_in(&rootdir)
125    })
126    .await?
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use camino::Utf8Path;
133
134    use cap_std_ext::cap_tempfile;
135
136    #[test]
137    fn commit() -> Result<()> {
138        let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
139
140        // Handle the empty case
141        prepare_ostree_commit_in(td).unwrap();
142        prepare_ostree_commit_in_nonstrict(td).unwrap();
143
144        let var = Utf8Path::new("var");
145        let run = Utf8Path::new("run");
146        let tmp = Utf8Path::new("tmp");
147        let vartmp_foobar = &var.join("tmp/foo/bar");
148        let runsystemd = &run.join("systemd");
149        let resolvstub = &runsystemd.join("resolv.conf");
150
151        for p in [var, run, tmp] {
152            td.create_dir(p)?;
153        }
154
155        td.create_dir_all(vartmp_foobar)?;
156        td.write(vartmp_foobar.join("a"), "somefile")?;
157        td.write(vartmp_foobar.join("b"), "somefile2")?;
158        td.create_dir_all(runsystemd)?;
159        td.write(resolvstub, "stub resolv")?;
160        prepare_ostree_commit_in(td).unwrap();
161        assert!(td.try_exists(var)?);
162        assert!(td.try_exists(var.join("tmp"))?);
163        assert!(!td.try_exists(vartmp_foobar)?);
164        assert!(td.try_exists(run)?);
165        assert!(!td.try_exists(runsystemd)?);
166
167        let systemd = run.join("systemd");
168        td.create_dir_all(&systemd)?;
169        prepare_ostree_commit_in(td).unwrap();
170        assert!(td.try_exists(var)?);
171        assert!(!td.try_exists(&systemd)?);
172
173        td.remove_dir_all(var)?;
174        td.create_dir(var)?;
175        td.write(var.join("foo"), "somefile")?;
176        prepare_ostree_commit_in(td).unwrap();
177        // Right now we don't auto-create var/tmp if it didn't exist, but maybe
178        // we will in the future.
179        assert!(!td.try_exists(var.join("tmp"))?);
180        assert!(td.try_exists(var)?);
181
182        td.write(var.join("foo"), "somefile")?;
183        prepare_ostree_commit_in_nonstrict(td).unwrap();
184        assert!(td.try_exists(var)?);
185
186        let nested = Utf8Path::new("var/lib/nested");
187        td.create_dir_all(nested)?;
188        td.write(nested.join("foo"), "test1")?;
189        td.write(nested.join("foo2"), "test2")?;
190        prepare_ostree_commit_in(td).unwrap();
191        assert!(td.try_exists(var)?);
192        assert!(td.try_exists(nested)?);
193
194        Ok(())
195    }
196
197    #[test]
198    fn container_commit_notice() -> Result<()> {
199        let mut out = Vec::new();
200        write_container_commit_notice(&mut out)?;
201        let out = String::from_utf8(out)?;
202        assert_eq!(out, format!("{CONTAINER_COMMIT_NOTICE}\n"));
203        for needle in ["deprecated", "bootc container lint"] {
204            assert!(out.contains(needle), "missing {needle:?} in {out:?}");
205        }
206        Ok(())
207    }
208}