Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
260c20e
WIP: composefs backend
Johan-Liebert1 May 7, 2025
24d0e98
Rework composefs_booted to use kernel cmdline
cgwalters Aug 7, 2025
d3a66c9
composefs/status: Read UKI entries to check for queued rollback
Johan-Liebert1 Jul 27, 2025
9694943
parser/grub: Use String instead of &str
Johan-Liebert1 Jul 27, 2025
8820dad
composefs/rollback: Handle UKI rollback
Johan-Liebert1 Jul 27, 2025
78e3fc8
composefs/state: Use atomic writes for origin and staged deployment f…
Johan-Liebert1 Jul 28, 2025
7e489dd
composefs/boot/bls: Handle duplicate VMLinuz + Initrd
Johan-Liebert1 Jul 28, 2025
c2ed79b
parser/bls: `impl Display` for BLSConfig
Johan-Liebert1 Jul 28, 2025
67590ce
lib/composefs: Centralize constants
Johan-Liebert1 Jul 29, 2025
52f2b5c
composefs/state: Name state directory `default`
Johan-Liebert1 Jul 29, 2025
2c51b6a
parser/bls: Add tests for bls parser
Johan-Liebert1 Jul 29, 2025
eefddd2
install/composefs/uki: Write only staged + booted menuentry on upgrade
Johan-Liebert1 Jul 31, 2025
d263a1f
rollback/composefs: Print whether we are reverting the queued rollback
Johan-Liebert1 Jul 31, 2025
6d33975
refactor: Pass boot dir to boot entry readers
Johan-Liebert1 Aug 1, 2025
ef3f885
test: Add tests for reading boot entries
Johan-Liebert1 Aug 1, 2025
224f838
Drop duplicate bls_config
cgwalters Aug 12, 2025
ffbc273
install: Use read_file from composefs-boot
cgwalters Aug 13, 2025
2afa715
install: Fix cargo fmt
cgwalters Aug 19, 2025
2ff7a13
status: Use constant for composefs
cgwalters Aug 19, 2025
6d5ead8
status: Enhance composefs cmdline parsing to handle ?
cgwalters Aug 19, 2025
42e44bb
composefs-backend: store boot assets in ESP during install
p5 Aug 23, 2025
b15b7b0
Post-rebase fixups:
jeckersb Aug 28, 2025
24bf572
composefs/install/bls: Fix empty version in config
Johan-Liebert1 Aug 28, 2025
023be10
composefs/install: Copy /etc contents to state
Johan-Liebert1 Aug 29, 2025
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/kernel_cmdline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ mod tests {
// non-UTF8 things are in fact valid
let non_utf8_byte = b"\xff";
#[allow(invalid_from_utf8)]
let failed_conversion = str::from_utf8(non_utf8_byte);
let failed_conversion = std::str::from_utf8(non_utf8_byte);
assert!(failed_conversion.is_err());
let mut p = b"foo=".to_vec();
p.push(non_utf8_byte[0]);
Expand Down
1 change: 1 addition & 0 deletions crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bootc-sysusers = { path = "../sysusers" }
bootc-tmpfiles = { path = "../tmpfiles" }
bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" }
ostree-ext = { path = "../ostree-ext", features = ["bootc"] }
bootc-initramfs-setup = { path = "../initramfs" }

# Workspace dependencies
anstream = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/bootc_composefs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod state;
47 changes: 47 additions & 0 deletions crates/lib/src/bootc_composefs/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::process::Command;

use anyhow::{Context, Result};
use bootc_utils::CommandRunExt;
use camino::Utf8PathBuf;
use fn_error_context::context;

use rustix::{
fs::{open, Mode, OFlags, CWD},
mount::{unmount, UnmountFlags},
path::Arg,
};

/// Mounts an EROFS image and copies the pristine /etc to the deployment's /etc
#[context("Copying etc")]
pub(crate) fn copy_etc_to_state(
sysroot_path: &Utf8PathBuf,
erofs_id: &String,
state_path: &Utf8PathBuf,
) -> Result<()> {
let sysroot_fd = open(
sysroot_path.as_std_path(),
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)
.context("Opening sysroot")?;

let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?;

let tempdir = tempfile::tempdir().context("Creating tempdir")?;

bootc_initramfs_setup::mount_at_wrapper(composefs_fd, CWD, tempdir.path())?;

// TODO: Replace this with a function to cap_std_ext
let cp_ret = Command::new("cp")
.args([
"-a",
&format!("{}/etc/.", tempdir.path().as_str()?),
&format!("{state_path}/etc/."),
])
.run_capture_stderr();
Comment on lines +35 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The use of tempdir.path().as_str()? could cause an error if the temporary directory path contains non-UTF-8 characters. While this is uncommon on many systems, it's safer to handle this potential failure explicitly. You can use camino::Utf8Path::from_path to convert the std::path::Path to a camino::Utf8Path and handle the Option result.

Suggested change
let cp_ret = Command::new("cp")
.args([
"-a",
&format!("{}/etc/.", tempdir.path().as_str()?),
&format!("{state_path}/etc/."),
])
.run_capture_stderr();
let temp_path = camino::Utf8Path::from_path(tempdir.path()).ok_or_else(|| anyhow::anyhow!("Tempdir path is not valid UTF-8"))?;
let cp_ret = Command::new("cp")
.args([
"-a",
&format!("{}/etc/.", temp_path),
&format!("{state_path}/etc/."),
])
.run_capture_stderr();


// Unmount regardless of copy succeeding
unmount(tempdir.path(), UnmountFlags::DETACH).context("Unmounting composefs")?;

cp_ret
}
11 changes: 8 additions & 3 deletions crates/lib/src/bootloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@ pub(crate) fn install_via_bootupd(
device: &PartitionTable,
rootfs: &Utf8Path,
configopts: &crate::install::InstallConfigOpts,
deployment_path: &str,
deployment_path: Option<&str>,
) -> Result<()> {
let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
// bootc defaults to only targeting the platform boot method.
let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);

let srcroot = rootfs.join(deployment_path);
let abs_deployment_path = deployment_path.map(|v| rootfs.join(v));
let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() {
vec!["--src-root", p.as_str()]
} else {
vec![]
};
let devpath = device.path();
println!("Installing bootloader via bootupd");
Command::new("bootupctl")
.args(["backend", "install", "--write-uuid"])
.args(verbose)
.args(bootupd_opts.iter().copied().flatten())
.args(["--src-root", srcroot.as_str()])
.args(src_root_arg)
.args(["--device", devpath.as_str(), rootfs.as_str()])
.log_debug()
.run_inherited_with_cmd_context()
Expand Down
192 changes: 170 additions & 22 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ use ostree_ext::sysroot::SysrootLock;
use schemars::schema_for;
use serde::{Deserialize, Serialize};

use crate::deploy::RequiredHostSpec;
use crate::deploy::{composefs_rollback, RequiredHostSpec};
use crate::install::{
pull_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state,
BootSetupType, BootType,
};
use crate::lints;
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
use crate::spec::ImageReference;
use crate::status::{composefs_booted, composefs_deployment_status};
use crate::utils::sigpolicy_from_opt;

/// Shared progress options
Expand Down Expand Up @@ -903,6 +908,69 @@ fn prepare_for_write() -> Result<()> {
Ok(())
}

#[context("Upgrading composefs")]
async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> {
// TODO: IMPORTANT Have all the checks here that `bootc upgrade` has for an ostree booted system

let host = composefs_deployment_status()
.await
.context("Getting composefs deployment status")?;

// TODO: IMPORTANT We need to check if any deployment is staged and get the image from that
let imgref = host
.spec
.image
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;

// let booted_image = host
// .status
// .booted
// .ok_or(anyhow::anyhow!("Could not find booted image"))?
// .image
// .ok_or(anyhow::anyhow!("Could not find booted image"))?;

// tracing::debug!("booted_image: {booted_image:#?}");
// tracing::debug!("imgref: {imgref:#?}");

// let digest = booted_image
// .digest()
// .context("Getting digest for booted image")?;

let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?;

let Some(entry) = entries.into_iter().next() else {
anyhow::bail!("No boot entries!");
};

let boot_type = BootType::from(&entry);
let mut boot_digest = None;

match boot_type {
BootType::Bls => {
boot_digest = Some(setup_composefs_bls_boot(
BootSetupType::Upgrade(&fs),
repo,
&id,
entry,
)?)
}

BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade(&fs), repo, &id, entry)?,
};

write_composefs_state(
&Utf8PathBuf::from("/sysroot"),
id,
imgref,
true,
boot_type,
boot_digest,
)?;

Ok(())
}

/// Implementation of the `bootc upgrade` CLI command.
#[context("Upgrading")]
async fn upgrade(opts: UpgradeOpts) -> Result<()> {
Expand Down Expand Up @@ -1016,9 +1084,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
Ok(())
}

/// Implementation of the `bootc switch` CLI command.
#[context("Switching")]
async fn switch(opts: SwitchOpts) -> Result<()> {
fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
let imgref = ostree_container::ImageReference {
transport,
Expand All @@ -1027,6 +1093,73 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
let target = ostree_container::OstreeImageReference { sigverify, imgref };
let target = ImageReference::from(target);

return Ok(target);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

In Rust, it's more idiomatic to omit the return keyword for the final expression in a function.

    Ok(target)

}

#[context("Composefs Switching")]
async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
let target = imgref_for_switch(&opts)?;
// TODO: Handle in-place

let host = composefs_deployment_status()
.await
.context("Getting composefs deployment status")?;

let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
new_spec
};

if new_spec == host.spec {
println!("Image specification is unchanged.");
return Ok(());
}

let Some(target_imgref) = new_spec.image else {
anyhow::bail!("Target image is undefined")
};

let (repo, entries, id, fs) =
pull_composefs_repo(&"docker".into(), &target_imgref.image).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The transport for pull_composefs_repo is hardcoded to "docker". This will likely fail for images from other transports. The target_imgref already contains the correct transport, which should be used instead.

        pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?;


let Some(entry) = entries.into_iter().next() else {
anyhow::bail!("No boot entries!");
};

let boot_type = BootType::from(&entry);
let mut boot_digest = None;

match boot_type {
BootType::Bls => {
boot_digest = Some(setup_composefs_bls_boot(
BootSetupType::Upgrade(&fs),
repo,
&id,
entry,
)?)
}
BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade(&fs), repo, &id, entry)?,
};

write_composefs_state(
&Utf8PathBuf::from("/sysroot"),
id,
&target_imgref,
true,
boot_type,
boot_digest,
)?;

Ok(())
}

/// Implementation of the `bootc switch` CLI command.
#[context("Switching")]
async fn switch(opts: SwitchOpts) -> Result<()> {
let target = imgref_for_switch(&opts)?;

let prog: ProgressWriter = opts.progress.try_into()?;

// If we're doing an in-place mutation, we shortcut most of the rest of the work here
Expand Down Expand Up @@ -1118,21 +1251,25 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
/// Implementation of the `bootc rollback` CLI command.
#[context("Rollback")]
async fn rollback(opts: RollbackOpts) -> Result<()> {
let sysroot = &get_storage().await?;
let ostree = sysroot.get_ostree()?;
crate::deploy::rollback(sysroot).await?;
if composefs_booted()?.is_some() {
composefs_rollback().await?
} else {
let sysroot = &get_storage().await?;
let ostree = sysroot.get_ostree()?;
crate::deploy::rollback(sysroot).await?;

if opts.soft_reboot.is_some() {
// Get status of rollback deployment to check soft-reboot capability
let host = crate::status::get_status_require_booted(ostree)?.2;

handle_soft_reboot(
opts.soft_reboot,
host.status.rollback.as_ref(),
"rollback",
|| soft_reboot_rollback(ostree),
)?;
}
if opts.soft_reboot.is_some() {
// Get status of rollback deployment to check soft-reboot capability
let host = crate::status::get_status_require_booted(ostree)?.2;

handle_soft_reboot(
opts.soft_reboot,
host.status.rollback.as_ref(),
"rollback",
|| soft_reboot_rollback(ostree),
)?;
}
};

if opts.apply {
crate::reboot::reboot()?;
Expand Down Expand Up @@ -1282,8 +1419,20 @@ impl Opt {
async fn run_from_opt(opt: Opt) -> Result<()> {
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::Upgrade(opts) => {
if composefs_booted()?.is_some() {
upgrade_composefs(opts).await
} else {
upgrade(opts).await
}
}
Opt::Switch(opts) => {
if composefs_booted()?.is_some() {
switch_composefs(opts).await
} else {
switch(opts).await
}
}
Opt::Rollback(opts) => rollback(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
Expand Down Expand Up @@ -1424,8 +1573,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
FsverityOpts::Enable { path } => {
let fd =
std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
// Note this is not robust to forks, we're not using the _maybe_copy variant
fsverity::enable_verity_with_retry::<fsverity::Sha256HashValue>(&fd)?;
fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
Ok(())
}
},
Expand Down
35 changes: 35 additions & 0 deletions crates/lib/src/composefs_consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// composefs= paramter in kernel cmdline
pub const COMPOSEFS_CMDLINE: &str = "composefs";

/// Directory to store transient state, such as staged deployemnts etc
pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs";
/// File created in /run/composefs to record a staged-deployment
pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment";

/// Absolute path to composefs-native state directory
pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy";
/// Relative path to composefs-native state directory. Relative to /sysroot
pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy";
/// Relative path to the shared 'var' directory. Relative to /sysroot
pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var";

/// Section in .origin file to store boot related metadata
pub(crate) const ORIGIN_KEY_BOOT: &str = "boot";
/// Whether the deployment was booted with BLS or UKI
pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type";
/// Key to store the SHA256 sum of vmlinuz + initrd for a deployment
pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest";

/// Filename for `loader/entries`
pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries";
/// Filename for staged boot loader entries
pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged";
/// Filename for rollback boot loader entries
pub(crate) const ROLLBACK_BOOT_LOADER_ENTRIES: &str = STAGED_BOOT_LOADER_ENTRIES;

/// Filename for grub user config
pub(crate) const USER_CFG: &str = "user.cfg";
/// Filename for staged grub user config
pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged";
/// Filename for rollback grub user config
pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED;
Loading