bootc_lib/bootc_composefs/
gc.rs1use anyhow::{Context, Result};
8use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
9use cfsctl::composefs;
10use cfsctl::composefs_boot;
11use composefs::repository::GcResult;
12use composefs_boot::bootloader::EFI_EXT;
13
14use crate::{
15 bootc_composefs::{
16 boot::{BOOTC_UKI_DIR, BootType, get_type1_dir_name, get_uki_addon_dir_name, get_uki_name},
17 delete::{delete_image, delete_staged, delete_state_dir},
18 status::{get_composefs_status, get_imginfo, list_bootloader_entries},
19 },
20 composefs_consts::{STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX},
21 store::{BootedComposefs, Storage},
22};
23
24#[fn_error_context::context("Listing EROFS images")]
25fn list_erofs_images(sysroot: &Dir) -> Result<Vec<String>> {
26 let images_dir = sysroot
27 .open_dir("composefs/images")
28 .context("Opening images dir")?;
29
30 let mut images = vec![];
31
32 for entry in images_dir.entries_utf8()? {
33 let entry = entry?;
34 let name = entry.file_name()?;
35 images.push(name);
36 }
37
38 Ok(images)
39}
40
41#[fn_error_context::context("Listing state directories")]
42fn list_state_dirs(sysroot: &Dir) -> Result<Vec<String>> {
43 let state = sysroot
44 .open_dir(STATE_DIR_RELATIVE)
45 .context("Opening state dir")?;
46
47 let mut dirs = vec![];
48
49 for dir in state.entries_utf8()? {
50 let dir = dir?;
51
52 if dir.file_type()?.is_file() {
53 continue;
54 }
55
56 dirs.push(dir.file_name()?);
57 }
58
59 Ok(dirs)
60}
61
62type BootBinary = (BootType, String);
63
64#[fn_error_context::context("Collecting boot binaries")]
68fn collect_boot_binaries(storage: &Storage) -> Result<Vec<BootBinary>> {
69 let mut boot_binaries = Vec::new();
70 let boot_dir = storage.bls_boot_binaries_dir()?;
71 let esp = storage.require_esp()?;
72
73 collect_uki_binaries(&esp.fd, &mut boot_binaries)?;
75
76 collect_type1_boot_binaries(&boot_dir, &mut boot_binaries)?;
79
80 Ok(boot_binaries)
81}
82
83#[fn_error_context::context("Collecting UKI binaries")]
85fn collect_uki_binaries(boot_dir: &Dir, boot_binaries: &mut Vec<BootBinary>) -> Result<()> {
86 let Ok(Some(efi_dir)) = boot_dir.open_dir_optional(BOOTC_UKI_DIR) else {
87 return Ok(());
88 };
89
90 for entry in efi_dir.entries_utf8()? {
91 let entry = entry?;
92 let name = entry.file_name()?;
93
94 let Some(efi_name_no_prefix) = name.strip_prefix(UKI_NAME_PREFIX) else {
95 continue;
96 };
97
98 if let Some(verity) = efi_name_no_prefix.strip_suffix(EFI_EXT) {
99 boot_binaries.push((BootType::Uki, verity.into()));
100 }
101 }
102
103 Ok(())
104}
105
106#[fn_error_context::context("Collecting Type1 boot binaries")]
111fn collect_type1_boot_binaries(boot_dir: &Dir, boot_binaries: &mut Vec<BootBinary>) -> Result<()> {
112 for entry in boot_dir.entries_utf8()? {
113 let entry = entry?;
114 let dir_name = entry.file_name()?;
115
116 if !entry.file_type()?.is_dir() {
117 continue;
118 }
119
120 let Some(verity) = dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) else {
121 continue;
122 };
123
124 boot_binaries.push((BootType::Bls, verity.to_string()));
126 }
127
128 Ok(())
129}
130
131#[fn_error_context::context("Deleting kernel and initrd")]
132fn delete_kernel_initrd(storage: &Storage, dir_to_delete: &str, dry_run: bool) -> Result<()> {
133 tracing::debug!("Deleting Type1 entry {dir_to_delete}");
134
135 if dry_run {
136 return Ok(());
137 }
138
139 let boot_dir = storage.bls_boot_binaries_dir()?;
140
141 boot_dir
142 .remove_dir_all(dir_to_delete)
143 .with_context(|| anyhow::anyhow!("Deleting {dir_to_delete}"))
144}
145
146#[fn_error_context::context("Deleting UKI and UKI addons {uki_id}")]
148fn delete_uki(storage: &Storage, uki_id: &str, dry_run: bool) -> Result<()> {
149 let esp_mnt = storage.require_esp()?;
150
151 let uki_dir = esp_mnt.fd.open_dir(BOOTC_UKI_DIR)?;
154
155 for entry in uki_dir.entries_utf8()? {
156 let entry = entry?;
157 let entry_name = entry.file_name()?;
158
159 if entry_name == get_uki_name(uki_id) {
161 tracing::debug!("Deleting UKI: {}", entry_name);
162
163 if dry_run {
164 continue;
165 }
166
167 entry.remove_file().context("Deleting UKI")?;
168 } else if entry_name == get_uki_addon_dir_name(uki_id) {
169 tracing::debug!("Deleting UKI addons directory: {}", entry_name);
171
172 if dry_run {
173 continue;
174 }
175
176 uki_dir
177 .remove_dir_all(entry_name)
178 .context("Deleting UKI addons dir")?;
179 }
180 }
181
182 Ok(())
183}
184
185#[fn_error_context::context("Running composefs garbage collection")]
202pub(crate) async fn composefs_gc(
203 storage: &Storage,
204 booted_cfs: &BootedComposefs,
205 dry_run: bool,
206) -> Result<GcResult> {
207 const COMPOSEFS_GC_JOURNAL_ID: &str = "3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7";
208
209 tracing::info!(
210 message_id = COMPOSEFS_GC_JOURNAL_ID,
211 bootc.operation = "gc",
212 bootc.current_deployment = booted_cfs.cmdline.digest,
213 "Starting composefs garbage collection"
214 );
215
216 let host = get_composefs_status(storage, booted_cfs).await?;
217 let booted_cfs_status = host.require_composefs_booted()?;
218
219 let sysroot = &storage.physical_root;
220
221 let bootloader_entries = list_bootloader_entries(storage)?;
222 let boot_binaries = collect_boot_binaries(storage)?;
223
224 tracing::debug!("bootloader_entries: {bootloader_entries:?}");
225 tracing::debug!("boot_binaries: {boot_binaries:?}");
226
227 let unreferenced_boot_binaries = boot_binaries
229 .iter()
230 .filter(|bin_path| {
231 !bootloader_entries
237 .iter()
238 .any(|boot_entry| boot_entry.boot_artifact_name == bin_path.1)
243 })
244 .collect::<Vec<_>>();
245
246 tracing::debug!("unreferenced_boot_binaries: {unreferenced_boot_binaries:?}");
247
248 if unreferenced_boot_binaries
249 .iter()
250 .find(|be| be.1 == booted_cfs_status.verity)
251 .is_some()
252 {
253 anyhow::bail!(
254 "Inconsistent state. Booted binaries '{}' found for cleanup",
255 booted_cfs_status.verity
256 )
257 }
258
259 for (ty, verity) in unreferenced_boot_binaries {
260 match ty {
261 BootType::Bls => delete_kernel_initrd(storage, &get_type1_dir_name(verity), dry_run)?,
262 BootType::Uki => delete_uki(storage, verity, dry_run)?,
263 }
264 }
265
266 let images = list_erofs_images(&sysroot)?;
267
268 let orphaned_images: Vec<&String> = images
273 .iter()
274 .filter(|image| {
275 !bootloader_entries
276 .iter()
277 .any(|entry| &entry.fsverity == *image)
278 })
279 .collect();
280
281 let orphaned_bootloader_entries: Vec<&String> = bootloader_entries
283 .iter()
284 .map(|entry| &entry.fsverity)
285 .filter(|verity| !images.contains(verity))
286 .collect();
287
288 let img_bootloader_diff: Vec<&String> = orphaned_images
289 .into_iter()
290 .chain(orphaned_bootloader_entries)
291 .collect();
292
293 tracing::debug!("img_bootloader_diff: {img_bootloader_diff:#?}");
294
295 let staged = &host.status.staged;
296
297 if img_bootloader_diff.contains(&&booted_cfs_status.verity) {
298 anyhow::bail!(
299 "Inconsistent state. Booted entry '{}' found for cleanup",
300 booted_cfs_status.verity
301 )
302 }
303
304 for verity in &img_bootloader_diff {
305 tracing::debug!("Cleaning up orphaned image: {verity}");
306
307 delete_staged(staged, &img_bootloader_diff, dry_run)?;
308 delete_image(&sysroot, verity, dry_run)?;
309 delete_state_dir(&sysroot, verity, dry_run)?;
310 }
311
312 let state_dirs = list_state_dirs(&sysroot)?;
313
314 let state_img_diff = state_dirs
317 .iter()
318 .filter(|s| !images.contains(s))
319 .collect::<Vec<_>>();
320
321 for verity in &state_img_diff {
322 delete_staged(staged, &state_img_diff, dry_run)?;
323 delete_state_dir(&sysroot, verity, dry_run)?;
324 }
325
326 let mut additional_roots = vec![];
328
329 for deployment in host.list_deployments() {
330 let verity = &deployment.require_composefs()?.verity;
331
332 if img_bootloader_diff.contains(&verity) || state_img_diff.contains(&verity) {
334 continue;
335 }
336
337 let image = get_imginfo(storage, verity, None).await?;
338 let stream = format!("oci-config-{}", image.manifest.config().digest());
339
340 additional_roots.push(verity.clone());
341 additional_roots.push(stream);
342 }
343
344 let additional_roots = additional_roots
345 .iter()
346 .map(|x| x.as_str())
347 .collect::<Vec<_>>();
348
349 let gc_result = if dry_run {
351 booted_cfs.repo.gc_dry_run(&additional_roots)?
352 } else {
353 booted_cfs.repo.gc(&additional_roots)?
354 };
355
356 Ok(gc_result)
357}