mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 15:45:53 +01:00
composefs/update: Ensure idempotency on update
Handle the following cases we can encounter on `bootc udpate`
1. The verity is the same as that of the currently booted deployment
- Nothing to do here in case of update as we're currently booted. But if we're switching
then we update the target imageref in the .origin file for the deployment
2. The verity is the same as that of the staged deployment
- Nothing to do, as we only get a "staged" deployment if we have
/run/composefs/staged-deployment which is the last thing we create
while upgrading
3. The verity is the same as that of the rollback deployment or any
other deployment we have already deployed
- Nothing to do since this is a rollback deployment which means
this was unstaged at some point
4. The verity is not found
- The update/switch might've been canceled before
/run/composefs/staged-deployment was created, or at any other point
in time, or it's a new one.
Any which way, we can overwrite everything. In this case we remove
all the staged bootloader entries, if any, and remove the entire
state directory, as it would most probably be in an inconsistent
state.
Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
This commit is contained in:
committed by
Colin Walters
parent
e747216980
commit
71f5ace6fa
@@ -1,4 +1,7 @@
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::Path;
|
||||
use std::{fs::create_dir_all, process::Command};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -8,7 +11,7 @@ use bootc_mount::tempmount::TempMount;
|
||||
use bootc_utils::CommandRunExt;
|
||||
use camino::Utf8PathBuf;
|
||||
use cap_std_ext::cap_std::ambient_authority;
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
|
||||
use cap_std_ext::dirext::CapStdExtDirExt;
|
||||
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
|
||||
use fn_error_context::context;
|
||||
@@ -23,6 +26,7 @@ use crate::bootc_composefs::boot::BootType;
|
||||
use crate::bootc_composefs::repo::get_imgref;
|
||||
use crate::bootc_composefs::status::get_sorted_type1_boot_entries;
|
||||
use crate::parsers::bls_config::BLSConfigType;
|
||||
use crate::store::{BootedComposefs, Storage};
|
||||
use crate::{
|
||||
composefs_consts::{
|
||||
COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
|
||||
@@ -104,6 +108,49 @@ pub(crate) fn copy_etc_to_state(
|
||||
cp_ret
|
||||
}
|
||||
|
||||
/// Updates the currently booted image's target imgref
|
||||
pub(crate) fn update_target_imgref_in_origin(
|
||||
storage: &Storage,
|
||||
booted_cfs: &BootedComposefs,
|
||||
imgref: &ImageReference,
|
||||
) -> Result<()> {
|
||||
let path = Path::new(STATE_DIR_RELATIVE).join(booted_cfs.cmdline.digest.deref());
|
||||
|
||||
let state_dir = storage
|
||||
.physical_root
|
||||
.open_dir(path)
|
||||
.context("Opening state dir")?;
|
||||
|
||||
let origin_filename = format!("{}.origin", booted_cfs.cmdline.digest.deref());
|
||||
|
||||
let origin_file = state_dir
|
||||
.read_to_string(&origin_filename)
|
||||
.context("Reading origin file")?;
|
||||
|
||||
let mut ini =
|
||||
tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
|
||||
|
||||
// Replace the origin
|
||||
ini = ini.section("origin").item(
|
||||
ORIGIN_CONTAINER,
|
||||
format!("ostree-unverified-image:{imgref}"),
|
||||
);
|
||||
|
||||
state_dir
|
||||
.atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
|
||||
f.write_all(ini.to_string().as_bytes())?;
|
||||
f.flush()?;
|
||||
|
||||
let perms = Permissions::from_mode(0o644);
|
||||
f.get_mut().as_file_mut().set_permissions(perms)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.context("Writing to origin file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates and populates /sysroot/state/deploy/image_id
|
||||
#[context("Writing composefs state")]
|
||||
pub(crate) fn write_composefs_state(
|
||||
|
||||
@@ -2,9 +2,11 @@ use anyhow::{Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use composefs::{
|
||||
fsverity::FsVerityHashValue,
|
||||
fsverity::{FsVerityHashValue, Sha512HashValue},
|
||||
util::{parse_sha256, Sha256Digest},
|
||||
};
|
||||
use composefs_boot::BootOps;
|
||||
use composefs_oci::image::create_filesystem;
|
||||
use fn_error_context::context;
|
||||
use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest};
|
||||
|
||||
@@ -13,11 +15,12 @@ use crate::{
|
||||
boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType},
|
||||
repo::{get_imgref, pull_composefs_repo},
|
||||
service::start_finalize_stated_svc,
|
||||
state::write_composefs_state,
|
||||
status::{get_composefs_status, get_container_manifest_and_config},
|
||||
state::{update_target_imgref_in_origin, write_composefs_state},
|
||||
status::{get_bootloader, get_composefs_status, get_container_manifest_and_config},
|
||||
},
|
||||
cli::UpgradeOpts,
|
||||
spec::ImageReference,
|
||||
composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED},
|
||||
spec::{Bootloader, Host, ImageReference},
|
||||
store::{BootedComposefs, ComposefsRepository, Storage},
|
||||
};
|
||||
|
||||
@@ -42,14 +45,14 @@ pub fn str_to_sha256digest(id: &str) -> Result<Sha256Digest> {
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a tuple containing:
|
||||
/// * `true` if the image is pulled/available locally, `false` otherwise
|
||||
/// * `Some<Sha512HashValue>` if the image is pulled/available locally, `None` otherwise
|
||||
/// * The container image manifest
|
||||
/// * The container image configuration
|
||||
#[context("Checking if image {} is pulled", imgref.image)]
|
||||
async fn is_image_pulled(
|
||||
repo: &ComposefsRepository,
|
||||
imgref: &ImageReference,
|
||||
) -> Result<(bool, ImageManifest, ImageConfiguration)> {
|
||||
) -> Result<(Option<Sha512HashValue>, ImageManifest, ImageConfiguration)> {
|
||||
let imgref_repr = get_imgref(&imgref.transport, &imgref.image);
|
||||
let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?;
|
||||
|
||||
@@ -59,97 +62,123 @@ async fn is_image_pulled(
|
||||
// check_stream is expensive to run, but probably a good idea
|
||||
let container_pulled = repo.check_stream(&img_sha256).context("Checking stream")?;
|
||||
|
||||
Ok((container_pulled.is_some(), manifest, config))
|
||||
Ok((container_pulled, manifest, config))
|
||||
}
|
||||
|
||||
#[context("Upgrading composefs")]
|
||||
pub(crate) async fn upgrade_composefs(
|
||||
opts: UpgradeOpts,
|
||||
fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> {
|
||||
if boot_dir.exists(TYPE1_ENT_PATH_STAGED) {
|
||||
boot_dir
|
||||
.remove_dir_all(TYPE1_ENT_PATH_STAGED)
|
||||
.context("Removing staged bootloader entry")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) enum UpdateAction {
|
||||
/// Skip the update. We probably have the update in our deployments
|
||||
Skip,
|
||||
/// Proceed with the update
|
||||
Proceed,
|
||||
/// Only update the target imgref in the .origin file
|
||||
UpdateOrigin,
|
||||
}
|
||||
|
||||
/// Determines what action should be taken for the update
|
||||
fn validate_update(
|
||||
storage: &Storage,
|
||||
composefs: &BootedComposefs,
|
||||
) -> Result<()> {
|
||||
let host = get_composefs_status(storage, composefs)
|
||||
.await
|
||||
.context("Getting composefs deployment status")?;
|
||||
booted_cfs: &BootedComposefs,
|
||||
host: &Host,
|
||||
img_digest: &str,
|
||||
config_verity: &Sha512HashValue,
|
||||
) -> Result<UpdateAction> {
|
||||
// Cases
|
||||
//
|
||||
// 1. The verity is the same as that of the currently booted deployment
|
||||
// - Nothing to do here as we're currently booted
|
||||
//
|
||||
// 2. The verity is the same as that of the staged deployment
|
||||
// - Nothing to do, as we only get a "staged" deployment if we have
|
||||
// /run/composefs/staged-deployment which is the last thing we create while upgrading
|
||||
//
|
||||
// 3. The verity is the same as that of the rollback deployment
|
||||
// - Nothing to do since this is a rollback deployment which means this was unstaged at some
|
||||
// point
|
||||
//
|
||||
// 4. The verity is not found
|
||||
// - The update/switch might've been canceled before /run/composefs/staged-deployment
|
||||
// was created, or at any other point in time, or it's a new one.
|
||||
// Any which way, we can overwrite everything
|
||||
|
||||
let mut imgref = host
|
||||
.spec
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
|
||||
let repo = &*booted_cfs.repo;
|
||||
|
||||
let repo = &*composefs.repo;
|
||||
let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?;
|
||||
fs.transform_for_boot(&repo)?;
|
||||
|
||||
let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, imgref).await?;
|
||||
let booted_img_digest = manifest.config().digest().digest();
|
||||
let image_id = fs.compute_image_id();
|
||||
|
||||
// We already have this container config. No update available
|
||||
if img_pulled {
|
||||
println!("No changes in: {imgref:#}");
|
||||
// TODO(Johan-Liebert1): What if we have the config but we failed the previous update in the middle?
|
||||
return Ok(());
|
||||
// Case1
|
||||
//
|
||||
// "update" image has the same verity as the one currently booted
|
||||
// This could be someone trying to `bootc switch <remote_image>` where
|
||||
// remote_image is the exact same image as the one currently booted, but
|
||||
// they are wanting to change the target
|
||||
//
|
||||
// We could simply update the image origin file here
|
||||
if image_id.to_hex() == *booted_cfs.cmdline.digest {
|
||||
// update_target_imgref_in_origin(storage, booted_cfs);
|
||||
return Ok(UpdateAction::UpdateOrigin);
|
||||
}
|
||||
|
||||
// Check if we already have this update staged
|
||||
let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref());
|
||||
let all_deployments = host.all_composefs_deployments()?;
|
||||
|
||||
if let Some(staged_image) = staged_image {
|
||||
// We have a staged image and it has the same digest as the currently booted image's latest
|
||||
// digest
|
||||
if staged_image.image_digest == booted_img_digest {
|
||||
if opts.apply {
|
||||
return crate::reboot::reboot();
|
||||
}
|
||||
let found_depl = all_deployments
|
||||
.iter()
|
||||
.find(|d| d.deployment.verity == image_id.to_hex());
|
||||
|
||||
println!("Update already staged. To apply update run `bootc update --apply`");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We have a staged image but it's not the update image.
|
||||
// Maybe it's something we got by `bootc switch`
|
||||
// Switch takes precedence over update, so we change the imgref
|
||||
imgref = &staged_image.image;
|
||||
|
||||
let (img_pulled, staged_manifest, staged_cfg) = is_image_pulled(&repo, imgref).await?;
|
||||
manifest = staged_manifest;
|
||||
config = staged_cfg;
|
||||
|
||||
// We already have this container config. No update available
|
||||
if img_pulled {
|
||||
println!("No changes in staged image: {imgref:#}");
|
||||
return Ok(());
|
||||
}
|
||||
// We have this in our deployments somewhere, i.e. Case 2 or 3
|
||||
if found_depl.is_some() {
|
||||
return Ok(UpdateAction::Skip);
|
||||
}
|
||||
|
||||
if opts.check {
|
||||
// TODO(Johan-Liebert1): If we have the previous, i.e. the current manifest with us then we can replace the
|
||||
// following with [`ostree_container::ManifestDiff::new`] which will be much cleaner
|
||||
for (idx, diff_id) in config.rootfs().diff_ids().iter().enumerate() {
|
||||
let diff_id = str_to_sha256digest(diff_id)?;
|
||||
let booted = host.require_composefs_booted()?;
|
||||
let boot_dir = storage.require_boot_dir()?;
|
||||
|
||||
// we could use `check_stream` here but that will most probably take forever as it
|
||||
// usually takes ~3s to verify one single layer
|
||||
let have_layer = repo.has_stream(&diff_id)?;
|
||||
// Remove staged bootloader entries, if any
|
||||
// GC should take care of the UKI PEs and other binaries
|
||||
match get_bootloader()? {
|
||||
Bootloader::Grub => match booted.boot_type {
|
||||
BootType::Bls => rm_staged_type1_ent(boot_dir)?,
|
||||
|
||||
if have_layer.is_none() {
|
||||
if idx >= manifest.layers().len() {
|
||||
anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers");
|
||||
BootType::Uki => {
|
||||
let grub = boot_dir.open_dir("grub2").context("Opening grub dir")?;
|
||||
|
||||
if grub.exists(USER_CFG_STAGED) {
|
||||
grub.remove_file(USER_CFG_STAGED)
|
||||
.context("Removing staged grub user config")?;
|
||||
}
|
||||
|
||||
let layer = &manifest.layers()[idx];
|
||||
|
||||
println!(
|
||||
"Added layer: {}\tSize: {}",
|
||||
layer.digest(),
|
||||
layer.size().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return Ok(());
|
||||
Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?,
|
||||
}
|
||||
|
||||
// Remove state directory
|
||||
let state_dir = storage
|
||||
.physical_root
|
||||
.open_dir(STATE_DIR_RELATIVE)
|
||||
.context("Opening state dir")?;
|
||||
|
||||
if state_dir.exists(image_id.to_hex()) {
|
||||
state_dir
|
||||
.remove_dir_all(image_id.to_hex())
|
||||
.context("Removing state")?;
|
||||
}
|
||||
|
||||
Ok(UpdateAction::Proceed)
|
||||
}
|
||||
|
||||
async fn do_upgrade(storage: &Storage, host: &Host, imgref: &ImageReference) -> Result<()> {
|
||||
start_finalize_stated_svc()?;
|
||||
|
||||
let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?;
|
||||
@@ -195,6 +224,134 @@ pub(crate) async fn upgrade_composefs(
|
||||
boot_digest,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[context("Upgrading composefs")]
|
||||
pub(crate) async fn upgrade_composefs(
|
||||
opts: UpgradeOpts,
|
||||
storage: &Storage,
|
||||
composefs: &BootedComposefs,
|
||||
) -> Result<()> {
|
||||
let host = get_composefs_status(storage, composefs)
|
||||
.await
|
||||
.context("Getting composefs deployment status")?;
|
||||
|
||||
let mut booted_imgref = host
|
||||
.spec
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
|
||||
|
||||
let repo = &*composefs.repo;
|
||||
|
||||
let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, booted_imgref).await?;
|
||||
let booted_img_digest = manifest.config().digest().digest().to_owned();
|
||||
|
||||
// Check if we already have this update staged
|
||||
// Or if we have another staged deployment with a different image
|
||||
let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref());
|
||||
|
||||
if let Some(staged_image) = staged_image {
|
||||
// We have a staged image and it has the same digest as the currently booted image's latest
|
||||
// digest
|
||||
if staged_image.image_digest == booted_img_digest {
|
||||
if opts.apply {
|
||||
return crate::reboot::reboot();
|
||||
}
|
||||
|
||||
println!("Update already staged. To apply update run `bootc update --apply`");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We have a staged image but it's not the update image.
|
||||
// Maybe it's something we got by `bootc switch`
|
||||
// Switch takes precedence over update, so we change the imgref
|
||||
booted_imgref = &staged_image.image;
|
||||
|
||||
let (img_pulled, staged_manifest, staged_cfg) =
|
||||
is_image_pulled(&repo, booted_imgref).await?;
|
||||
manifest = staged_manifest;
|
||||
config = staged_cfg;
|
||||
|
||||
if let Some(cfg_verity) = img_pulled {
|
||||
let action = validate_update(
|
||||
storage,
|
||||
composefs,
|
||||
&host,
|
||||
manifest.config().digest().digest(),
|
||||
&cfg_verity,
|
||||
)?;
|
||||
|
||||
match action {
|
||||
UpdateAction::Skip => {
|
||||
println!("No changes in staged image: {booted_imgref:#}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
UpdateAction::Proceed => {
|
||||
return do_upgrade(storage, &host, booted_imgref).await;
|
||||
}
|
||||
|
||||
UpdateAction::UpdateOrigin => {
|
||||
// The staged image will never be the current image's verity digest
|
||||
anyhow::bail!("Staged image verity digest is the same as booted image")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We already have this container config
|
||||
if let Some(cfg_verity) = img_pulled {
|
||||
let action = validate_update(storage, composefs, &host, &booted_img_digest, &cfg_verity)?;
|
||||
|
||||
match action {
|
||||
UpdateAction::Skip => {
|
||||
println!("No changes in: {booted_imgref:#}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
UpdateAction::Proceed => {
|
||||
return do_upgrade(storage, &host, booted_imgref).await;
|
||||
}
|
||||
|
||||
UpdateAction::UpdateOrigin => {
|
||||
return update_target_imgref_in_origin(storage, composefs, booted_imgref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.check {
|
||||
// TODO(Johan-Liebert1): If we have the previous, i.e. the current manifest with us then we can replace the
|
||||
// following with [`ostree_container::ManifestDiff::new`] which will be much cleaner
|
||||
for (idx, diff_id) in config.rootfs().diff_ids().iter().enumerate() {
|
||||
let diff_id = str_to_sha256digest(diff_id)?;
|
||||
|
||||
// we could use `check_stream` here but that will most probably take forever as it
|
||||
// usually takes ~3s to verify one single layer
|
||||
let have_layer = repo.has_stream(&diff_id)?;
|
||||
|
||||
if have_layer.is_none() {
|
||||
if idx >= manifest.layers().len() {
|
||||
anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers");
|
||||
}
|
||||
|
||||
let layer = &manifest.layers()[idx];
|
||||
|
||||
println!(
|
||||
"Added layer: {}\tSize: {}",
|
||||
layer.digest(),
|
||||
layer.size().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
do_upgrade(storage, &host, booted_imgref).await?;
|
||||
|
||||
if opts.apply {
|
||||
return crate::reboot::reboot();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user