1use 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
16const FORCE_CLEAN_PATHS: &[&str] = &["run", "tmp", "var/tmp", "var/cache"];
18
19fn 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 if dev != rootdev {
57 tracing::trace!("Skipping entry in foreign dev {path:?}");
58 continue;
59 }
60 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
88pub fn prepare_ostree_commit_in(root: &Dir) -> Result<()> {
91 let rootdev = root.dir_metadata()?.dev();
92 clean_paths_in(root, rootdev)
93}
94
95pub fn prepare_ostree_commit_in_nonstrict(root: &Dir) -> Result<()> {
98 let rootdev = root.dir_metadata()?.dev();
99 clean_paths_in(root, rootdev)
100}
101
102pub(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 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 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}