Skip to content

Commit ce84d61

Browse files
composefs/update: Handle --download-only flag
When `--download-only` is passed, only download the image into the composefs repository but don't finalize it. Conver the /run/composefs/staged-deployment to a JSON file and Add a finalization_locked field depending upon which the finalize service will either finalize the staged deployment or leave it as is for garbage collection (even though GC is not fully implemented right now). Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent b90eb08 commit ce84d61

6 files changed

Lines changed: 125 additions & 43 deletions

File tree

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1277,7 +1277,7 @@ pub(crate) async fn setup_composefs_boot(
12771277
&root_setup.physical_root_path,
12781278
&id,
12791279
&crate::spec::ImageReference::from(state.target_imgref.clone()),
1280-
false,
1280+
None,
12811281
boot_type,
12821282
boot_digest,
12831283
&get_container_manifest_and_config(&get_imgref(

crates/lib/src/bootc_composefs/finalize.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ pub(crate) async fn composefs_backend_finalize(
5252
return Ok(());
5353
};
5454

55+
if staged_depl.download_only {
56+
tracing::debug!("Staged deployment is marked download only. Won't finalize");
57+
return Ok(());
58+
}
59+
5560
let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
5661
"Staged deployment is not a composefs deployment"
5762
))?;

crates/lib/src/bootc_composefs/state.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bootc_kernel_cmdline::utf8::Cmdline;
99
use bootc_mount::tempmount::TempMount;
1010
use bootc_utils::CommandRunExt;
1111
use camino::Utf8PathBuf;
12+
use canon_json::CanonJsonSerialize;
1213
use cap_std_ext::cap_std::ambient_authority;
1314
use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
1415
use cap_std_ext::dirext::CapStdExtDirExt;
@@ -23,7 +24,9 @@ use rustix::{
2324

2425
use crate::bootc_composefs::boot::BootType;
2526
use crate::bootc_composefs::repo::get_imgref;
26-
use crate::bootc_composefs::status::{ImgConfigManifest, get_sorted_type1_boot_entries};
27+
use crate::bootc_composefs::status::{
28+
ImgConfigManifest, StagedDeployment, get_sorted_type1_boot_entries,
29+
};
2730
use crate::parsers::bls_config::BLSConfigType;
2831
use crate::store::{BootedComposefs, Storage};
2932
use crate::{
@@ -227,7 +230,7 @@ pub(crate) async fn write_composefs_state(
227230
root_path: &Utf8PathBuf,
228231
deployment_id: &Sha512HashValue,
229232
target_imgref: &ImageReference,
230-
staged: bool,
233+
staged: Option<StagedDeployment>,
231234
boot_type: BootType,
232235
boot_digest: String,
233236
container_details: &ImgConfigManifest,
@@ -248,7 +251,12 @@ pub(crate) async fn write_composefs_state(
248251
)
249252
.context("Failed to create symlink for /var")?;
250253

251-
initialize_state(&root_path, &deployment_id.to_hex(), &state_path, !staged)?;
254+
initialize_state(
255+
&root_path,
256+
&deployment_id.to_hex(),
257+
&state_path,
258+
staged.is_none(),
259+
)?;
252260

253261
let ImageReference {
254262
image: image_name,
@@ -291,7 +299,7 @@ pub(crate) async fn write_composefs_state(
291299
)
292300
.context("Failed to write to .origin file")?;
293301

294-
if staged {
302+
if let Some(staged) = staged {
295303
std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
296304
.with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
297305

@@ -302,7 +310,9 @@ pub(crate) async fn write_composefs_state(
302310
staged_depl_dir
303311
.atomic_write(
304312
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
305-
deployment_id.to_hex().as_bytes(),
313+
staged
314+
.to_canon_json_vec()
315+
.context("Failed to serialize staged deployment JSON")?,
306316
)
307317
.with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
308318
}

crates/lib/src/bootc_composefs/status.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ impl std::fmt::Display for ComposefsCmdline {
7979
}
8080
}
8181

82+
#[derive(Debug, Serialize, Deserialize)]
83+
pub(crate) struct StagedDeployment {
84+
pub(crate) depl_id: String,
85+
pub(crate) finalization_locked: bool,
86+
}
87+
8288
/// Detect if we have composefs=<digest> in /proc/cmdline
8389
pub(crate) fn composefs_booted() -> Result<Option<&'static ComposefsCmdline>> {
8490
static CACHED_DIGEST_VALUE: OnceLock<Option<ComposefsCmdline>> = OnceLock::new();
@@ -554,7 +560,7 @@ pub(crate) async fn composefs_deployment_status_from(
554560

555561
let mut host = Host::new(host_spec);
556562

557-
let staged_deployment_id = match std::fs::File::open(format!(
563+
let staged_deployment = match std::fs::File::open(format!(
558564
"{COMPOSEFS_TRANSIENT_STATE_DIR}/{COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"
559565
)) {
560566
Ok(mut f) => {
@@ -590,7 +596,7 @@ pub(crate) async fn composefs_deployment_status_from(
590596
let ini = tini::Ini::from_string(&config)
591597
.with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
592598

593-
let boot_entry =
599+
let mut boot_entry =
594600
boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?;
595601

596602
// SAFETY: boot_entry.composefs will always be present
@@ -614,8 +620,11 @@ pub(crate) async fn composefs_deployment_status_from(
614620
continue;
615621
}
616622

617-
if let Some(staged_deployment_id) = &staged_deployment_id {
618-
if depl_file_name == staged_deployment_id.trim() {
623+
if let Some(staged_deployment) = &staged_deployment {
624+
let staged_depl = serde_json::from_str::<StagedDeployment>(&staged_deployment)?;
625+
626+
if depl_file_name == staged_depl.depl_id {
627+
boot_entry.download_only = staged_depl.finalization_locked;
619628
host.status.staged = Some(boot_entry);
620629
continue;
621630
}

crates/lib/src/bootc_composefs/switch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub(crate) async fn switch_composefs(
4545
let do_upgrade_opts = DoUpgradeOpts {
4646
soft_reboot: opts.soft_reboot,
4747
apply: opts.apply,
48+
download_only: false,
4849
};
4950

5051
if let Some(cfg_verity) = image {

crates/lib/src/bootc_composefs/update.rs

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
use std::io::Write;
2+
13
use anyhow::{Context, Result};
24
use camino::Utf8PathBuf;
3-
use cap_std_ext::cap_std::fs::Dir;
5+
use canon_json::CanonJsonSerialize;
6+
use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
47
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
58
use composefs_boot::BootOps;
69
use composefs_oci::image::create_filesystem;
710
use fn_error_context::context;
11+
use ocidir::cap_std::ambient_authority;
812
use ostree_ext::container::ManifestDiff;
913

1014
use crate::{
@@ -15,12 +19,15 @@ use crate::{
1519
soft_reboot::prepare_soft_reboot_composefs,
1620
state::write_composefs_state,
1721
status::{
18-
ImgConfigManifest, get_bootloader, get_composefs_status,
22+
ImgConfigManifest, StagedDeployment, get_bootloader, get_composefs_status,
1923
get_container_manifest_and_config, get_imginfo,
2024
},
2125
},
2226
cli::{SoftRebootMode, UpgradeOpts},
23-
composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED},
27+
composefs_consts::{
28+
COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE,
29+
TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED,
30+
},
2431
spec::{Bootloader, Host, ImageReference},
2532
store::{BootedComposefs, ComposefsRepository, Storage},
2633
};
@@ -206,6 +213,31 @@ pub(crate) fn validate_update(
206213
pub(crate) struct DoUpgradeOpts {
207214
pub(crate) apply: bool,
208215
pub(crate) soft_reboot: Option<SoftRebootMode>,
216+
pub(crate) download_only: bool,
217+
}
218+
219+
async fn apply_upgrade(
220+
storage: &Storage,
221+
booted_cfs: &BootedComposefs,
222+
depl_id: &String,
223+
opts: &DoUpgradeOpts,
224+
) -> Result<()> {
225+
if let Some(soft_reboot_mode) = opts.soft_reboot {
226+
return prepare_soft_reboot_composefs(
227+
storage,
228+
booted_cfs,
229+
Some(depl_id),
230+
soft_reboot_mode,
231+
opts.apply,
232+
)
233+
.await;
234+
};
235+
236+
if opts.apply {
237+
return crate::reboot::reboot();
238+
}
239+
240+
Ok(())
209241
}
210242

211243
/// Performs the Update or Switch operation
@@ -255,29 +287,17 @@ pub(crate) async fn do_upgrade(
255287
&Utf8PathBuf::from("/sysroot"),
256288
&id,
257289
imgref,
258-
true,
290+
Some(StagedDeployment {
291+
depl_id: id.to_hex(),
292+
finalization_locked: opts.download_only,
293+
}),
259294
boot_type,
260295
boot_digest,
261296
img_manifest_config,
262297
)
263298
.await?;
264299

265-
if let Some(soft_reboot_mode) = opts.soft_reboot {
266-
return prepare_soft_reboot_composefs(
267-
storage,
268-
booted_cfs,
269-
Some(&id.to_hex()),
270-
soft_reboot_mode,
271-
opts.apply,
272-
)
273-
.await;
274-
};
275-
276-
if opts.apply {
277-
return crate::reboot::reboot();
278-
}
279-
280-
Ok(())
300+
apply_upgrade(storage, booted_cfs, &id.to_hex(), opts).await
281301
}
282302

283303
#[context("Upgrading composefs")]
@@ -286,18 +306,60 @@ pub(crate) async fn upgrade_composefs(
286306
storage: &Storage,
287307
composefs: &BootedComposefs,
288308
) -> Result<()> {
289-
// Download-only mode is not yet supported for composefs backend
290-
if opts.download_only {
291-
anyhow::bail!("--download-only is not yet supported for composefs backend");
292-
}
293-
if opts.from_downloaded {
294-
anyhow::bail!("--from-downloaded is not yet supported for composefs backend");
295-
}
296-
297309
let host = get_composefs_status(storage, composefs)
298310
.await
299311
.context("Getting composefs deployment status")?;
300312

313+
let do_upgrade_opts = DoUpgradeOpts {
314+
soft_reboot: opts.soft_reboot,
315+
apply: opts.apply,
316+
download_only: opts.download_only,
317+
};
318+
319+
if opts.from_downloaded {
320+
let staged = host
321+
.status
322+
.staged
323+
.as_ref()
324+
.ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
325+
326+
// Staged deployment exists, but it will be finalized
327+
if !staged.download_only {
328+
println!("Staged deployment is present and not in download only mode.");
329+
println!("Use `bootc update --apply` to apply the update.");
330+
return Ok(());
331+
}
332+
333+
start_finalize_stated_svc()?;
334+
335+
// Make the staged deployment not download_only
336+
let new_staged = StagedDeployment {
337+
depl_id: staged.require_composefs()?.verity.clone(),
338+
finalization_locked: false,
339+
};
340+
341+
let staged_depl_dir =
342+
Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
343+
.context("Opening transient state directory")?;
344+
345+
staged_depl_dir
346+
.atomic_replace_with(
347+
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
348+
|f| -> std::io::Result<()> {
349+
f.write_all(new_staged.to_canon_json_string()?.as_bytes())
350+
},
351+
)
352+
.context("Writing staged file")?;
353+
354+
return apply_upgrade(
355+
storage,
356+
composefs,
357+
&staged.require_composefs()?.verity,
358+
&do_upgrade_opts,
359+
)
360+
.await;
361+
}
362+
301363
let mut booted_imgref = host
302364
.spec
303365
.image
@@ -313,11 +375,6 @@ pub(crate) async fn upgrade_composefs(
313375
// Or if we have another staged deployment with a different image
314376
let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref());
315377

316-
let do_upgrade_opts = DoUpgradeOpts {
317-
soft_reboot: opts.soft_reboot,
318-
apply: opts.apply,
319-
};
320-
321378
if let Some(staged_image) = staged_image {
322379
// We have a staged image and it has the same digest as the currently booted image's latest
323380
// digest

0 commit comments

Comments
 (0)