|
1 | 1 | use anyhow::{Context, Result}; |
2 | 2 | use camino::Utf8PathBuf; |
| 3 | +use composefs::util::{parse_sha256, Sha256Digest}; |
3 | 4 | use fn_error_context::context; |
| 5 | +use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest}; |
4 | 6 |
|
5 | 7 | use crate::{ |
6 | 8 | bootc_composefs::{ |
7 | 9 | boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, |
8 | | - repo::pull_composefs_repo, |
| 10 | + repo::{get_imgref, open_composefs_repo, pull_composefs_repo}, |
9 | 11 | service::start_finalize_stated_svc, |
10 | 12 | state::write_composefs_state, |
11 | | - status::composefs_deployment_status, |
| 13 | + status::{composefs_deployment_status, get_container_manifest_and_config}, |
12 | 14 | }, |
13 | 15 | cli::UpgradeOpts, |
| 16 | + spec::ImageReference, |
| 17 | + store::ComposefsRepository, |
14 | 18 | }; |
15 | 19 |
|
| 20 | +use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; |
| 21 | + |
| 22 | +#[context("Getting SHA256 Digest for {id}")] |
| 23 | +pub fn str_to_sha256digest(id: &str) -> Result<Sha256Digest> { |
| 24 | + let id = if id.starts_with("sha256:") { |
| 25 | + id.strip_prefix("sha256:").unwrap() |
| 26 | + } else { |
| 27 | + id |
| 28 | + }; |
| 29 | + |
| 30 | + Ok(parse_sha256(&id)?) |
| 31 | +} |
| 32 | + |
| 33 | +/// Checks if a container image has been pulled to the local composefs repository. |
| 34 | +/// |
| 35 | +/// This function verifies whether the specified container image exists in the local |
| 36 | +/// composefs repository by checking if the image's configuration digest stream is |
| 37 | +/// available. It retrieves the image manifest and configuration from the container |
| 38 | +/// registry and uses the configuration digest to perform the local availability check. |
| 39 | +/// |
| 40 | +/// # Arguments |
| 41 | +/// |
| 42 | +/// * `repo` - The composefs repository |
| 43 | +/// * `imgref` - Reference to the container image to check |
| 44 | +/// |
| 45 | +/// # Returns |
| 46 | +/// |
| 47 | +/// Returns a tuple containing: |
| 48 | +/// * `true` if the image is pulled/available locally, `false` otherwise |
| 49 | +/// * The container image manifest |
| 50 | +/// * The container image configuration |
| 51 | +#[context("Checking if image {} is pulled", imgref.image)] |
| 52 | +async fn is_image_pulled( |
| 53 | + repo: &ComposefsRepository, |
| 54 | + imgref: &ImageReference, |
| 55 | +) -> Result<(bool, ImageManifest, ImageConfiguration)> { |
| 56 | + let imgref_repr = get_imgref(&imgref.transport, &imgref.image); |
| 57 | + let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?; |
| 58 | + |
| 59 | + let img_digest = manifest.config().digest().digest(); |
| 60 | + let img_sha256 = str_to_sha256digest(&img_digest)?; |
| 61 | + |
| 62 | + // check_stream is expensive to run, but probably a good idea |
| 63 | + let container_pulled = repo.check_stream(&img_sha256).context("Checking stream")?; |
| 64 | + |
| 65 | + Ok((container_pulled.is_some(), manifest, config)) |
| 66 | +} |
| 67 | + |
16 | 68 | #[context("Upgrading composefs")] |
17 | | -pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { |
| 69 | +pub(crate) async fn upgrade_composefs(opts: UpgradeOpts) -> Result<()> { |
18 | 70 | let host = composefs_deployment_status() |
19 | 71 | .await |
20 | 72 | .context("Getting composefs deployment status")?; |
21 | 73 |
|
22 | | - start_finalize_stated_svc()?; |
23 | | - |
24 | | - // TODO: IMPORTANT We need to check if any deployment is staged and get the image from that |
25 | | - let imgref = host |
| 74 | + let mut imgref = host |
26 | 75 | .spec |
27 | 76 | .image |
28 | 77 | .as_ref() |
29 | 78 | .ok_or_else(|| anyhow::anyhow!("No image source specified"))?; |
30 | 79 |
|
| 80 | + let sysroot = |
| 81 | + Dir::open_ambient_dir("/sysroot", ambient_authority()).context("Opening sysroot")?; |
| 82 | + let repo = open_composefs_repo(&sysroot)?; |
| 83 | + |
| 84 | + let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, imgref).await?; |
| 85 | + let booted_img_digest = manifest.config().digest().digest(); |
| 86 | + |
| 87 | + // We already have this container config. No update available |
| 88 | + if img_pulled { |
| 89 | + println!("No changes in: {imgref:#}"); |
| 90 | + // TODO(Johan-Liebert1): What if we have the config but we failed the previous update in the middle? |
| 91 | + return Ok(()); |
| 92 | + } |
| 93 | + |
| 94 | + // Check if we already have this update staged |
| 95 | + let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref()); |
| 96 | + |
| 97 | + if let Some(staged_image) = staged_image { |
| 98 | + // We have a staged image and it has the same digest as the currently booted image's latest |
| 99 | + // digest |
| 100 | + if staged_image.image_digest == booted_img_digest { |
| 101 | + if opts.apply { |
| 102 | + return crate::reboot::reboot(); |
| 103 | + } |
| 104 | + |
| 105 | + println!("Update already staged. To apply update run `bootc update --apply`"); |
| 106 | + |
| 107 | + return Ok(()); |
| 108 | + } |
| 109 | + |
| 110 | + // We have a staged image but it's not the update image. |
| 111 | + // Maybe it's something we got by `bootc switch` |
| 112 | + // Switch takes precedence over update, so we change the imgref |
| 113 | + imgref = &staged_image.image; |
| 114 | + |
| 115 | + let (img_pulled, staged_manifest, staged_cfg) = is_image_pulled(&repo, imgref).await?; |
| 116 | + manifest = staged_manifest; |
| 117 | + config = staged_cfg; |
| 118 | + |
| 119 | + // We already have this container config. No update available |
| 120 | + if img_pulled { |
| 121 | + println!("No changes in staged image: {imgref:#}"); |
| 122 | + return Ok(()); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + if opts.check { |
| 127 | + // TODO(Johan-Liebert1): If we have the previous, i.e. the current manifest with us then we can replace the |
| 128 | + // following with [`ostree_container::ManifestDiff::new`] which will be much cleaner |
| 129 | + for (idx, diff_id) in config.rootfs().diff_ids().iter().enumerate() { |
| 130 | + let diff_id = str_to_sha256digest(diff_id)?; |
| 131 | + |
| 132 | + // we could use `check_stream` here but that will most probably take forever as it |
| 133 | + // usually takes ~3s to verify one single layer |
| 134 | + let have_layer = repo.has_stream(&diff_id)?; |
| 135 | + |
| 136 | + if have_layer.is_none() { |
| 137 | + if idx >= manifest.layers().len() { |
| 138 | + anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers"); |
| 139 | + } |
| 140 | + |
| 141 | + let layer = &manifest.layers()[idx]; |
| 142 | + |
| 143 | + println!( |
| 144 | + "Added layer: {}\tSize: {}", |
| 145 | + layer.digest(), |
| 146 | + layer.size().to_string() |
| 147 | + ); |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + return Ok(()); |
| 152 | + } |
| 153 | + |
| 154 | + start_finalize_stated_svc()?; |
| 155 | + |
31 | 156 | let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; |
32 | 157 |
|
33 | 158 | let Some(entry) = entries.iter().next() else { |
@@ -61,5 +186,9 @@ pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { |
61 | 186 | boot_digest, |
62 | 187 | )?; |
63 | 188 |
|
| 189 | + if opts.apply { |
| 190 | + return crate::reboot::reboot(); |
| 191 | + } |
| 192 | + |
64 | 193 | Ok(()) |
65 | 194 | } |
0 commit comments