Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions crates/lib/src/bootc_composefs/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use fn_error_context::context;
use crate::{
bootc_composefs::{
status::get_composefs_status,
update::{DoUpgradeOpts, UpdateAction, do_upgrade, is_image_pulled, validate_update},
update::{
DoUpgradeOpts, UpdateAction, apply_upgrade_from_downloaded, do_upgrade,
is_image_pulled, validate_update,
},
},
cli::{SwitchOpts, imgref_for_switch},
progress_jsonl::ProgressWriter,
Expand All @@ -17,13 +20,28 @@ pub(crate) async fn switch_composefs(
storage: &Storage,
booted_cfs: &BootedComposefs,
) -> Result<()> {
let target = imgref_for_switch(&opts)?;

// TODO: Handle in-place
let host = get_composefs_status(storage, booted_cfs)
.await
.context("Getting composefs deployment status")?;

let prog: ProgressWriter = opts.progress.clone().try_into()?;

let mut do_upgrade_opts = DoUpgradeOpts {
soft_reboot: opts.soft_reboot,
apply: opts.apply,
download_only: opts.download_opts.download_only,
use_unified: false,
quiet: opts.quiet,
prog,
};

if opts.download_opts.from_downloaded {
return apply_upgrade_from_downloaded(storage, booted_cfs, &host, &do_upgrade_opts).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care that staged deployments don't have any provenance whether they came from upgrade or switch? As-is one could do bootc upgrade --download-only && bootc switch --from-downloaded to apply an upgrade. There's no way to distinguish between an "upgrade-staged" vs "switch-staged" deployment.

I worry that a typical user that doesn't understand or care about all of the implementation details is not going to realize that upgrades and switches can be used interchangeably in this way and might be surprised if they think that upgrades and switches use distinct staged deployments.

Then again I guess the ostree backend already does it this way too, so it's at least consistent.

Maybe something we address in a followup.

}

let target = imgref_for_switch(&opts)?;

let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
Expand All @@ -49,17 +67,18 @@ pub(crate) async fn switch_composefs(
bootc.operation = "switch",
bootc.target_image = target_imgref.to_string(),
bootc.apply_mode = opts.apply,
bootc.download_only = opts.download_opts.download_only,
bootc.from_downloaded = opts.download_opts.from_downloaded,
"Starting composefs switch operation",
);

let repo = &*booted_cfs.repo;
let (image, img_config) = is_image_pulled(repo, &target_imgref).await?;

// Use unified storage if explicitly requested, or auto-detect: either the
// target image is already in bootc-owned containers-storage, OR the booted
// image is — which means the user has opted into unified storage and all
// subsequent operations (including switch to a new image) should use it.
let use_unified = if opts.unified_storage_exp {
do_upgrade_opts.use_unified = if opts.unified_storage_exp {
true
} else {
let booted_imgref = host.spec.image.as_ref();
Expand All @@ -73,16 +92,7 @@ pub(crate) async fn switch_composefs(
booted_unified || target_unified
};

let prog: ProgressWriter = opts.progress.try_into()?;

let do_upgrade_opts = DoUpgradeOpts {
soft_reboot: opts.soft_reboot,
apply: opts.apply,
download_only: false,
use_unified,
quiet: opts.quiet,
prog,
};
let (image, img_config) = is_image_pulled(repo, &target_imgref).await?;

if let Some(cfg_verity) = image {
let action = validate_update(
Expand Down
107 changes: 58 additions & 49 deletions crates/lib/src/bootc_composefs/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,59 @@ pub(crate) async fn do_upgrade(
apply_upgrade(storage, booted_cfs, &id.to_hex(), opts).await
}

#[context("Applying downloaded upgrade")]
pub(crate) async fn apply_upgrade_from_downloaded(
storage: &Storage,
composefs: &BootedComposefs,
host: &Host,
do_upgrade_opts: &DoUpgradeOpts,
) -> Result<()> {
let staged = host
.status
.staged
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;

// Staged deployment exists, but it will be finalized
if !staged.download_only {
println!("Staged deployment is present and not in download only mode.");
println!("Use `bootc update --apply` to apply the update.");
return Ok(());
}

start_finalize_stated_svc()?;

let staged_depl_dir = Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
.context("Opening transient state directory")?;

let current = staged_depl_dir
.read_to_string(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)
.context("Reading staged file")?;

let mut new_staged: StagedDeployment =
serde_json::from_str(&current).context("Deserialzing staged file")?;

// Make the staged deployment not download_only
new_staged.finalization_locked = false;

staged_depl_dir
.atomic_replace_with(
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
|f| -> std::io::Result<()> {
serde_json::to_writer(f, &new_staged).map_err(std::io::Error::from)
},
)
.context("Writing staged file")?;

return apply_upgrade(
storage,
composefs,
&staged.require_composefs()?.verity,
&do_upgrade_opts,
)
.await;
}

#[context("Upgrading composefs")]
pub(crate) async fn upgrade_composefs(
opts: UpgradeOpts,
Expand All @@ -372,8 +425,8 @@ pub(crate) async fn upgrade_composefs(
message_id = COMPOSEFS_UPGRADE_JOURNAL_ID,
bootc.operation = "upgrade",
bootc.apply_mode = opts.apply,
bootc.download_only = opts.download_only,
bootc.from_downloaded = opts.from_downloaded,
bootc.download_only = opts.download_opts.download_only,
bootc.from_downloaded = opts.download_opts.from_downloaded,
"Starting composefs upgrade operation"
);

Expand All @@ -398,58 +451,14 @@ pub(crate) async fn upgrade_composefs(
let mut do_upgrade_opts = DoUpgradeOpts {
soft_reboot: opts.soft_reboot,
apply: opts.apply,
download_only: opts.download_only,
download_only: opts.download_opts.download_only,
use_unified: false,
quiet: opts.quiet,
prog,
};

if opts.from_downloaded {
let staged = host
.status
.staged
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;

// Staged deployment exists, but it will be finalized
if !staged.download_only {
println!("Staged deployment is present and not in download only mode.");
println!("Use `bootc update --apply` to apply the update.");
return Ok(());
}

start_finalize_stated_svc()?;

let staged_depl_dir =
Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
.context("Opening transient state directory")?;

let current = staged_depl_dir
.read_to_string(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)
.context("Reading staged file")?;

let mut new_staged: StagedDeployment =
serde_json::from_str(&current).context("Deserialzing staged file")?;

// Make the staged deployment not download_only
new_staged.finalization_locked = false;

staged_depl_dir
.atomic_replace_with(
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
|f| -> std::io::Result<()> {
serde_json::to_writer(f, &new_staged).map_err(std::io::Error::from)
},
)
.context("Writing staged file")?;

return apply_upgrade(
storage,
composefs,
&staged.require_composefs()?.verity,
&do_upgrade_opts,
)
.await;
if opts.download_opts.from_downloaded {
return apply_upgrade_from_downloaded(storage, composefs, &host, &do_upgrade_opts).await;
}

let imgref = derived_image.as_ref().or(current_image);
Expand Down
Loading