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
19 changes: 15 additions & 4 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,13 @@ use rustix::{mount::MountFlags, path::Arg};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::bootc_kargs::compute_new_kargs;
use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
use crate::parsers::grub_menuconfig::MenuEntry;
use crate::task::Task;
use crate::{
bootc_composefs::repo::get_imgref,
composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED},
};
use crate::{
bootc_composefs::repo::open_composefs_repo,
store::{ComposefsFilesystem, Storage},
Expand All @@ -109,6 +111,9 @@ use crate::{
bootc_composefs::state::{get_booted_bls, write_composefs_state},
bootloader::esp_in,
};
use crate::{
bootc_composefs::status::get_container_manifest_and_config, bootc_kargs::compute_new_kargs,
};
use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState};
use crate::{
composefs_consts::{
Expand Down Expand Up @@ -1142,7 +1147,7 @@ pub(crate) fn setup_composefs_uki_boot(
}

#[context("Setting up composefs boot")]
pub(crate) fn setup_composefs_boot(
pub(crate) async fn setup_composefs_boot(
root_setup: &RootSetup,
state: &State,
image_id: &str,
Expand Down Expand Up @@ -1217,7 +1222,13 @@ pub(crate) fn setup_composefs_boot(
false,
boot_type,
boot_digest,
)?;
&get_container_manifest_and_config(&get_imgref(
&state.source.imageref.transport.to_string(),
&state.source.imageref.name,
))
.await?,
)
.await?;

Ok(())
}
Expand Down
47 changes: 42 additions & 5 deletions crates/lib/src/bootc_composefs/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use rustix::{

use crate::bootc_composefs::boot::BootType;
use crate::bootc_composefs::repo::get_imgref;
use crate::bootc_composefs::status::get_sorted_type1_boot_entries;
use crate::bootc_composefs::status::{get_sorted_type1_boot_entries, ImgConfigManifest};
use crate::parsers::bls_config::BLSConfigType;
use crate::store::{BootedComposefs, Storage};
use crate::{
Expand Down Expand Up @@ -151,15 +151,41 @@ pub(crate) fn update_target_imgref_in_origin(
Ok(())
}

/// Creates and populates /sysroot/state/deploy/image_id
/// Creates and populates the composefs state directory for a deployment.
///
/// This function sets up the state directory structure and configuration files
/// needed for a composefs deployment. It creates the deployment state directory,
/// copies configuration, sets up the shared `/var` directory, and writes metadata
/// files including the origin configuration and image information.
///
/// # Arguments
///
/// * `root_path` - The root filesystem path (typically `/sysroot`)
/// * `deployment_id` - Unique SHA512 hash identifier for this deployment
/// * `imgref` - Container image reference for the deployment
/// * `staged` - Whether this is a staged deployment (writes to transient state dir)
/// * `boot_type` - Boot loader type (`Bls` or `Uki`)
/// * `boot_digest` - Optional boot digest for verification
/// * `container_details` - Container manifest and config used to create this deployment
///
/// # State Directory Structure
///
/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`:
/// * `etc/` - Copy of system configuration files
/// * `var` - Symlink to shared `/var` directory
/// * `{deployment_id}.origin` - OSTree-style origin configuration
/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON
///
/// For staged deployments, also writes to `/run/composefs/staged-deployment`.
#[context("Writing composefs state")]
pub(crate) fn write_composefs_state(
pub(crate) async fn write_composefs_state(
root_path: &Utf8PathBuf,
deployment_id: Sha512HashValue,
imgref: &ImageReference,
target_imgref: &ImageReference,
staged: bool,
boot_type: BootType,
boot_digest: Option<String>,
container_details: &ImgConfigManifest,
) -> Result<()> {
let state_path = root_path
.join(STATE_DIR_RELATIVE)
Expand All @@ -183,7 +209,9 @@ pub(crate) fn write_composefs_state(
image: image_name,
transport,
..
} = &imgref;
} = &target_imgref;

println!("imgref: {target_imgref:#?}");

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

Expand All @@ -206,6 +234,15 @@ pub(crate) fn write_composefs_state(
let state_dir =
Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;

// NOTE: This is only supposed to be temporary until we decide on where to store
// the container manifest/config
state_dir
.atomic_write(
format!("{}.imginfo", deployment_id.to_hex()),
serde_json::to_vec(&container_details)?,
)
.context("Failed to write to .imginfo file")?;

state_dir
.atomic_write(
format!("{}.origin", deployment_id.to_hex()),
Expand Down
70 changes: 42 additions & 28 deletions crates/lib/src/bootc_composefs/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{io::Read, sync::OnceLock};
use anyhow::{Context, Result};
use bootc_kernel_cmdline::utf8::Cmdline;
use fn_error_context::context;
use serde::{Deserialize, Serialize};

use crate::{
bootc_composefs::boot::BootType,
Expand All @@ -22,10 +23,10 @@ use std::str::FromStr;
use bootc_utils::try_deserialize_timestamp;
use cap_std_ext::cap_std::fs::Dir;
use ostree_container::OstreeImageReference;
use ostree_ext::container::deploy::ORIGIN_CONTAINER;
use ostree_ext::container::{self as ostree_container};
use ostree_ext::containers_image_proxy;
use ostree_ext::oci_spec;
use ostree_ext::{container::deploy::ORIGIN_CONTAINER, oci_spec::image::ImageConfiguration};

use ostree_ext::oci_spec::image::ImageManifest;
use tokio::io::AsyncReadExt;
Expand All @@ -36,6 +37,13 @@ use crate::composefs_consts::{
};
use crate::spec::Bootloader;

/// Used for storing the container image info alongside of .origin file
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ImgConfigManifest {
pub(crate) config: ImageConfiguration,
pub(crate) manifest: ImageManifest,
}

/// A parsed composefs command line
#[derive(Clone)]
pub(crate) struct ComposefsCmdline {
Expand Down Expand Up @@ -134,7 +142,7 @@ pub(crate) fn get_sorted_type1_boot_entries(
#[context("Getting container info")]
pub(crate) async fn get_container_manifest_and_config(
imgref: &String,
) -> Result<(ImageManifest, oci_spec::image::ImageConfiguration)> {
) -> Result<ImgConfigManifest> {
let config = containers_image_proxy::ImageProxyConfig::default();
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;

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

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

Ok((manifest, config))
Ok(ImgConfigManifest { manifest, config })
}

#[context("Getting bootloader")]
Expand All @@ -173,49 +181,55 @@ pub(crate) fn get_bootloader() -> Result<Bootloader> {
}
}

/// Reads the .imginfo file for the provided deployment
#[context("Reading imginfo")]
pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result<ImgConfigManifest> {
let path = std::path::PathBuf::from(STATE_DIR_RELATIVE)
.join(deployment_id)
.join(format!("{deployment_id}.imginfo"));

let img_conf = storage
.physical_root
.read_to_string(&path)
.context("Failed to open file")?;

let img_conf = serde_json::from_str::<ImgConfigManifest>(&img_conf)
.context("Failed to parse file as JSON")?;

Ok(img_conf)
}

#[context("Getting composefs deployment metadata")]
async fn boot_entry_from_composefs_deployment(
storage: &Storage,
origin: tini::Ini,
verity: String,
) -> Result<BootEntry> {
let image = match origin.get::<String>("origin", ORIGIN_CONTAINER) {
Some(img_name_from_config) => {
let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
let imgref = ostree_img_ref.imgref.to_string();
let img_ref = ImageReference::from(ostree_img_ref);

// The image might've been removed, so don't error if we can't get the image manifest
let (image_digest, version, architecture, created_at) =
match get_container_manifest_and_config(&imgref).await {
Ok((manifest, config)) => {
let digest = manifest.config().digest().to_string();
let arch = config.architecture().to_string();
let created = config.created().clone();
let version = manifest
.annotations()
.as_ref()
.and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());

(digest, version, arch, created)
}
let img_conf = get_imginfo(storage, &verity)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This change removes the fallback mechanism that was present before. If get_imginfo fails (e.g., for an older deployment without an .imginfo file), this will cause bootc status and other commands to fail. While the PR description mentions breaking older systems, a status command should ideally be more resilient.

Consider adding a fallback to the previous behavior of fetching the manifest from the registry if the local .imginfo file is not available. This would provide backward compatibility and a more robust user experience.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider adding a fallback to the previous behavior of fetching the manifest from the registry if the local .imginfo file is not available.

Yeah I also lean towards keeping the previous code for now if it's not too hard

Copy link
Collaborator Author

@Johan-Liebert1 Johan-Liebert1 Dec 8, 2025

Choose a reason for hiding this comment

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

It's definitely not difficult, but we did not want to make a network request on every bootc status so I left it out. Guess we won't make the request anyway after this change. But, also, the previous code had a bug where it would show the "latest" shasum of the image on bootc status even when the image was not pulled into the composefs repo, because we were just fetching everything from a remote repository


Err(e) => {
tracing::debug!("Failed to open image {img_ref}, because {e:?}");
("".into(), None, "".into(), None)
}
};
let image_digest = img_conf.manifest.config().digest().to_string();
let architecture = img_conf.config.architecture().to_string();
let version = img_conf
.manifest
.annotations()
.as_ref()
.and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());

let created_at = img_conf.config.created().clone();
let timestamp = created_at.and_then(|x| try_deserialize_timestamp(&x));

let image_status = ImageStatus {
Some(ImageStatus {
image: img_ref,
version,
timestamp,
image_digest,
architecture,
};

Some(image_status)
})
}

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

let boot_entry =
boot_entry_from_composefs_deployment(ini, depl_file_name.to_string()).await?;
boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?;

// SAFETY: boot_entry.composefs will always be present
let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type;
Expand Down
8 changes: 4 additions & 4 deletions crates/lib/src/bootc_composefs/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ pub(crate) async fn switch_composefs(
};

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

if let Some(cfg_verity) = image {
let action = validate_update(
storage,
booted_cfs,
&host,
manifest.config().digest().digest(),
img_config.manifest.config().digest().digest(),
&cfg_verity,
true,
)?;
Expand All @@ -59,7 +59,7 @@ pub(crate) async fn switch_composefs(
}

UpdateAction::Proceed => {
return do_upgrade(storage, &host, &target_imgref).await;
return do_upgrade(storage, &host, &target_imgref, &img_config).await;
}

UpdateAction::UpdateOrigin => {
Expand All @@ -71,7 +71,7 @@ pub(crate) async fn switch_composefs(
}
}

do_upgrade(storage, &host, &target_imgref).await?;
do_upgrade(storage, &host, &target_imgref, &img_config).await?;

Ok(())
}
Loading