Skip to content

Commit d1f366d

Browse files
committed
composefs: Read manifest+config from composefs repo
The latest composefs-rs stores manifest and config objects and the manifest becomes a GC root, so we can use that instead of `.imginfo` sidecar files. The flow now is: bootloader entry -> deployment -> origin file -> manifest digest -> manifest -> [config | objects] For backward compatibility, fall back to the legacy .imginfo file if the .origin does not contain a manifest_digest key. Drop the really old hacky fallback that did network fetches. Note the manifest becomes part of the GC root. Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 782ceb6 commit d1f366d

File tree

10 files changed

+117
-141
lines changed

10 files changed

+117
-141
lines changed

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: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ use crate::{
1515
bootc_composefs::{
1616
boot::{BOOTC_UKI_DIR, BootType, get_type1_dir_name, get_uki_addon_dir_name, get_uki_name},
1717
delete::{delete_image, delete_staged, delete_state_dir},
18+
state::read_origin,
1819
status::{get_composefs_status, get_imginfo, list_bootloader_entries},
1920
},
20-
composefs_consts::{STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX},
21+
composefs_consts::{
22+
ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST, STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX,
23+
UKI_NAME_PREFIX,
24+
},
2125
store::{BootedComposefs, Storage},
2226
};
2327

@@ -313,11 +317,22 @@ pub(crate) async fn composefs_gc(
313317
continue;
314318
}
315319

316-
let image = get_imginfo(storage, verity, None).await?;
317-
let stream = format!("oci-config-{}", image.manifest.config().digest());
320+
let image = get_imginfo(storage, verity)?;
321+
let config_stream = format!("oci-config-{}", image.manifest.config().digest());
318322

319323
additional_roots.push(verity.clone());
320-
additional_roots.push(stream);
324+
additional_roots.push(config_stream);
325+
326+
// Also protect the manifest stream so that `bootc status` can
327+
// read the manifest+config via OciImage::open() after GC.
328+
if let Some(ini) = read_origin(sysroot, verity)? {
329+
if let Some(manifest_digest) =
330+
ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST)
331+
{
332+
let manifest_stream = format!("oci-manifest-{manifest_digest}");
333+
additional_roots.push(manifest_stream);
334+
}
335+
}
321336
}
322337

323338
let additional_roots = additional_roots

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
@@ -27,16 +27,14 @@ use rustix::{
2727

2828
use crate::bootc_composefs::boot::BootType;
2929
use crate::bootc_composefs::repo::get_imgref;
30-
use crate::bootc_composefs::status::{
31-
ImgConfigManifest, StagedDeployment, get_sorted_type1_boot_entries,
32-
};
30+
use crate::bootc_composefs::status::{StagedDeployment, get_sorted_type1_boot_entries};
3331
use crate::parsers::bls_config::BLSConfigType;
3432
use crate::store::{BootedComposefs, Storage};
3533
use crate::{
3634
composefs_consts::{
3735
COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
38-
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH,
39-
STATE_DIR_RELATIVE,
36+
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, ORIGIN_KEY_IMAGE,
37+
ORIGIN_KEY_MANIFEST_DIGEST, SHARED_VAR_PATH, STATE_DIR_RELATIVE,
4038
},
4139
parsers::bls_config::BLSConfig,
4240
spec::ImageReference,
@@ -244,15 +242,15 @@ pub(crate) fn update_boot_digest_in_origin(
244242
/// * `staged` - Whether this is a staged deployment (writes to transient state dir)
245243
/// * `boot_type` - Boot loader type (`Bls` or `Uki`)
246244
/// * `boot_digest` - Optional boot digest for verification
247-
/// * `container_details` - Container manifest and config used to create this deployment
245+
/// * `manifest_digest` - OCI manifest content digest, stored in the origin file so the
246+
/// manifest+config can be retrieved from the composefs repo later
248247
///
249248
/// # State Directory Structure
250249
///
251250
/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`:
252251
/// * `etc/` - Copy of system configuration files
253252
/// * `var` - Symlink to shared `/var` directory
254-
/// * `{deployment_id}.origin` - OSTree-style origin configuration
255-
/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON
253+
/// * `{deployment_id}.origin` - Origin configuration with image ref, boot, and image metadata
256254
///
257255
/// For staged deployments, also writes to `/run/composefs/staged-deployment`.
258256
#[context("Writing composefs state")]
@@ -263,7 +261,7 @@ pub(crate) async fn write_composefs_state(
263261
staged: Option<StagedDeployment>,
264262
boot_type: BootType,
265263
boot_digest: String,
266-
container_details: &ImgConfigManifest,
264+
manifest_digest: &str,
267265
allow_missing_fsverity: bool,
268266
) -> Result<()> {
269267
let state_path = root_path
@@ -312,18 +310,15 @@ pub(crate) async fn write_composefs_state(
312310
.section(ORIGIN_KEY_BOOT)
313311
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
314312

313+
// Store the OCI manifest digest so we can retrieve the manifest+config
314+
// from the composefs repository later (composefs-rs stores them as splitstreams).
315+
config = config
316+
.section(ORIGIN_KEY_IMAGE)
317+
.item(ORIGIN_KEY_MANIFEST_DIGEST, manifest_digest);
318+
315319
let state_dir =
316320
Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
317321

318-
// NOTE: This is only supposed to be temporary until we decide on where to store
319-
// the container manifest/config
320-
state_dir
321-
.atomic_write(
322-
format!("{}.imginfo", deployment_id.to_hex()),
323-
serde_json::to_vec(&container_details)?,
324-
)
325-
.context("Failed to write to .imginfo file")?;
326-
327322
state_dir
328323
.atomic_write(
329324
format!("{}.origin", deployment_id.to_hex()),

crates/lib/src/bootc_composefs/status.rs

Lines changed: 58 additions & 40 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, read_origin},
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,63 @@ 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");
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> {
372+
let ini = read_origin(&storage.physical_root, deployment_id)?
373+
.ok_or_else(|| anyhow::anyhow!("No origin file for deployment {deployment_id}"))?;
374+
375+
// Try to read the manifest digest from the origin file (new path)
376+
if let Some(manifest_digest) = ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST) {
377+
let repo = storage.get_ensure_composefs()?;
378+
let oci_image = OciImage::<Sha512HashValue>::open(&repo, &manifest_digest, None)
379+
.with_context(|| format!("Opening OCI image for manifest {manifest_digest}"))?;
380+
381+
let manifest = oci_image.manifest().clone();
382+
let config = oci_image
383+
.config()
384+
.cloned()
385+
.ok_or_else(|| anyhow::anyhow!("OCI image has no config (artifact?)"))?;
386+
387+
return Ok(ImgConfigManifest { config, manifest });
388+
}
369389

390+
// Fallback: read legacy .imginfo file for deployments created before
391+
// the manifest digest was stored in .origin
370392
let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id);
371-
let path = depl_state_path.join(imginfo_fname);
393+
let imginfo_fname = format!("{deployment_id}.imginfo");
394+
let path = depl_state_path.join(&imginfo_fname);
372395

373396
let mut img_conf = storage
374397
.physical_root
375398
.open_optional(&path)
376-
.context("Failed to open file")?;
399+
.with_context(|| format!("Opening legacy {imginfo_fname}"))?;
377400

378401
let Some(img_conf) = &mut img_conf else {
379-
let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No imgref or imginfo file found"))?;
380-
381-
let container_details =
382-
get_container_manifest_and_config(&get_imgref(&imgref.transport, &imgref.image))
383-
.await?;
384-
385-
let state_dir = storage.physical_root.open_dir(depl_state_path)?;
386-
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")?;
393-
394-
let state_dir = state_dir.reopen_as_ownedfd()?;
395-
396-
rustix::fs::fsync(state_dir).context("fsync")?;
397-
398-
return Ok(container_details);
402+
anyhow::bail!(
403+
"No manifest_digest in origin and no legacy .imginfo file \
404+
for deployment {deployment_id}"
405+
);
399406
};
400407

401408
let mut buffer = String::new();
402409
img_conf.read_to_string(&mut buffer)?;
403410

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

407414
Ok(img_conf)
408415
}
409416

410417
#[context("Getting composefs deployment metadata")]
411-
async fn boot_entry_from_composefs_deployment(
418+
fn boot_entry_from_composefs_deployment(
412419
storage: &Storage,
413420
origin: tini::Ini,
414421
verity: &str,
@@ -418,7 +425,7 @@ async fn boot_entry_from_composefs_deployment(
418425
let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
419426
let img_ref = ImageReference::from(ostree_img_ref);
420427

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

423430
let image_digest = img_conf.manifest.config().digest().to_string();
424431
let architecture = img_conf.config.architecture().to_string();
@@ -699,6 +706,11 @@ async fn composefs_deployment_status_from(
699706
// This is our source of truth
700707
let bootloader_entry_verity = list_bootloader_entries(storage)?;
701708

709+
let state_dir = storage
710+
.physical_root
711+
.open_dir(STATE_DIR_RELATIVE)
712+
.with_context(|| format!("Opening {STATE_DIR_RELATIVE}"))?;
713+
702714
let host_spec = HostSpec {
703715
image: None,
704716
boot_order: BootOrder::Default,
@@ -727,11 +739,17 @@ async fn composefs_deployment_status_from(
727739
let mut extra_deployment_boot_entries: Vec<BootEntry> = Vec::new();
728740

729741
for verity_digest in bootloader_entry_verity {
730-
let ini = read_origin(&storage.physical_root, &verity_digest)?
731-
.ok_or_else(|| anyhow::anyhow!("No origin file for deployment {verity_digest}"))?;
742+
// read the origin file
743+
let config = state_dir
744+
.open_dir(&verity_digest)
745+
.with_context(|| format!("Failed to open {verity_digest}"))?
746+
.read_to_string(format!("{verity_digest}.origin"))
747+
.with_context(|| format!("Reading file {verity_digest}.origin"))?;
748+
749+
let ini = tini::Ini::from_string(&config)
750+
.with_context(|| format!("Failed to parse file {verity_digest}.origin as ini"))?;
732751

733-
let mut boot_entry =
734-
boot_entry_from_composefs_deployment(storage, ini, &verity_digest).await?;
752+
let mut boot_entry = boot_entry_from_composefs_deployment(storage, ini, &verity_digest)?;
735753

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

0 commit comments

Comments
 (0)