Skip to content
Merged
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
45 changes: 40 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,7 @@ pub(crate) fn write_composefs_state(
image: image_name,
transport,
..
} = &imgref;
} = &target_imgref;

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

Expand All @@ -206,6 +232,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
103 changes: 73 additions & 30 deletions crates/lib/src/bootc_composefs/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ 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,
bootc_composefs::{boot::BootType, repo::get_imgref},
composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG},
install::EFI_LOADER_INFO,
parsers::{
Expand All @@ -20,12 +21,12 @@ use crate::{
use std::str::FromStr;

use bootc_utils::try_deserialize_timestamp;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
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,84 @@ pub(crate) fn get_bootloader() -> Result<Bootloader> {
}
}

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

let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id);
let path = depl_state_path.join(imginfo_fname);

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

let Some(img_conf) = &mut img_conf else {
let container_details =
get_container_manifest_and_config(&get_imgref(&imgref.transport, &imgref.image))
.await?;

let state_dir = storage.physical_root.open_dir(depl_state_path)?;

state_dir
.atomic_write(
format!("{}.imginfo", deployment_id),
serde_json::to_vec(&container_details)?,
)
.context("Failed to write to .imginfo file")?;

let state_dir = state_dir.reopen_as_ownedfd()?;

rustix::fs::fsync(state_dir).context("fsync")?;

return Ok(container_details);
};

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

let img_conf = serde_json::from_str::<ImgConfigManifest>(&buffer)
.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, &img_ref).await?;

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 @@ -313,7 +356,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