Skip to content

Commit 81b09a0

Browse files
composefs: Store image manifest and config
Until now, when doing a `bootc status` for a compoesfs booted system, we were reaching out a container registry to fetch image manifest and config, which is pretty suboptimal as the command took upwards of 1.5s to execute, sometimes. Instead, now we store the manifest + config as a JSON structure inside an `.imginfo` file alongside the `.origin` file Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 2c34df6 commit 81b09a0

File tree

6 files changed

+111
-58
lines changed

6 files changed

+111
-58
lines changed

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ pub(crate) fn setup_composefs_uki_boot(
11421142
}
11431143

11441144
#[context("Setting up composefs boot")]
1145-
pub(crate) fn setup_composefs_boot(
1145+
pub(crate) async fn setup_composefs_boot(
11461146
root_setup: &RootSetup,
11471147
state: &State,
11481148
image_id: &str,
@@ -1217,7 +1217,9 @@ pub(crate) fn setup_composefs_boot(
12171217
false,
12181218
boot_type,
12191219
boot_digest,
1220-
)?;
1220+
None,
1221+
)
1222+
.await?;
12211223

12221224
Ok(())
12231225
}

crates/lib/src/bootc_composefs/state.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ use rustix::{
2424

2525
use crate::bootc_composefs::boot::BootType;
2626
use crate::bootc_composefs::repo::get_imgref;
27-
use crate::bootc_composefs::status::get_sorted_type1_boot_entries;
27+
use crate::bootc_composefs::status::{
28+
get_container_manifest_and_config, get_sorted_type1_boot_entries, ImgConfigManifest,
29+
};
2830
use crate::parsers::bls_config::BLSConfigType;
2931
use crate::store::{BootedComposefs, Storage};
3032
use crate::{
@@ -151,15 +153,41 @@ pub(crate) fn update_target_imgref_in_origin(
151153
Ok(())
152154
}
153155

154-
/// Creates and populates /sysroot/state/deploy/image_id
156+
/// Creates and populates the composefs state directory for a deployment.
157+
///
158+
/// This function sets up the state directory structure and configuration files
159+
/// needed for a composefs deployment. It creates the deployment state directory,
160+
/// copies configuration, sets up the shared `/var` directory, and writes metadata
161+
/// files including the origin configuration and image information.
162+
///
163+
/// # Arguments
164+
///
165+
/// * `root_path` - The root filesystem path (typically `/sysroot`)
166+
/// * `deployment_id` - Unique SHA512 hash identifier for this deployment
167+
/// * `imgref` - Container image reference for the deployment
168+
/// * `staged` - Whether this is a staged deployment (writes to transient state dir)
169+
/// * `boot_type` - Boot loader type (`Bls` or `Uki`)
170+
/// * `boot_digest` - Optional boot digest for verification
171+
/// * `container_details` - Optional container manifest and config. Fetched if not provided
172+
///
173+
/// # State Directory Structure
174+
///
175+
/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`:
176+
/// * `etc/` - Copy of system configuration files
177+
/// * `var` - Symlink to shared `/var` directory
178+
/// * `{deployment_id}.origin` - OSTree-style origin configuration
179+
/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON
180+
///
181+
/// For staged deployments, also writes to `/run/composefs/staged-deployment`.
155182
#[context("Writing composefs state")]
156-
pub(crate) fn write_composefs_state(
183+
pub(crate) async fn write_composefs_state(
157184
root_path: &Utf8PathBuf,
158185
deployment_id: Sha512HashValue,
159186
imgref: &ImageReference,
160187
staged: bool,
161188
boot_type: BootType,
162189
boot_digest: Option<String>,
190+
container_details: Option<&ImgConfigManifest>,
163191
) -> Result<()> {
164192
let state_path = root_path
165193
.join(STATE_DIR_RELATIVE)
@@ -187,6 +215,11 @@ pub(crate) fn write_composefs_state(
187215

188216
let imgref = get_imgref(&transport, &image_name);
189217

218+
let img_config = match container_details {
219+
Some(val) => val,
220+
None => &get_container_manifest_and_config(&imgref).await?,
221+
};
222+
190223
let mut config = tini::Ini::new().section("origin").item(
191224
ORIGIN_CONTAINER,
192225
// TODO (Johan-Liebert1): The image won't always be unverified
@@ -206,6 +239,15 @@ pub(crate) fn write_composefs_state(
206239
let state_dir =
207240
Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
208241

242+
// NOTE: This is only supposed to be temporary until we decide on where to store
243+
// the container manifest/config
244+
state_dir
245+
.atomic_write(
246+
format!("{}.imginfo", deployment_id.to_hex()),
247+
serde_json::to_vec(&img_config)?,
248+
)
249+
.context("Failed to write to .imginfo file")?;
250+
209251
state_dir
210252
.atomic_write(
211253
format!("{}.origin", deployment_id.to_hex()),

crates/lib/src/bootc_composefs/status.rs

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{io::Read, sync::OnceLock};
33
use anyhow::{Context, Result};
44
use bootc_kernel_cmdline::utf8::Cmdline;
55
use fn_error_context::context;
6+
use serde::{Deserialize, Serialize};
67

78
use crate::{
89
bootc_composefs::boot::BootType,
@@ -22,10 +23,10 @@ use std::str::FromStr;
2223
use bootc_utils::try_deserialize_timestamp;
2324
use cap_std_ext::cap_std::fs::Dir;
2425
use ostree_container::OstreeImageReference;
25-
use ostree_ext::container::deploy::ORIGIN_CONTAINER;
2626
use ostree_ext::container::{self as ostree_container};
2727
use ostree_ext::containers_image_proxy;
2828
use ostree_ext::oci_spec;
29+
use ostree_ext::{container::deploy::ORIGIN_CONTAINER, oci_spec::image::ImageConfiguration};
2930

3031
use ostree_ext::oci_spec::image::ImageManifest;
3132
use tokio::io::AsyncReadExt;
@@ -36,6 +37,13 @@ use crate::composefs_consts::{
3637
};
3738
use crate::spec::Bootloader;
3839

40+
/// Used for storing the container image info alongside of .origin file
41+
#[derive(Debug, Serialize, Deserialize)]
42+
pub(crate) struct ImgConfigManifest {
43+
pub(crate) config: ImageConfiguration,
44+
pub(crate) manifest: ImageManifest,
45+
}
46+
3947
/// A parsed composefs command line
4048
#[derive(Clone)]
4149
pub(crate) struct ComposefsCmdline {
@@ -134,7 +142,7 @@ pub(crate) fn get_sorted_type1_boot_entries(
134142
#[context("Getting container info")]
135143
pub(crate) async fn get_container_manifest_and_config(
136144
imgref: &String,
137-
) -> Result<(ImageManifest, oci_spec::image::ImageConfiguration)> {
145+
) -> Result<ImgConfigManifest> {
138146
let config = containers_image_proxy::ImageProxyConfig::default();
139147
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
140148

@@ -150,7 +158,7 @@ pub(crate) async fn get_container_manifest_and_config(
150158

151159
let config: oci_spec::image::ImageConfiguration = serde_json::from_slice(&buf)?;
152160

153-
Ok((manifest, config))
161+
Ok(ImgConfigManifest { manifest, config })
154162
}
155163

156164
#[context("Getting bootloader")]
@@ -175,47 +183,45 @@ pub(crate) fn get_bootloader() -> Result<Bootloader> {
175183

176184
#[context("Getting composefs deployment metadata")]
177185
async fn boot_entry_from_composefs_deployment(
186+
storage: &Storage,
178187
origin: tini::Ini,
179188
verity: String,
180189
) -> Result<BootEntry> {
181190
let image = match origin.get::<String>("origin", ORIGIN_CONTAINER) {
182191
Some(img_name_from_config) => {
183192
let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
184-
let imgref = ostree_img_ref.imgref.to_string();
185193
let img_ref = ImageReference::from(ostree_img_ref);
186194

187-
// The image might've been removed, so don't error if we can't get the image manifest
188-
let (image_digest, version, architecture, created_at) =
189-
match get_container_manifest_and_config(&imgref).await {
190-
Ok((manifest, config)) => {
191-
let digest = manifest.config().digest().to_string();
192-
let arch = config.architecture().to_string();
193-
let created = config.created().clone();
194-
let version = manifest
195-
.annotations()
196-
.as_ref()
197-
.and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());
198-
199-
(digest, version, arch, created)
200-
}
195+
let path = std::path::PathBuf::from(STATE_DIR_RELATIVE)
196+
.join(&verity)
197+
.join(format!("{verity}.imginfo"));
201198

202-
Err(e) => {
203-
tracing::debug!("Failed to open image {img_ref}, because {e:?}");
204-
("".into(), None, "".into(), None)
205-
}
206-
};
199+
let img_conf = storage
200+
.physical_root
201+
.read_to_string(&path)
202+
.context("Failed to open imginfo file")?;
203+
204+
let img_conf: ImgConfigManifest =
205+
serde_json::from_str(&img_conf).context("Failed to parse imginfo file as JSON")?;
207206

207+
let image_digest = img_conf.manifest.config().digest().to_string();
208+
let architecture = img_conf.config.architecture().to_string();
209+
let version = img_conf
210+
.manifest
211+
.annotations()
212+
.as_ref()
213+
.and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());
214+
215+
let created_at = img_conf.config.created().clone();
208216
let timestamp = created_at.and_then(|x| try_deserialize_timestamp(&x));
209217

210-
let image_status = ImageStatus {
218+
Some(ImageStatus {
211219
image: img_ref,
212220
version,
213221
timestamp,
214222
image_digest,
215223
architecture,
216-
};
217-
218-
Some(image_status)
224+
})
219225
}
220226

221227
// Wasn't booted using a container image. Do nothing
@@ -312,7 +318,7 @@ pub(crate) async fn composefs_deployment_status_from(
312318
.with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
313319

314320
let boot_entry =
315-
boot_entry_from_composefs_deployment(ini, depl_file_name.to_string()).await?;
321+
boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?;
316322

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

crates/lib/src/bootc_composefs/switch.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ pub(crate) async fn switch_composefs(
4040
};
4141

4242
let repo = &*booted_cfs.repo;
43-
let (image, manifest, _) = is_image_pulled(repo, &target_imgref).await?;
43+
let (image, img_config) = is_image_pulled(repo, &target_imgref).await?;
4444

4545
if let Some(cfg_verity) = image {
4646
let action = validate_update(
4747
storage,
4848
booted_cfs,
4949
&host,
50-
manifest.config().digest().digest(),
50+
img_config.manifest.config().digest().digest(),
5151
&cfg_verity,
5252
true,
5353
)?;
@@ -59,7 +59,7 @@ pub(crate) async fn switch_composefs(
5959
}
6060

6161
UpdateAction::Proceed => {
62-
return do_upgrade(storage, &host, &target_imgref).await;
62+
return do_upgrade(storage, &host, &target_imgref, &img_config).await;
6363
}
6464

6565
UpdateAction::UpdateOrigin => {
@@ -71,7 +71,7 @@ pub(crate) async fn switch_composefs(
7171
}
7272
}
7373

74-
do_upgrade(storage, &host, &target_imgref).await?;
74+
do_upgrade(storage, &host, &target_imgref, &img_config).await?;
7575

7676
Ok(())
7777
}

0 commit comments

Comments
 (0)