bootc_lib/bootc_composefs/
update.rs1use anyhow::{Context, Result};
2use camino::Utf8PathBuf;
3use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
4use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
5use composefs_boot::BootOps;
6use composefs_oci::image::create_filesystem;
7use fn_error_context::context;
8use ocidir::cap_std::ambient_authority;
9use ostree_ext::container::ManifestDiff;
10
11use crate::{
12 bootc_composefs::{
13 boot::{BootSetupType, BootType, setup_composefs_bls_boot, setup_composefs_uki_boot},
14 repo::{get_imgref, pull_composefs_repo},
15 service::start_finalize_stated_svc,
16 soft_reboot::prepare_soft_reboot_composefs,
17 state::write_composefs_state,
18 status::{
19 ImgConfigManifest, StagedDeployment, get_bootloader, get_composefs_status,
20 get_container_manifest_and_config, get_imginfo,
21 },
22 },
23 cli::{SoftRebootMode, UpgradeOpts},
24 composefs_consts::{
25 COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE,
26 TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED,
27 },
28 spec::{Bootloader, Host, ImageReference},
29 store::{BootedComposefs, ComposefsRepository, Storage},
30};
31
32#[context("Checking if image {} is pulled", imgref.image)]
51pub(crate) async fn is_image_pulled(
52 repo: &ComposefsRepository,
53 imgref: &ImageReference,
54) -> Result<(Option<Sha512HashValue>, ImgConfigManifest)> {
55 let imgref_repr = get_imgref(&imgref.transport, &imgref.image);
56 let img_config_manifest = get_container_manifest_and_config(&imgref_repr).await?;
57
58 let img_digest = img_config_manifest.manifest.config().digest().digest();
59
60 let img_id = format!("oci-config-sha256:{img_digest}");
62
63 let container_pulled = repo.has_stream(&img_id).context("Checking stream")?;
65
66 Ok((container_pulled, img_config_manifest))
67}
68
69fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> {
70 if boot_dir.exists(TYPE1_ENT_PATH_STAGED) {
71 boot_dir
72 .remove_dir_all(TYPE1_ENT_PATH_STAGED)
73 .context("Removing staged bootloader entry")?;
74 }
75
76 Ok(())
77}
78
79#[derive(Debug)]
80pub(crate) enum UpdateAction {
81 Skip,
83 Proceed,
85 UpdateOrigin,
88}
89
90pub(crate) fn validate_update(
128 storage: &Storage,
129 booted_cfs: &BootedComposefs,
130 host: &Host,
131 img_digest: &str,
132 config_verity: &Sha512HashValue,
133 is_switch: bool,
134) -> Result<UpdateAction> {
135 let repo = &*booted_cfs.repo;
136
137 let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?;
138 fs.transform_for_boot(&repo)?;
139
140 let image_id = fs.compute_image_id();
141
142 if image_id.to_hex() == *booted_cfs.cmdline.digest {
152 let ret = if is_switch {
153 UpdateAction::UpdateOrigin
154 } else {
155 UpdateAction::Skip
156 };
157
158 return Ok(ret);
159 }
160
161 let all_deployments = host.all_composefs_deployments()?;
162
163 let found_depl = all_deployments
164 .iter()
165 .find(|d| d.deployment.verity == image_id.to_hex());
166
167 if found_depl.is_some() {
169 return Ok(UpdateAction::Skip);
170 }
171
172 let booted = host.require_composefs_booted()?;
173 let boot_dir = storage.require_boot_dir()?;
174
175 match get_bootloader()? {
178 Bootloader::Grub => match booted.boot_type {
179 BootType::Bls => rm_staged_type1_ent(boot_dir)?,
180
181 BootType::Uki => {
182 let grub = boot_dir.open_dir("grub2").context("Opening grub dir")?;
183
184 if grub.exists(USER_CFG_STAGED) {
185 grub.remove_file(USER_CFG_STAGED)
186 .context("Removing staged grub user config")?;
187 }
188 }
189 },
190
191 Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?,
192 }
193
194 let state_dir = storage
196 .physical_root
197 .open_dir(STATE_DIR_RELATIVE)
198 .context("Opening state dir")?;
199
200 if state_dir.exists(image_id.to_hex()) {
201 state_dir
202 .remove_dir_all(image_id.to_hex())
203 .context("Removing state")?;
204 }
205
206 Ok(UpdateAction::Proceed)
207}
208
209pub(crate) struct DoUpgradeOpts {
211 pub(crate) apply: bool,
212 pub(crate) soft_reboot: Option<SoftRebootMode>,
213 pub(crate) download_only: bool,
214}
215
216async fn apply_upgrade(
217 storage: &Storage,
218 booted_cfs: &BootedComposefs,
219 depl_id: &String,
220 opts: &DoUpgradeOpts,
221) -> Result<()> {
222 if let Some(soft_reboot_mode) = opts.soft_reboot {
223 return prepare_soft_reboot_composefs(
224 storage,
225 booted_cfs,
226 Some(depl_id),
227 soft_reboot_mode,
228 opts.apply,
229 )
230 .await;
231 };
232
233 if opts.apply {
234 return crate::reboot::reboot();
235 }
236
237 Ok(())
238}
239
240#[context("Performing Upgrade Operation")]
242pub(crate) async fn do_upgrade(
243 storage: &Storage,
244 booted_cfs: &BootedComposefs,
245 host: &Host,
246 imgref: &ImageReference,
247 img_manifest_config: &ImgConfigManifest,
248 opts: &DoUpgradeOpts,
249) -> Result<()> {
250 start_finalize_stated_svc()?;
251
252 let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?;
253
254 let Some(entry) = entries.iter().next() else {
255 anyhow::bail!("No boot entries!");
256 };
257
258 let mounted_fs = Dir::reopen_dir(
259 &repo
260 .mount(&id.to_hex())
261 .context("Failed to mount composefs image")?,
262 )?;
263
264 let boot_type = BootType::from(entry);
265
266 let boot_digest = match boot_type {
267 BootType::Bls => setup_composefs_bls_boot(
268 BootSetupType::Upgrade((storage, &fs, &host)),
269 repo,
270 &id,
271 entry,
272 &mounted_fs,
273 )?,
274
275 BootType::Uki => setup_composefs_uki_boot(
276 BootSetupType::Upgrade((storage, &fs, &host)),
277 repo,
278 &id,
279 entries,
280 )?,
281 };
282
283 write_composefs_state(
284 &Utf8PathBuf::from("/sysroot"),
285 &id,
286 imgref,
287 Some(StagedDeployment {
288 depl_id: id.to_hex(),
289 finalization_locked: opts.download_only,
290 }),
291 boot_type,
292 boot_digest,
293 img_manifest_config,
294 )
295 .await?;
296
297 apply_upgrade(storage, booted_cfs, &id.to_hex(), opts).await
298}
299
300#[context("Upgrading composefs")]
301pub(crate) async fn upgrade_composefs(
302 opts: UpgradeOpts,
303 storage: &Storage,
304 composefs: &BootedComposefs,
305) -> Result<()> {
306 let host = get_composefs_status(storage, composefs)
307 .await
308 .context("Getting composefs deployment status")?;
309
310 let do_upgrade_opts = DoUpgradeOpts {
311 soft_reboot: opts.soft_reboot,
312 apply: opts.apply,
313 download_only: opts.download_only,
314 };
315
316 if opts.from_downloaded {
317 let staged = host
318 .status
319 .staged
320 .as_ref()
321 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
322
323 if !staged.download_only {
325 println!("Staged deployment is present and not in download only mode.");
326 println!("Use `bootc update --apply` to apply the update.");
327 return Ok(());
328 }
329
330 start_finalize_stated_svc()?;
331
332 let new_staged = StagedDeployment {
334 depl_id: staged.require_composefs()?.verity.clone(),
335 finalization_locked: false,
336 };
337
338 let staged_depl_dir =
339 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
340 .context("Opening transient state directory")?;
341
342 staged_depl_dir
343 .atomic_replace_with(
344 COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
345 |f| -> std::io::Result<()> {
346 serde_json::to_writer(f, &new_staged).map_err(std::io::Error::from)
347 },
348 )
349 .context("Writing staged file")?;
350
351 return apply_upgrade(
352 storage,
353 composefs,
354 &staged.require_composefs()?.verity,
355 &do_upgrade_opts,
356 )
357 .await;
358 }
359
360 let mut booted_imgref = host
361 .spec
362 .image
363 .as_ref()
364 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
365
366 let repo = &*composefs.repo;
367
368 let (img_pulled, mut img_config) = is_image_pulled(&repo, booted_imgref).await?;
369 let booted_img_digest = img_config.manifest.config().digest().digest().to_owned();
370
371 let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref());
374
375 if let Some(staged_image) = staged_image {
376 if staged_image.image_digest == booted_img_digest {
379 if opts.apply {
380 return crate::reboot::reboot();
381 }
382
383 println!("Update already staged. To apply update run `bootc update --apply`");
384
385 return Ok(());
386 }
387
388 booted_imgref = &staged_image.image;
392
393 let (img_pulled, staged_img_config) = is_image_pulled(&repo, booted_imgref).await?;
394 img_config = staged_img_config;
395
396 if let Some(cfg_verity) = img_pulled {
397 let action = validate_update(
398 storage,
399 composefs,
400 &host,
401 img_config.manifest.config().digest().digest(),
402 &cfg_verity,
403 false,
404 )?;
405
406 match action {
407 UpdateAction::Skip => {
408 println!("No changes in staged image: {booted_imgref:#}");
409 return Ok(());
410 }
411
412 UpdateAction::Proceed => {
413 return do_upgrade(
414 storage,
415 composefs,
416 &host,
417 booted_imgref,
418 &img_config,
419 &do_upgrade_opts,
420 )
421 .await;
422 }
423
424 UpdateAction::UpdateOrigin => {
425 anyhow::bail!("Updating origin not supported for update operation")
426 }
427 }
428 }
429 }
430
431 if let Some(cfg_verity) = img_pulled {
433 let action = validate_update(
434 storage,
435 composefs,
436 &host,
437 &booted_img_digest,
438 &cfg_verity,
439 false,
440 )?;
441
442 match action {
443 UpdateAction::Skip => {
444 println!("No changes in: {booted_imgref:#}");
445 return Ok(());
446 }
447
448 UpdateAction::Proceed => {
449 return do_upgrade(
450 storage,
451 composefs,
452 &host,
453 booted_imgref,
454 &img_config,
455 &do_upgrade_opts,
456 )
457 .await;
458 }
459
460 UpdateAction::UpdateOrigin => {
461 anyhow::bail!("Updating origin not supported for update operation")
462 }
463 }
464 }
465
466 if opts.check {
467 let current_manifest =
468 get_imginfo(storage, &*composefs.cmdline.digest, Some(booted_imgref)).await?;
469 let diff = ManifestDiff::new(¤t_manifest.manifest, &img_config.manifest);
470 diff.print();
471 return Ok(());
472 }
473
474 do_upgrade(
475 storage,
476 composefs,
477 &host,
478 booted_imgref,
479 &img_config,
480 &do_upgrade_opts,
481 )
482 .await?;
483
484 Ok(())
485}