Skip to content
Closed
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
72 changes: 72 additions & 0 deletions crates/lib/src/bootc_composefs/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Detecting which backend a container image's root filesystem is built for.

use anyhow::Result;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt as _;

/// The setup-root configuration, relative to the root directory.
pub(crate) fn setup_root_conf_path() -> &'static str {
bootc_initramfs_setup::SETUP_ROOT_CONF_PATH.trim_start_matches('/')
}

/// Whether the image is intended to be deployed with the composefs
/// backend, which is signaled by the presence of a setup-root configuration
/// file (even if empty).
pub(crate) fn is_composefs_native(root: &Dir) -> Result<bool> {
Ok(root
.symlink_metadata_optional(setup_root_conf_path())?
.is_some())
}

/// Whether `bootc install` should default to the composefs backend for this
/// root: it is composefs-native, and it has no ostree `prepare-root.conf`
/// (`has_ostree_prepareroot`, which the caller loads anyway), without which
/// it can't be installed with the ostree backend. An image that has both
/// configurations still defaults to ostree.
pub(crate) fn defaults_to_composefs_backend(
root: &Dir,
has_ostree_prepareroot: bool,
) -> Result<bool> {
Ok(!has_ostree_prepareroot && is_composefs_native(root)?)
}

#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8Path;
use ostree_ext::ostree_prepareroot;

const OSTREE_PREPAREROOT: &str = "usr/lib/ostree/prepare-root.conf";
const OSTREE_PREPAREROOT_ETC: &str = "etc/ostree/prepare-root.conf";

#[test]
fn test_defaults_to_composefs_backend() -> Result<()> {
let setup_root = setup_root_conf_path();
// (files present in the image) => (composefs-native, defaults to composefs)
let cases: &[(&[&str], bool, bool)] = &[
(&[], false, false),
(&[setup_root], true, true),
(&[OSTREE_PREPAREROOT], false, false),
(&[OSTREE_PREPAREROOT_ETC], false, false),
(&[setup_root, OSTREE_PREPAREROOT], true, false),
(&[setup_root, OSTREE_PREPAREROOT_ETC], true, false),
];
for &(files, native, composefs) in cases {
let td = cap_std_ext::cap_tempfile::tempdir(cap_std_ext::cap_std::ambient_authority())?;
for f in files {
let f = Utf8Path::new(f);
td.create_dir_all(f.parent().unwrap())?;
// Both files are signals even if empty
td.write(f, "")?;
}
assert_eq!(is_composefs_native(&td)?, native, "{files:?}");
let has_ostree = ostree_prepareroot::load_config_from_root(&td)?.is_some();
assert_eq!(
defaults_to_composefs_backend(&td, has_ostree)?,
composefs,
"{files:?}"
);
}
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/lib/src/bootc_composefs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) mod digest;
pub(crate) mod export;
pub(crate) mod finalize;
pub(crate) mod gc;
pub(crate) mod image;
pub(crate) mod progress;
pub(crate) mod repo;
pub(crate) mod rollback;
Expand Down
79 changes: 61 additions & 18 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,19 +397,24 @@ pub(crate) struct InstallConfigOpts {

#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct InstallComposefsOpts {
/// If true, composefs backend is used, else ostree backend is used
/// If true, composefs backend is used, else ostree backend is used.
///
/// Images with a UKI always use it, and so do images that ship
/// /usr/lib/composefs/setup-root-conf.toml but no ostree prepare-root.conf.
#[clap(long, default_value_t)]
#[serde(default)]
pub(crate) composefs_backend: bool,

/// Make fs-verity validation optional in case the filesystem doesn't support it
#[clap(long, default_value_t, requires = "composefs_backend")]
/// (composefs backend only)
#[clap(long, default_value_t)]
#[serde(default)]
pub(crate) allow_missing_verity: bool,

/// Name of the UKI addons to install without the ".efi.addon" suffix.
/// This option can be provided multiple times if multiple addons are to be installed.
#[clap(long, requires = "composefs_backend")]
/// This option can be provided multiple times if multiple addons are to be installed
/// (composefs backend only).
#[clap(long)]
#[serde(default)]
pub(crate) uki_addon: Option<Vec<String>>,
}
Expand Down Expand Up @@ -1720,13 +1725,54 @@ async fn prepare_install(

tracing::debug!("Composefs required: {composefs_required}");

if composefs_required {
composefs_options.composefs_backend = true;
// ostree's prepare-root.conf is read from the running root even with
// --source-imgref, like the install configuration: tools such as
// bootc-image-builder run bootc from the image they install. Convert the
// keyfile to a hashmap because GKeyFile isnt Send for probably bad reasons.
let prepareroot_config = ostree_prepareroot::load_config_from_root(&rootfs)?
.map(|kf| -> Result<HashMap<String, String>> {
let mut r = HashMap::new();
for grp in kf.groups() {
for key in kf.keys(&grp)? {
let key = key.as_str();
let value = kf.value(&grp, key)?;
r.insert(format!("{grp}.{key}"), value.to_string());
}
}
Ok(r)
})
.transpose()?;

// A UKI requires the composefs backend, and a composefs-native image
// without ostree's configuration defaults to it.
let composefs_default = crate::bootc_composefs::image::defaults_to_composefs_backend(
&rootfs,
prepareroot_config.is_some(),
)?;
tracing::debug!("Composefs default: {composefs_default}");
let composefs_explicit = composefs_options.composefs_backend;
composefs_options.composefs_backend |= composefs_required || composefs_default;
if !composefs_options.composefs_backend {
anyhow::ensure!(
!composefs_options.allow_missing_verity,
"--allow-missing-verity requires the composefs backend"
);
anyhow::ensure!(
composefs_options.uki_addon.is_none(),
"--uki-addon requires the composefs backend"
);
}

if composefs_options.composefs_backend
&& matches!(config_opts.bootloader, Some(Bootloader::None))
{
if !composefs_explicit && !composefs_required {
anyhow::bail!(
"Bootloader set to none is not supported with the composefs backend, which this image selects \
because it has {} and no ostree prepare-root.conf",
bootc_initramfs_setup::SETUP_ROOT_CONF_PATH
);
}
anyhow::bail!("Bootloader set to none is not supported with the composefs backend");
}

Expand Down Expand Up @@ -1831,18 +1877,15 @@ async fn prepare_install(
}
}

// Convert the keyfile to a hashmap because GKeyFile isnt Send for probably bad reasons.
let prepareroot_config = {
let kf = ostree_prepareroot::require_config_from_root(&rootfs)?;
let mut r = HashMap::new();
for grp in kf.groups() {
for key in kf.keys(&grp)? {
let key = key.as_str();
let value = kf.value(&grp, key)?;
r.insert(format!("{grp}.{key}"), value.to_string());
}
}
r
// Only the ostree backend uses prepare-root.conf, and composefs-native
// images needn't have one.
let prepareroot_config = match prepareroot_config {
Some(c) => c,
None if composefs_options.composefs_backend => HashMap::new(),
None => anyhow::bail!(
"Failed to find {} in /usr/lib or /etc",
ostree_prepareroot::CONF_PATH
),
};

// Eagerly read the file now to ensure we error out early if e.g. it doesn't exist,
Expand Down
39 changes: 37 additions & 2 deletions crates/lib/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use ostree_ext::ostree_prepareroot;
use serde::Serialize;

use crate::bootc_composefs::boot::EFI_LINUX;
use crate::bootc_composefs::image::is_composefs_native;

/// Create a default WalkConfiguration with noxdev enabled.
///
Expand Down Expand Up @@ -573,12 +574,17 @@ fn check_api_dirs(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
static LINT_COMPOSEFS: Lint = Lint::new_warning(
"baseimage-composefs",
indoc! { r#"
Check that composefs is enabled for ostree. More in
Check that composefs is enabled for ostree. Skipped for composefs-native
images, i.e. those that ship /usr/lib/composefs/setup-root-conf.toml. More in
<https://ostreedev.github.io/ostree/composefs/>.
"#},
check_composefs,
);
fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult {
// ostree's prepare-root.conf is irrelevant for composefs-native images.
if is_composefs_native(dir)? {
return lint_ok();
}
if let Err(e) = check_prepareroot_composefs_norecurse(dir)? {
return Ok(Err(e));
}
Expand All @@ -604,6 +610,10 @@ fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> L

// Check /ostree -> sysroot/ostree
let Some(meta) = dir.symlink_metadata_optional("ostree")? else {
// Composefs-native images don't use ostree, so they don't need it.
if is_composefs_native(dir)? {
return lint_ok();
}
return lint_err("Missing ostree -> sysroot/ostree link");
};
if !meta.is_symlink() {
Expand All @@ -624,7 +634,9 @@ static LINT_BASEIMAGE_ROOT: Lint = Lint::new_fatal(
"baseimage-root",
indoc! { r#"
Check that expected files are present in the root of the filesystem; such
as /sysroot and a composefs configuration for ostree. More in
as /sysroot and a composefs configuration for ostree. The /ostree symlink
is not required for composefs-native images, i.e. those that ship
/usr/lib/composefs/setup-root-conf.toml. More in
<https://bootc.dev/bootc/bootc-images.html#standard-image-content>.
"#},
check_baseimage_root,
Expand Down Expand Up @@ -949,6 +961,7 @@ mod tests {
use std::sync::LazyLock;

use super::*;
use crate::bootc_composefs::image::setup_root_conf_path;

static ALTROOT_LINTS: LazyLock<usize> = LazyLock::new(|| {
LINTS
Expand Down Expand Up @@ -1365,6 +1378,21 @@ mod tests {
drop(td);
let td = passing_fixture()?;
check_baseimage_root(&td, config).unwrap().unwrap();

// Composefs-native images don't need /ostree...
td.remove_file("ostree")?;
assert!(check_baseimage_root(&td, config).unwrap().is_err());
let conf = Utf8Path::new(setup_root_conf_path());
td.create_dir_all(conf.parent().unwrap())?;
td.write(conf, "")?;
check_baseimage_root(&td, config).unwrap().unwrap();
// ...but if they have it, it must still be correct
td.create_dir("ostree")?;
assert!(check_baseimage_root(&td, config).unwrap().is_err());
td.remove_dir("ostree")?;
// ...and they still need /sysroot
td.remove_dir("sysroot")?;
assert!(check_baseimage_root(&td, config).unwrap().is_err());
Ok(())
}

Expand All @@ -1388,6 +1416,13 @@ mod tests {
// Now it should fail because composefs is explicitly disabled.
assert!(check_composefs(&td, config).unwrap().is_err());

// ...unless the image is composefs-native, where ostree's
// configuration doesn't matter.
let conf = Utf8Path::new(setup_root_conf_path());
td.create_dir_all(conf.parent().unwrap())?;
td.write(conf, "")?;
check_composefs(&td, config).unwrap().unwrap();

Ok(())
}

Expand Down
21 changes: 10 additions & 11 deletions crates/tests-integration/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,11 @@ pub(crate) fn test_bootc_upgrade() -> Result<()> {
Ok(())
}

/// This is a single test because it adds a drop-in to /run/bootc/install,
/// which would race with any other test loading the install configuration:
/// libtest runs tests in parallel, and a file that vanishes between the
/// directory scan and the read fails `print-configuration`.
pub(crate) fn test_bootc_install_config() -> Result<()> {
let sh = &xshell::Shell::new()?;
let config = cmd!(sh, "bootc install print-configuration").read()?;
let config: serde_json::Value =
serde_json::from_str(&config).context("Parsing install config")?;
// check that it parses okay, but also ensure kargs is not available here (only via --all)
assert!(config.get("kargs").is_none());
Ok(())
}

pub(crate) fn test_bootc_install_config_all() -> Result<()> {
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct TestOstreeConfig {
Expand Down Expand Up @@ -152,6 +146,12 @@ pub(crate) fn test_bootc_install_config_all() -> Result<()> {
}

let sh = &xshell::Shell::new()?;
let config = cmd!(sh, "bootc install print-configuration").read()?;
let config: serde_json::Value =
serde_json::from_str(&config).context("Parsing install config")?;
// check that it parses okay, but also ensure kargs is not available here (only via --all)
assert!(config.get("kargs").is_none());

let config = cmd!(sh, "bootc install print-configuration --all").read()?;
let config: TestInstallConfig =
serde_json::from_str(&config).context("Parsing install config")?;
Expand Down Expand Up @@ -531,7 +531,6 @@ pub(crate) fn run(testargs: libtest_mimic::Arguments) -> Result<()> {
new_test("variant-base-crosscheck", test_variant_base_crosscheck),
new_test("bootc upgrade", test_bootc_upgrade),
new_test("install config", test_bootc_install_config),
new_test("printconfig --all", test_bootc_install_config_all),
new_test("status", test_bootc_status),
new_test("container inspect", test_bootc_container_inspect),
new_test("system-reinstall --help", test_system_reinstall_help),
Expand Down
6 changes: 5 additions & 1 deletion docs/src/bootc-compatible-images.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ For the composefs backend, the UKI must be located at `/boot/EFI/Linux/$kver.efi

### /ostree symlink and `bootc container lint`

This is [a bug](https://github.com/bootc-dev/bootc/issues/2256): currently a `/ostree -> /sysroot/ostree` symlink is required just for `bootc container lint` to pass, even though it's not required for `/sysroot/ostree` to exist.
`bootc container lint` requires a `/ostree -> sysroot/ostree` symlink,
unless the image is composefs-native, which is signaled by the presence of
`/usr/lib/composefs/setup-root-conf.toml` (it may be empty); see
[bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md). Such
images also don't need ostree's `prepare-root.conf` to enable composefs.

## composefs backend

Expand Down
7 changes: 7 additions & 0 deletions docs/src/bootc-experimental-composefs.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,13 @@ Composefs installs using a traditional `vmlinuz`/`initramfs.img` layout instead

There is a `--composefs-backend` option for `bootc install` to explicitly select a composefs backend apart from sealed images; this is not as heavily tested yet.

An image built only for the composefs backend selects it by itself: if it ships
`/usr/lib/composefs/setup-root-conf.toml` (see [bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md);
it may be empty) and no ostree `prepare-root.conf` (in `/usr/lib/ostree` or `/etc/ostree`),
`bootc install` uses composefs without the flag. Like the install configuration, these files are
read from the root bootc runs in, also with `--source-imgref`, so this applies to tools like
bootc-image-builder that run bootc from the image they install. An image that ships both is installed with ostree unless the flag is passed.

## Known issues

The composefs backend is experimental; on-disk formats are subject to change.
Expand Down
4 changes: 2 additions & 2 deletions docs/src/man/bootc-install-to-disk.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,13 @@ set `discoverable-partitions = true` in their install configuration

**--allow-missing-verity**

Make fs-verity validation optional in case the filesystem doesn't support it
Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only)

Default: false

**--uki-addon**=*UKI_ADDON*

Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed
Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only)

<!-- END GENERATED OPTIONS -->

Expand Down
4 changes: 2 additions & 2 deletions docs/src/man/bootc-install-to-existing-root.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,13 @@ of migrating the fstab entries. See the "Injecting kernel arguments" section abo

**--allow-missing-verity**

Make fs-verity validation optional in case the filesystem doesn't support it
Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only)

Default: false

**--uki-addon**=*UKI_ADDON*

Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed
Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only)

<!-- END GENERATED OPTIONS -->

Expand Down
Loading