Skip to main content

bootc_lib/bootc_composefs/
switch.rs

1use anyhow::{Context, Result};
2use fn_error_context::context;
3
4use crate::{
5    bootc_composefs::{
6        status::get_composefs_status,
7        update::{DoUpgradeOpts, UpdateAction, do_upgrade, is_image_pulled, validate_update},
8    },
9    cli::{SwitchOpts, imgref_for_switch},
10    store::{BootedComposefs, Storage},
11};
12
13#[context("Composefs Switching")]
14pub(crate) async fn switch_composefs(
15    opts: SwitchOpts,
16    storage: &Storage,
17    booted_cfs: &BootedComposefs,
18) -> Result<()> {
19    let target = imgref_for_switch(&opts)?;
20
21    // TODO: Handle in-place
22    let host = get_composefs_status(storage, booted_cfs)
23        .await
24        .context("Getting composefs deployment status")?;
25
26    let new_spec = {
27        let mut new_spec = host.spec.clone();
28        new_spec.image = Some(target.clone());
29        new_spec
30    };
31
32    if new_spec == host.spec {
33        println!("Image specification is unchanged.");
34        if opts.apply && host.status.staged.is_some() {
35            crate::reboot::reboot()?;
36        }
37        return Ok(());
38    }
39
40    let Some(target_imgref) = new_spec.image else {
41        anyhow::bail!("Target image is undefined")
42    };
43
44    const COMPOSEFS_SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
45
46    tracing::info!(
47        message_id = COMPOSEFS_SWITCH_JOURNAL_ID,
48        bootc.operation = "switch",
49        bootc.target_image = target_imgref.to_string(),
50        bootc.apply_mode = opts.apply,
51        "Starting composefs switch operation",
52    );
53
54    let repo = &*booted_cfs.repo;
55    let (image, img_config) = is_image_pulled(repo, &target_imgref).await?;
56
57    // Use unified storage if explicitly requested, or auto-detect: either the
58    // target image is already in bootc-owned containers-storage, OR the booted
59    // image is — which means the user has opted into unified storage and all
60    // subsequent operations (including switch to a new image) should use it.
61    let use_unified = if opts.unified_storage_exp {
62        true
63    } else {
64        let booted_imgref = host.spec.image.as_ref();
65        let booted_unified = if let Some(booted) = booted_imgref {
66            crate::deploy::image_exists_in_unified_storage(storage, booted).await?
67        } else {
68            false
69        };
70        let target_unified =
71            crate::deploy::image_exists_in_unified_storage(storage, &target_imgref).await?;
72        booted_unified || target_unified
73    };
74
75    let do_upgrade_opts = DoUpgradeOpts {
76        soft_reboot: opts.soft_reboot,
77        apply: opts.apply,
78        download_only: false,
79        use_unified,
80    };
81
82    if let Some(cfg_verity) = image {
83        let action = validate_update(
84            storage,
85            booted_cfs,
86            &host,
87            img_config.manifest.config().digest().as_ref(),
88            &cfg_verity,
89            true,
90        )?;
91
92        match action {
93            UpdateAction::Skip => {
94                println!("No changes in image: {target_imgref:#}");
95                return Ok(());
96            }
97
98            UpdateAction::Proceed => {
99                return do_upgrade(
100                    storage,
101                    booted_cfs,
102                    &host,
103                    &target_imgref,
104                    &do_upgrade_opts,
105                    &img_config.manifest,
106                )
107                .await;
108            }
109        }
110    }
111
112    do_upgrade(
113        storage,
114        booted_cfs,
115        &host,
116        &target_imgref,
117        &do_upgrade_opts,
118        &img_config.manifest,
119    )
120    .await?;
121
122    Ok(())
123}