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