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 std::time::Duration;
15use tokio::task;
16
17const FORCE_CLEAN_PATHS: &[&str] = &["run", "tmp", "var/tmp", "var/cache"];
19
20fn 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 if dev != rootdev {
58 tracing::trace!("Skipping entry in foreign dev {path:?}");
59 continue;
60 }
61 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
89pub fn prepare_ostree_commit_in(root: &Dir) -> Result<()> {
92 let rootdev = root.dir_metadata()?.dev();
93 clean_paths_in(root, rootdev)
94}
95
96pub fn prepare_ostree_commit_in_nonstrict(root: &Dir) -> Result<()> {
99 let rootdev = root.dir_metadata()?.dev();
100 clean_paths_in(root, rootdev)
101}
102
103const CONTAINER_COMMIT_NOTICE: &str =
105 "notice: This command is deprecated, use `bootc container lint` instead.";
106
107const CONTAINER_COMMIT_NOTICE_DELAY: Duration = Duration::from_secs(5);
109
110fn 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
116pub(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 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 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}