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::{
8            DoUpgradeOpts, UpdateAction, apply_upgrade_from_downloaded,
9            discard_failed_aboot_update, do_upgrade, ensure_delta_source_present, is_image_pulled,
10            lookup_config_splitstream, validate_update,
11        },
12    },
13    cli::{SwitchOpts, imgref_for_switch},
14    progress_jsonl::ProgressWriter,
15    store::{BootedComposefs, Storage},
16};
17
18#[context("Composefs Switching")]
19pub(crate) async fn switch_composefs(
20    opts: SwitchOpts,
21    storage: &Storage,
22    booted_cfs: &BootedComposefs,
23) -> Result<()> {
24    // TODO: Handle in-place
25    let mut host = get_composefs_status(storage, booted_cfs)
26        .await
27        .context("Getting composefs deployment status")?;
28
29    let delta = crate::delta::open_opt(opts.from_delta.as_deref()).await?;
30
31    let prog: ProgressWriter = opts.progress.clone().try_into()?;
32
33    let mut do_upgrade_opts = DoUpgradeOpts {
34        soft_reboot: opts.soft_reboot,
35        apply: opts.apply,
36        download_only: opts.download_opts.download_only,
37        use_unified: false,
38        quiet: opts.quiet,
39        prog,
40        delta: delta.as_ref(),
41        uki_addon_opts: opts.uki_addon_opts.clone(),
42    };
43
44    if opts.download_opts.from_downloaded {
45        return apply_upgrade_from_downloaded(storage, booted_cfs, &host, &do_upgrade_opts).await;
46    }
47
48    let target = imgref_for_switch(&opts)?;
49    if let Some(delta) = delta.as_ref() {
50        delta.validate_image_reference(&target)?;
51    }
52
53    // There may be an existing failed update with the same name as "target", and
54    // in that case we don't want the "nothing changed" check below to trigger.
55    // Instead we want to discard the failed attempt and redo the update.
56    let failed_target_matches = host
57        .status
58        .staged
59        .as_ref()
60        .and_then(|entry| entry.image.as_ref())
61        .is_some_and(|image| image.image == target);
62    let discarded_failed_update = discard_failed_aboot_update(storage, &host)?;
63    if discarded_failed_update {
64        // Refresh host to skip discarded update
65        host = get_composefs_status(storage, booted_cfs).await?;
66    }
67    let retry_failed_target = discarded_failed_update && failed_target_matches;
68
69    let new_spec = {
70        let mut new_spec = host.spec.clone();
71        new_spec.image = Some(target.clone());
72        new_spec
73    };
74
75    if new_spec == host.spec && !retry_failed_target {
76        println!("Image specification is unchanged.");
77        if opts.apply && host.status.staged.is_some() {
78            crate::reboot::reboot()?;
79        }
80        return Ok(());
81    }
82
83    let Some(target_imgref) = new_spec.image else {
84        anyhow::bail!("Target image is undefined")
85    };
86
87    const COMPOSEFS_SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
88
89    tracing::info!(
90        message_id = COMPOSEFS_SWITCH_JOURNAL_ID,
91        bootc.operation = "switch",
92        bootc.target_image = target_imgref.to_string(),
93        bootc.apply_mode = opts.apply,
94        bootc.download_only = opts.download_opts.download_only,
95        bootc.from_downloaded = opts.download_opts.from_downloaded,
96        "Starting composefs switch operation",
97    );
98
99    let repo = &*booted_cfs.repo;
100
101    // Use unified storage if explicitly requested, or auto-detect: either the
102    // target image is already in bootc-owned containers-storage, OR the booted
103    // image is — which means the user has opted into unified storage and all
104    // subsequent operations (including switch to a new image) should use it.
105    do_upgrade_opts.use_unified = if opts.unified_storage_exp {
106        true
107    } else {
108        let booted_imgref = host.spec.image.as_ref();
109        let booted_unified = if let Some(booted) = booted_imgref {
110            crate::deploy::image_exists_in_unified_storage(storage, booted).await?
111        } else {
112            false
113        };
114        let target_unified =
115            crate::deploy::image_exists_in_unified_storage(storage, &target_imgref).await?;
116        booted_unified || target_unified
117    };
118
119    // With a delta the target is whatever the delta says it is, and we can look
120    // it up locally; without one we have to ask the registry.
121    let (image, manifest) = match &delta {
122        Some(delta) => {
123            crate::delta::reject_unified_storage(delta, do_upgrade_opts.use_unified)?;
124            ensure_delta_source_present(repo, delta)?;
125            (
126                lookup_config_splitstream(repo, delta.target_manifest().config().digest())?,
127                delta.target_manifest().clone(),
128            )
129        }
130        None => {
131            let (image, img_config, _) = is_image_pulled(repo, &target_imgref).await?;
132            (image, img_config.manifest)
133        }
134    };
135
136    if let Some(cfg_verity) = image {
137        let action = validate_update(
138            storage,
139            booted_cfs,
140            &host,
141            manifest.config().digest().as_ref(),
142            &cfg_verity,
143            true,
144        )?;
145
146        match action {
147            UpdateAction::Skip => {
148                println!("No changes in image: {target_imgref:#}");
149                return Ok(());
150            }
151
152            UpdateAction::Proceed => {
153                return do_upgrade(
154                    storage,
155                    booted_cfs,
156                    &host,
157                    &target_imgref,
158                    &do_upgrade_opts,
159                    &manifest,
160                )
161                .await;
162            }
163        }
164    }
165
166    do_upgrade(
167        storage,
168        booted_cfs,
169        &host,
170        &target_imgref,
171        &do_upgrade_opts,
172        &manifest,
173    )
174    .await?;
175
176    Ok(())
177}