Skip to content

Commit 0d8ac87

Browse files
committed
composefs: Read manifest+config from composefs repo instead of .imginfo
As of recently composefs-oci stores the manifest+config in the composefs repo too, so switch over to using that by default. We discover which manifest is in use by storing its digest in the origin. For backwards compatibility, get_imginfo() falls back to reading legacy .imginfo files for deployments created before this change. But we can now drop the original version of this which fetched from the network. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 93375f6 commit 0d8ac87

10 files changed

Lines changed: 93 additions & 134 deletions

File tree

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -93,31 +93,20 @@ use rustix::{mount::MountFlags, path::Arg};
9393
use schemars::JsonSchema;
9494
use serde::{Deserialize, Serialize};
9595

96+
use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
97+
use crate::bootc_kargs::compute_new_kargs;
98+
use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
99+
use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
96100
use crate::task::Task;
97-
use crate::{
98-
bootc_composefs::repo::get_imgref,
99-
composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED},
100-
};
101101
use crate::{
102102
bootc_composefs::repo::open_composefs_repo,
103103
store::{ComposefsFilesystem, Storage},
104104
};
105-
use crate::{
106-
bootc_composefs::state::{get_booted_bls, write_composefs_state},
107-
composefs_consts::TYPE1_BOOT_DIR_PREFIX,
108-
};
109-
use crate::{
110-
bootc_composefs::status::get_container_manifest_and_config, bootc_kargs::compute_new_kargs,
111-
};
112105
use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState};
113-
use crate::{
114-
composefs_consts::UKI_NAME_PREFIX,
115-
parsers::bls_config::{BLSConfig, BLSConfigType},
116-
};
117106
use crate::{
118107
composefs_consts::{
119108
BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST,
120-
STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
109+
STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, UKI_NAME_PREFIX, USER_CFG, USER_CFG_STAGED,
121110
},
122111
spec::{Bootloader, Host},
123112
};
@@ -1259,23 +1248,23 @@ fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
12591248
pub(crate) async fn setup_composefs_boot(
12601249
root_setup: &RootSetup,
12611250
state: &State,
1262-
image_id: &str,
1251+
pull_result: &composefs_oci::skopeo::PullResult<Sha512HashValue>,
12631252
allow_missing_fsverity: bool,
12641253
) -> Result<()> {
12651254
const COMPOSEFS_BOOT_SETUP_JOURNAL_ID: &str = "1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5";
12661255

12671256
tracing::info!(
12681257
message_id = COMPOSEFS_BOOT_SETUP_JOURNAL_ID,
12691258
bootc.operation = "boot_setup",
1270-
bootc.image_id = image_id,
1259+
bootc.config_digest = pull_result.config_digest,
12711260
bootc.allow_missing_fsverity = allow_missing_fsverity,
12721261
"Setting up composefs boot",
12731262
);
12741263

12751264
let mut repo = open_composefs_repo(&root_setup.physical_root)?;
12761265
repo.set_insecure(allow_missing_fsverity);
12771266

1278-
let mut fs = create_composefs_filesystem(&repo, image_id, None)?;
1267+
let mut fs = create_composefs_filesystem(&repo, &pull_result.config_digest, None)?;
12791268
let entries = fs.transform_for_boot(&repo)?;
12801269
let id = fs.commit_image(&repo, None)?;
12811270
let mounted_fs = Dir::reopen_dir(
@@ -1343,11 +1332,7 @@ pub(crate) async fn setup_composefs_boot(
13431332
None,
13441333
boot_type,
13451334
boot_digest,
1346-
&get_container_manifest_and_config(&get_imgref(
1347-
&state.source.imageref.transport.to_string(),
1348-
&state.source.imageref.name,
1349-
))
1350-
.await?,
1335+
&pull_result.manifest_digest,
13511336
allow_missing_fsverity,
13521337
)
13531338
.await?;

crates/lib/src/bootc_composefs/export.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub async fn export_repo_to_image(
4343

4444
let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?;
4545

46-
let imginfo = get_imginfo(storage, &depl_verity, None).await?;
46+
let imginfo = get_imginfo(storage, &depl_verity)?;
4747

4848
// We want the digest in the form of "sha256:abc123"
4949
let config_digest = format!("{}", imginfo.manifest.config().digest());

crates/lib/src/bootc_composefs/gc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ pub(crate) async fn composefs_gc(
313313
continue;
314314
}
315315

316-
let image = get_imginfo(storage, verity, None).await?;
316+
let image = get_imginfo(storage, verity)?;
317317
let stream = format!("oci-config-{}", image.manifest.config().digest());
318318

319319
additional_roots.push(verity.clone());

crates/lib/src/bootc_composefs/repo.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ pub(crate) fn get_imgref(transport: &str, image: &str) -> String {
100100

101101
/// Result of pulling a composefs repository, including the OCI manifest digest
102102
/// needed to reconstruct image metadata from the local composefs repo.
103-
#[allow(dead_code)]
104103
pub(crate) struct PullRepoResult {
105104
pub(crate) repo: crate::store::ComposefsRepository,
106105
pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,

crates/lib/src/bootc_composefs/state.rs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,14 @@ use rustix::{
2626

2727
use crate::bootc_composefs::boot::BootType;
2828
use crate::bootc_composefs::repo::get_imgref;
29-
use crate::bootc_composefs::status::{
30-
ImgConfigManifest, StagedDeployment, get_sorted_type1_boot_entries,
31-
};
29+
use crate::bootc_composefs::status::{StagedDeployment, get_sorted_type1_boot_entries};
3230
use crate::parsers::bls_config::BLSConfigType;
3331
use crate::store::{BootedComposefs, Storage};
3432
use crate::{
3533
composefs_consts::{
3634
COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
37-
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH,
38-
STATE_DIR_RELATIVE,
35+
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, ORIGIN_KEY_IMAGE,
36+
ORIGIN_KEY_MANIFEST_DIGEST, SHARED_VAR_PATH, STATE_DIR_RELATIVE,
3937
},
4038
parsers::bls_config::BLSConfig,
4139
spec::ImageReference,
@@ -222,15 +220,15 @@ pub(crate) fn update_boot_digest_in_origin(
222220
/// * `staged` - Whether this is a staged deployment (writes to transient state dir)
223221
/// * `boot_type` - Boot loader type (`Bls` or `Uki`)
224222
/// * `boot_digest` - Optional boot digest for verification
225-
/// * `container_details` - Container manifest and config used to create this deployment
223+
/// * `manifest_digest` - OCI manifest content digest, stored in the origin file so the
224+
/// manifest+config can be retrieved from the composefs repo later
226225
///
227226
/// # State Directory Structure
228227
///
229228
/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`:
230229
/// * `etc/` - Copy of system configuration files
231230
/// * `var` - Symlink to shared `/var` directory
232-
/// * `{deployment_id}.origin` - OSTree-style origin configuration
233-
/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON
231+
/// * `{deployment_id}.origin` - Origin configuration with image ref, boot, and image metadata
234232
///
235233
/// For staged deployments, also writes to `/run/composefs/staged-deployment`.
236234
#[context("Writing composefs state")]
@@ -241,7 +239,7 @@ pub(crate) async fn write_composefs_state(
241239
staged: Option<StagedDeployment>,
242240
boot_type: BootType,
243241
boot_digest: String,
244-
container_details: &ImgConfigManifest,
242+
manifest_digest: &str,
245243
allow_missing_fsverity: bool,
246244
) -> Result<()> {
247245
let state_path = root_path
@@ -290,18 +288,15 @@ pub(crate) async fn write_composefs_state(
290288
.section(ORIGIN_KEY_BOOT)
291289
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
292290

291+
// Store the OCI manifest digest so we can retrieve the manifest+config
292+
// from the composefs repository later (composefs-rs stores them as splitstreams).
293+
config = config
294+
.section(ORIGIN_KEY_IMAGE)
295+
.item(ORIGIN_KEY_MANIFEST_DIGEST, manifest_digest);
296+
293297
let state_dir =
294298
Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
295299

296-
// NOTE: This is only supposed to be temporary until we decide on where to store
297-
// the container manifest/config
298-
state_dir
299-
.atomic_write(
300-
format!("{}.imginfo", deployment_id.to_hex()),
301-
serde_json::to_vec(&container_details)?,
302-
)
303-
.context("Failed to write to .imginfo file")?;
304-
305300
state_dir
306301
.atomic_write(
307302
format!("{}.origin", deployment_id.to_hex()),

crates/lib/src/bootc_composefs/status.rs

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ use std::{collections::HashSet, io::Read, sync::OnceLock};
33
use anyhow::{Context, Result};
44
use bootc_kernel_cmdline::utf8::Cmdline;
55
use bootc_mount::inspect_filesystem;
6+
use cfsctl::composefs::fsverity::Sha512HashValue;
7+
use cfsctl::composefs_oci::OciImage;
68
use fn_error_context::context;
79
use serde::{Deserialize, Serialize};
810

911
use crate::{
1012
bootc_composefs::{
1113
boot::BootType,
12-
repo::get_imgref,
1314
selinux::are_selinux_policies_compatible,
1415
state::get_composefs_usr_overlay_status,
1516
utils::{compute_store_boot_digest_for_uki, get_uki_cmdline},
1617
},
1718
composefs_consts::{
18-
COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG,
19-
USER_CFG_STAGED,
19+
COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST,
20+
TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG, USER_CFG_STAGED,
2021
},
2122
install::EFI_LOADER_INFO,
2223
parsers::{
@@ -358,57 +359,73 @@ pub(crate) fn get_bootloader() -> Result<Bootloader> {
358359
}
359360
}
360361

361-
/// Reads the .imginfo file for the provided deployment
362-
#[context("Reading imginfo")]
363-
pub(crate) async fn get_imginfo(
364-
storage: &Storage,
365-
deployment_id: &str,
366-
imgref: Option<&ImageReference>,
367-
) -> Result<ImgConfigManifest> {
368-
let imginfo_fname = format!("{deployment_id}.imginfo");
369-
362+
/// Retrieves the OCI manifest and config for a deployment from the composefs repository.
363+
///
364+
/// The manifest digest is read from the deployment's `.origin` file,
365+
/// then `OciImage::open()` retrieves manifest+config from the composefs repo
366+
/// where composefs-rs stores them as splitstreams during pull.
367+
///
368+
/// Falls back to reading legacy `.imginfo` files for backwards compatibility
369+
/// with deployments created before the manifest digest was stored in `.origin`.
370+
#[context("Reading image info for deployment {deployment_id}")]
371+
pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result<ImgConfigManifest> {
370372
let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id);
371-
let path = depl_state_path.join(imginfo_fname);
372373

373-
let mut img_conf = storage
374+
let state_dir = storage
374375
.physical_root
375-
.open_optional(&path)
376-
.context("Failed to open file")?;
376+
.open_dir(&depl_state_path)
377+
.with_context(|| format!("Opening state dir for {deployment_id}"))?;
377378

378-
let Some(img_conf) = &mut img_conf else {
379-
let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No imgref or imginfo file found"))?;
379+
let origin_filename = format!("{deployment_id}.origin");
380+
let origin_contents = state_dir
381+
.read_to_string(&origin_filename)
382+
.with_context(|| format!("Reading {origin_filename}"))?;
383+
384+
let ini = tini::Ini::from_string(&origin_contents).context("Failed to parse origin file")?;
380385

381-
let container_details =
382-
get_container_manifest_and_config(&get_imgref(&imgref.transport, &imgref.image))
383-
.await?;
386+
// Try to read the manifest digest from the origin file (new path)
387+
if let Some(manifest_digest) = ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST) {
388+
let repo = storage.get_ensure_composefs()?;
389+
let oci_image = OciImage::<Sha512HashValue>::open(&repo, &manifest_digest, None)
390+
.with_context(|| format!("Opening OCI image for manifest {manifest_digest}"))?;
384391

385-
let state_dir = storage.physical_root.open_dir(depl_state_path)?;
392+
let manifest = oci_image.manifest().clone();
393+
let config = oci_image
394+
.config()
395+
.cloned()
396+
.ok_or_else(|| anyhow::anyhow!("OCI image has no config (artifact?)"))?;
386397

387-
state_dir
388-
.atomic_write(
389-
format!("{}.imginfo", deployment_id),
390-
serde_json::to_vec(&container_details)?,
391-
)
392-
.context("Failed to write to .imginfo file")?;
398+
return Ok(ImgConfigManifest { config, manifest });
399+
}
393400

394-
let state_dir = state_dir.reopen_as_ownedfd()?;
401+
// Fallback: read legacy .imginfo file for deployments created before
402+
// the manifest digest was stored in .origin
403+
let imginfo_fname = format!("{deployment_id}.imginfo");
404+
let path = depl_state_path.join(&imginfo_fname);
395405

396-
rustix::fs::fsync(state_dir).context("fsync")?;
406+
let mut img_conf = storage
407+
.physical_root
408+
.open_optional(&path)
409+
.with_context(|| format!("Opening legacy {imginfo_fname}"))?;
397410

398-
return Ok(container_details);
411+
let Some(img_conf) = &mut img_conf else {
412+
anyhow::bail!(
413+
"No manifest_digest in origin and no legacy .imginfo file \
414+
for deployment {deployment_id}"
415+
);
399416
};
400417

401418
let mut buffer = String::new();
402419
img_conf.read_to_string(&mut buffer)?;
403420

404421
let img_conf = serde_json::from_str::<ImgConfigManifest>(&buffer)
405-
.context("Failed to parse file as JSON")?;
422+
.context("Failed to parse .imginfo file as JSON")?;
406423

407424
Ok(img_conf)
408425
}
409426

410427
#[context("Getting composefs deployment metadata")]
411-
async fn boot_entry_from_composefs_deployment(
428+
fn boot_entry_from_composefs_deployment(
412429
storage: &Storage,
413430
origin: tini::Ini,
414431
verity: &str,
@@ -418,7 +435,7 @@ async fn boot_entry_from_composefs_deployment(
418435
let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
419436
let img_ref = ImageReference::from(ostree_img_ref);
420437

421-
let img_conf = get_imginfo(storage, &verity, Some(&img_ref)).await?;
438+
let img_conf = get_imginfo(storage, &verity)?;
422439

423440
let image_digest = img_conf.manifest.config().digest().to_string();
424441
let architecture = img_conf.config.architecture().to_string();
@@ -742,8 +759,7 @@ async fn composefs_deployment_status_from(
742759
let ini = tini::Ini::from_string(&config)
743760
.with_context(|| format!("Failed to parse file {verity_digest}.origin as ini"))?;
744761

745-
let mut boot_entry =
746-
boot_entry_from_composefs_deployment(storage, ini, &verity_digest).await?;
762+
let mut boot_entry = boot_entry_from_composefs_deployment(storage, ini, &verity_digest)?;
747763

748764
// SAFETY: boot_entry.composefs will always be present
749765
let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type;

crates/lib/src/bootc_composefs/switch.rs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,8 @@ pub(crate) async fn switch_composefs(
7575
}
7676

7777
UpdateAction::Proceed => {
78-
return do_upgrade(
79-
storage,
80-
booted_cfs,
81-
&host,
82-
&target_imgref,
83-
&img_config,
84-
&do_upgrade_opts,
85-
)
86-
.await;
78+
return do_upgrade(storage, booted_cfs, &host, &target_imgref, &do_upgrade_opts)
79+
.await;
8780
}
8881

8982
UpdateAction::UpdateOrigin => {
@@ -95,15 +88,7 @@ pub(crate) async fn switch_composefs(
9588
}
9689
}
9790

98-
do_upgrade(
99-
storage,
100-
booted_cfs,
101-
&host,
102-
&target_imgref,
103-
&img_config,
104-
&do_upgrade_opts,
105-
)
106-
.await?;
91+
do_upgrade(storage, booted_cfs, &host, &target_imgref, &do_upgrade_opts).await?;
10792

10893
Ok(())
10994
}

0 commit comments

Comments
 (0)