diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94601538e..256f1a992 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -552,7 +552,7 @@ jobs: cargo xtask run-tmt \ --env=BOOTC_variant=composefs \ --env=BOOTC_baseconfigs=${{ matrix.baseconfigs }} \ - --composefs-backend --bootloader=grub --filesystem=ext4 \ + --filesystem=ext4 \ --seal-state=unsealed --boot-type=bls \ --upgrade-image=localhost/bootc-upgrade \ localhost/bootc readonly diff --git a/Dockerfile b/Dockerfile index c6b825dba..a36bc5638 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,6 +89,14 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --install system-reinstall-bootc \ --add-dir /var/add-dir/usr \ --manifest=standard /target-rootfs + + # Composefs test images signal the backend the way composefs-native images + # are meant to (see bootc-installation(7)): inject-baseconfig ships + # setup-root-conf.toml, and there must be no ostree prepare-root.conf, so + # that `bootc install` picks composefs without --composefs-backend. + if [[ "${variant}" == composefs* ]]; then + rm -vf /target-rootfs/usr/lib/ostree/prepare-root.conf /target-rootfs/etc/ostree/prepare-root.conf + fi EOF RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp < /usr/lib/bootc/install/80-composefs-bootloader.toml +fi + if [[ "${boot_type}" == "uki" ]]; then cp /run/packaging/seal-uki /usr/bin/seal-uki cp /run/packaging/finalize-uki /usr/bin/finalize-uki diff --git a/Justfile b/Justfile index 59bd8d64b..afcb3a198 100644 --- a/Justfile +++ b/Justfile @@ -228,8 +228,7 @@ test-composefs bootloader filesystem boot_type seal_state *ARGS: filesystem={{filesystem}} \ boot_type={{boot_type}} \ seal_state={{seal_state}} \ - test-tmt --composefs-backend \ - --bootloader={{bootloader}} \ + test-tmt \ --filesystem={{filesystem}} \ --seal-state={{seal_state}} \ --boot-type={{boot_type}} \ @@ -246,6 +245,8 @@ test-upgrade *ARGS: build _build-upgrade-source-image set -xeuo pipefail composefs_args=() if [[ "{{variant}}" = composefs ]]; then + # Unlike the composefs test images, the published base image doesn't + # select the composefs backend itself, and its bootc may predate that. composefs_args=(--composefs-backend \ --bootloader={{bootloader}} \ --filesystem={{filesystem}} \ @@ -308,8 +309,6 @@ test-tmt-baseconfig baseconfig *ARGS: --env=BOOTC_erofs_version={{erofs_version}} \ --env=BOOTC_baseconfigs={{baseconfig}} \ --upgrade-image={{upgrade_img}} \ - --composefs-backend \ - --bootloader={{bootloader}} \ --filesystem={{filesystem}} \ --boot-type={{boot_type}} \ --seal-state={{seal_state}} \ diff --git a/contrib/packaging/inject-baseconfig b/contrib/packaging/inject-baseconfig index 4d1b2cf6f..ac6c9ca80 100755 --- a/contrib/packaging/inject-baseconfig +++ b/contrib/packaging/inject-baseconfig @@ -10,25 +10,25 @@ BASE_DIR="${1:-/}" VARIANT="${2:-}" BASECONFIGS="${3:-}" -# No-op if no baseconfigs specified -if [ -z "${BASECONFIGS}" ]; then - exit 0 -fi - # setup-root-conf.toml is composefs-specific; ostree uses prepare-root.conf # which has a different (INI) format and different option names. case "${VARIANT}" in composefs*) TARGET="${BASE_DIR}/usr/lib/composefs/setup-root-conf.toml" + # Always ship it, even empty: it marks the image as composefs-native, so + # `bootc install` picks the composefs backend without --composefs-backend. + mkdir -p "$(dirname "${TARGET}")" + touch "${TARGET}" ;; *) - echo "inject-baseconfig: baseconfigs not supported for variant '${VARIANT}'" >&2 - exit 1 + if [ -n "${BASECONFIGS}" ]; then + echo "inject-baseconfig: baseconfigs not supported for variant '${VARIANT}'" >&2 + exit 1 + fi + exit 0 ;; esac -mkdir -p "$(dirname "${TARGET}")" - # Split on commas and process each token IFS=',' read -ra TOKENS <<< "${BASECONFIGS}" for raw_token in "${TOKENS[@]}"; do diff --git a/crates/lib/src/bootc_composefs/image.rs b/crates/lib/src/bootc_composefs/image.rs new file mode 100644 index 000000000..f6691f75e --- /dev/null +++ b/crates/lib/src/bootc_composefs/image.rs @@ -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 { + 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 { + 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(()) + } +} diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index 42d521150..fe5bc9c6a 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -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; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index b2654b867..b0aa39f26 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -397,23 +397,49 @@ 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 + /// Use the composefs backend instead of ostree. This is the default for images with a UKI, + /// and for images with /usr/lib/composefs/setup-root-conf.toml and 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>, } +impl InstallComposefsOpts { + /// Check that the options fit together, once `composefs_backend` says + /// whether the composefs backend is used (passed, or selected by the image). + pub(crate) fn validate(&self, bootloader: Option<&Bootloader>) -> Result<()> { + if self.composefs_backend { + anyhow::ensure!( + !matches!(bootloader, Some(Bootloader::None)), + "Bootloader set to none is not supported with the composefs backend" + ); + } else { + anyhow::ensure!( + !self.allow_missing_verity, + "--allow-missing-verity requires the composefs backend" + ); + anyhow::ensure!( + self.uki_addon.is_none(), + "--uki-addon requires the composefs backend" + ); + } + Ok(()) + } +} + #[cfg(feature = "install-to-disk")] #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallToDiskOpts { @@ -635,7 +661,7 @@ pub(crate) struct State { pub(crate) target_opts: InstallTargetOpts, pub(crate) target_imgref: ostree_container::OstreeImageReference, #[allow(dead_code)] - pub(crate) prepareroot_config: HashMap, + pub(crate) ostree_prepareroot_config: HashMap, pub(crate) install_config: Option, /// The parsed contents of the authorized_keys (not the file path) pub(crate) root_ssh_authorized_keys: Option, @@ -1004,7 +1030,7 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?; let prepare_root_composefs = state - .prepareroot_config + .ostree_prepareroot_config .get("composefs.enabled") .map(|v| ComposefsState::from_str(&v)) .transpose()? @@ -1724,15 +1750,33 @@ 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 ostree_prepareroot_config = ostree_prepareroot::load_config_from_root(&rootfs)? + .map(|kf| -> Result> { + 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()?; - if composefs_options.composefs_backend - && matches!(config_opts.bootloader, Some(Bootloader::None)) - { - anyhow::bail!("Bootloader set to none is not supported with the composefs backend"); - } + // 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, + ostree_prepareroot_config.is_some(), + )?; + tracing::debug!("Composefs default: {composefs_default}"); + composefs_options.composefs_backend |= composefs_required || composefs_default; + composefs_options.validate(config_opts.bootloader.as_ref())?; // Read the file eagerly so we error out early, and before the mount changes // below hide a file bind mounted under e.g. /tmp. We may re-exec further down @@ -1867,18 +1911,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 ostree_prepareroot_config = match ostree_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 + ), }; // Create our global (read-only) state which gets wrapped in an Arc @@ -1891,7 +1932,7 @@ async fn prepare_install( target_opts, target_imgref, install_config, - prepareroot_config, + ostree_prepareroot_config, root_ssh_authorized_keys, container_root: rootfs, tempdir, @@ -3079,6 +3120,33 @@ pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> { mod tests { use super::*; + #[test] + fn test_composefs_opts_validate() { + let addon = || Some(vec!["addon".to_string()]); + // (composefs_backend, allow_missing_verity, uki_addon, bootloader, valid) + let cases = [ + (false, false, None, None, true), + (false, false, None, Some(Bootloader::None), true), + (false, true, None, None, false), + (false, false, addon(), None, false), + (true, false, None, None, true), + (true, true, addon(), Some(Bootloader::Systemd), true), + (true, false, None, Some(Bootloader::None), false), + ]; + for (composefs_backend, allow_missing_verity, uki_addon, bootloader, valid) in cases { + let opts = InstallComposefsOpts { + composefs_backend, + allow_missing_verity, + uki_addon, + }; + assert_eq!( + opts.validate(bootloader.as_ref()).is_ok(), + valid, + "{opts:?} {bootloader:?}" + ); + } + } + #[test] #[cfg(feature = "install-to-disk")] fn install_opts_serializable() { diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs index f3c599d6d..e997f0ca2 100644 --- a/crates/lib/src/lints.rs +++ b/crates/lib/src/lints.rs @@ -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. /// @@ -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 . "#}, 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)); } @@ -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() { @@ -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 . "#}, check_baseimage_root, @@ -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 = LazyLock::new(|| { LINTS @@ -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(()) } @@ -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(()) } diff --git a/crates/xtask/src/bcvk.rs b/crates/xtask/src/bcvk.rs index 520640ebf..03791e604 100644 --- a/crates/xtask/src/bcvk.rs +++ b/crates/xtask/src/bcvk.rs @@ -19,7 +19,13 @@ const DEFAULT_SB_KEYS_DIR: &str = "target/test-secureboot"; /// or populate fields directly. #[derive(Debug, Default)] pub(crate) struct BcvkInstallOpts { + /// Pass `--composefs-backend` (and `--bootloader`, which bcvk only + /// takes with it). Images that select the composefs backend themselves, + /// like bootc's composefs test images, don't need it. pub(crate) composefs_backend: bool, + /// Whether the image is installed with the composefs backend, with or + /// without `composefs_backend`. + pub(crate) composefs: bool, pub(crate) bootloader: Option, pub(crate) filesystem: Option, pub(crate) seal_state: Option, @@ -29,7 +35,8 @@ pub(crate) struct BcvkInstallOpts { impl BcvkInstallOpts { /// Build from `BOOTC_*` environment variables. /// - /// `BOOTC_variant=composefs` implies `composefs_backend = true`. + /// `BOOTC_variant=composefs` implies `composefs_backend = true`, since the + /// dev VM may fall back to a stock base image. pub(crate) fn from_env() -> Self { let composefs_backend = std::env::var("BOOTC_variant") .map(|v| v == "composefs") @@ -55,6 +62,7 @@ impl BcvkInstallOpts { Self { composefs_backend, + composefs: composefs_backend, bootloader, filesystem, seal_state, @@ -65,15 +73,19 @@ impl BcvkInstallOpts { /// Return the install-related args for `bcvk libvirt run`. /// /// This covers `--composefs-backend`, `--filesystem`, `--bootloader`, - /// and `--karg` flags. Note that `--bootloader` and `--filesystem` - /// are only valid when `--composefs-backend` is also set (bcvk - /// enforces this via a clap `requires` relationship). + /// and `--karg` flags. Note that `--bootloader` is only valid when + /// `--composefs-backend` is also set (bcvk enforces this via a clap + /// `requires` relationship). pub(crate) fn install_args(&self) -> Vec { let mut args = Vec::new(); if self.composefs_backend { args.push("--composefs-backend".into()); + } + if self.composefs_backend || self.composefs { let fs = self.filesystem.as_deref().unwrap_or("ext4"); args.push(format!("--filesystem={fs}")); + } + if self.composefs_backend { if let Some(b) = &self.bootloader { args.push(format!("--bootloader={b}")); } @@ -121,3 +133,41 @@ impl BcvkInstallOpts { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_install_args() { + // (composefs_backend, composefs) => expected args + let cases: &[(bool, bool, &[&str])] = &[ + (false, false, &[]), + // A composefs-native image selects the backend itself + (false, true, &["--filesystem=xfs"]), + ( + true, + true, + &[ + "--composefs-backend", + "--filesystem=xfs", + "--bootloader=systemd", + ], + ), + ]; + for &(composefs_backend, composefs, expected) in cases { + let opts = BcvkInstallOpts { + composefs_backend, + composefs, + bootloader: Some(Bootloader::Systemd), + filesystem: Some("xfs".into()), + ..Default::default() + }; + assert_eq!( + opts.install_args(), + expected, + "composefs_backend={composefs_backend} composefs={composefs}" + ); + } + } +} diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index 4836aea22..ed7e7fea5 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -400,6 +400,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { let bcvk_opts = BcvkInstallOpts { composefs_backend: args.composefs_backend, + composefs: args.composefs, bootloader: args.bootloader.clone(), filesystem: args.filesystem.clone(), seal_state: args.seal_state.clone(), @@ -459,7 +460,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { plans.retain(|plan| filter_args.iter().any(|arg| plan.contains(arg.as_str()))); } - if args.composefs_backend { + if args.composefs { plans.retain(|plan| { !plan_metadata .iter() diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index b6ecd5f3c..654983ea1 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -229,8 +229,9 @@ impl Display for SealState { /// Arguments for run-tmt command. /// /// The composefs-related fields can be set via CLI flags or via the standard -/// `BOOTC_*` environment variables used by the Justfile. When `BOOTC_variant` -/// is set to `composefs`, `--composefs-backend` is implied automatically. +/// `BOOTC_*` environment variables used by the Justfile. `BOOTC_variant=composefs` +/// selects the composefs plans, but doesn't pass `--composefs-backend` to bcvk: +/// those test images select the composefs backend themselves. #[derive(Debug, Args)] pub(crate) struct RunTmtArgs { /// Image name (e.g., "localhost/bootc") @@ -278,10 +279,18 @@ pub(crate) struct RunTmtArgs { #[arg(long)] pub(crate) preserve_vm: bool, - /// Use composefs backend. Also implied when BOOTC_variant=composefs. + /// Install with `--composefs-backend` (and `--bootloader`), for images that + /// don't select the composefs backend themselves. #[arg(long)] pub(crate) composefs_backend: bool, + /// Whether the image is installed with the composefs backend: set by + /// `--composefs-backend` or `BOOTC_variant=composefs`. + #[arg(skip)] + pub(crate) composefs: bool, + + /// Only passed to bcvk with `--composefs-backend`; composefs test images + /// configure it themselves. #[arg(long, env = "BOOTC_bootloader")] pub(crate) bootloader: Option, @@ -308,15 +317,13 @@ pub(crate) struct RunTmtArgs { } impl RunTmtArgs { - /// Derive composefs_backend from BOOTC_variant if not explicitly set. + /// Derive `composefs` from `--composefs-backend` and BOOTC_variant, from + /// the environment or passed to the tests with `--env` (as the Justfile does). pub(crate) fn resolve_composefs(&mut self) { - if !self.composefs_backend { - if let Ok(v) = std::env::var("BOOTC_variant") { - if v == "composefs" { - self.composefs_backend = true; - } - } - } + const COMPOSEFS_VARIANT: &str = "BOOTC_variant=composefs"; + self.composefs = self.composefs_backend + || std::env::var("BOOTC_variant").is_ok_and(|v| v == "composefs") + || self.env.iter().any(|e| e == COMPOSEFS_VARIANT); } } diff --git a/docs/src/bootc-compatible-images.7.md b/docs/src/bootc-compatible-images.7.md index b98705df4..64ebf0416 100644 --- a/docs/src/bootc-compatible-images.7.md +++ b/docs/src/bootc-compatible-images.7.md @@ -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 diff --git a/docs/src/bootc-experimental-composefs.7.md b/docs/src/bootc-experimental-composefs.7.md index ee8b22004..d99217cb0 100644 --- a/docs/src/bootc-experimental-composefs.7.md +++ b/docs/src/bootc-experimental-composefs.7.md @@ -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. + ## Known issues The composefs backend is experimental; on-disk formats are subject to change. diff --git a/docs/src/bootc-installation.7.md b/docs/src/bootc-installation.7.md index 90154c533..3a87689c3 100644 --- a/docs/src/bootc-installation.7.md +++ b/docs/src/bootc-installation.7.md @@ -118,6 +118,23 @@ For example, a derived image can supply `50-myos.toml` with See [bootc-install-config](man/bootc-install-config.5.md) for file discovery, merge precedence, and the available configuration fields. +### The storage backend + +The storage backend is determined by the image. It is installed with the +[experimental composefs backend](bootc-experimental-composefs.7.md) when it +ships a UKI, or when it matches both of these rules: + +- it ships `/usr/lib/composefs/setup-root-conf.toml` (which may be empty; see + [bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md)); +- it has no ostree `prepare-root.conf`, in either `/usr/lib/ostree` or `/etc/ostree`. + +Any other image is installed with ostree. `bootc container lint` uses the same +`setup-root-conf.toml` marker to stop requiring the ostree-specific parts of +an image, such as the `/ostree` symlink. + +Like the install configuration, these files are read from the root `bootc` runs +in, also with `--source-imgref`. + ## Installing an "unconfigured" image The bootc project aims to support generic/general-purpose operating diff --git a/docs/src/man/bootc-install-to-disk.8.md b/docs/src/man/bootc-install-to-disk.8.md index 9c1173948..776c01ab1 100644 --- a/docs/src/man/bootc-install-to-disk.8.md +++ b/docs/src/man/bootc-install-to-disk.8.md @@ -176,19 +176,19 @@ set `discoverable-partitions = true` in their install configuration **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--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) diff --git a/docs/src/man/bootc-install-to-existing-root.8.md b/docs/src/man/bootc-install-to-existing-root.8.md index 973c174f8..3ff11166e 100644 --- a/docs/src/man/bootc-install-to-existing-root.8.md +++ b/docs/src/man/bootc-install-to-existing-root.8.md @@ -217,19 +217,19 @@ of migrating the fstab entries. See the "Injecting kernel arguments" section abo **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--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) diff --git a/docs/src/man/bootc-install-to-filesystem.8.md b/docs/src/man/bootc-install-to-filesystem.8.md index 2d2b8b6f6..9549e156b 100644 --- a/docs/src/man/bootc-install-to-filesystem.8.md +++ b/docs/src/man/bootc-install-to-filesystem.8.md @@ -126,19 +126,19 @@ is currently expected to be empty by default. **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--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) diff --git a/docs/src/man/bootc-setup-root-conf.5.md b/docs/src/man/bootc-setup-root-conf.5.md index 3e7082f06..d4c3350c8 100644 --- a/docs/src/man/bootc-setup-root-conf.5.md +++ b/docs/src/man/bootc-setup-root-conf.5.md @@ -15,6 +15,14 @@ mounted. If the file does not exist all options take their documented defaults. +The presence of this file (even if empty) also marks the image as +composefs-native, which decides the storage backend `bootc install` uses: +if the image also has no ostree `prepare-root.conf`, it is installed with +the composefs backend, and otherwise with ostree (see +[bootc-installation(7)](../bootc-installation.7.md)). `bootc container lint` +then also no longer requires the `/ostree` symlink or the ostree +`prepare-root.conf` composefs configuration used by the ostree backend. + The `51bootc` dracut module installs this file into the initramfs automatically when it is present on the host image. Image authors can therefore ship the file at this path in their container image and rebuild the initramfs with a diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index cf08422d9..2042e3cdb 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -355,4 +355,12 @@ execute: - /tmt/tests/tests/test-57-composefs-separate-boot extra-skip_if_ostree: true extra-fixme_skip_if_uki: true + +/plan-60-install-composefs-native: + summary: Test that composefs-native images default to the composefs backend + discover: + how: fmf + test: + - /tmt/tests/tests/test-60-install-composefs-native + extra-fixme_skip_if_uki: true # END GENERATED PLANS diff --git a/tmt/tests/booted/test-install-composefs-native.nu b/tmt/tests/booted/test-install-composefs-native.nu new file mode 100644 index 000000000..59751137b --- /dev/null +++ b/tmt/tests/booted/test-install-composefs-native.nu @@ -0,0 +1,87 @@ +# number: 60 +# tmt: +# summary: Test that composefs-native images default to the composefs backend +# duration: 45m +# extra: +# # A UKI selects the composefs backend by itself, and a derived layer +# # wouldn't match the composefs digest embedded in it. +# fixme_skip_if_uki: true +# +# An image that ships /usr/lib/composefs/setup-root-conf.toml and no ostree +# prepare-root.conf must be installed with the composefs backend by +# `bootc install` run from that image without `--composefs-backend`, both +# as a self-install and with --source-imgref (as bootc-image-builder does). +# An image with both files is still installed with ostree. This runs on the +# ostree variant too, where nothing else selects composefs; on the composefs +# variant, every test's install already relies on this default. +# +# TODO: This doesn't depend on the booted host; move it into a dedicated +# install test suite, sharing the install-in-test code with the other install +# tests: https://github.com/cgwalters-forge/tracker/issues/249 + +use std assert +use tap.nu + +const NATIVE = "localhost/bootc-composefs-native" +const BOTH = "localhost/bootc-composefs-both" +const DISK = "/var/tmp/composefs-native.img" +const MNT = "/var/mnt/composefs-native" + +def build [image: string, extra: string] { + let td = mktemp -d + $"FROM localhost/bootc +RUN rm -rf /usr/lib/bootc/bound-images.d/* +RUN mkdir -p /usr/lib/composefs && touch /usr/lib/composefs/setup-root-conf.toml +($extra) +" | save $"($td)/Containerfile" + # Keep an OCI manifest, see https://github.com/bootc-dev/bootc/issues/1703 + podman build --format oci -t $image $td + rm -rf $td +} + +# Install `image` from itself and return the backend found on the disk +def install [image: string, ...args: string] { + truncate -s 15G $DISK + (podman run --rm --privileged --pid=host + --security-opt label=type:unconfined_t + -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v /var/tmp:/var/tmp + $image + bootc install to-disk --disable-selinux --via-loopback ...$args $DISK) + + # Inspect the root partition of the installed disk + let parts = sfdisk --json $DISK | from json | get partitiontable + let root = $parts.partitions | where name == "root" | first + let offset = $root.start * ($parts.sectorsize? | default 512) + mkdir $MNT + mount -o $"ro,loop,offset=($offset)" $DISK $MNT + let composefs = ($"($MNT)/composefs" | path exists) and ((ls $"($MNT)/state/deploy" | length) == 1) + let ostree = ($"($MNT)/ostree/deploy" | path exists) + umount $MNT + rm -f $DISK + match [$composefs $ostree] { + [true false] => "composefs", + [false true] => "ostree", + _ => $"unexpected: composefs=($composefs) ostree=($ostree)", + } +} + +def main [] { + tap begin "composefs-native images default to the composefs backend" + + bootc image copy-to-storage + build $NATIVE "RUN rm -f /usr/lib/ostree/prepare-root.conf /etc/ostree/prepare-root.conf" + # On the composefs variant, localhost/bootc is itself composefs-native: it + # has no prepare-root.conf, and configures its composefs bootloader. + build $BOTH "RUN printf '[composefs]\\nenabled = yes\\n' > /usr/lib/ostree/prepare-root.conf && rm -f /usr/lib/bootc/install/80-composefs-bootloader.toml" + + assert equal (install $NATIVE) "composefs" "composefs-native self-install" + let src = $"--source-imgref=containers-storage:($NATIVE)" + assert equal (install $NATIVE $src) "composefs" "composefs-native with --source-imgref" + # The ostree backend needs bootupd, which images built for systemd-boot drop + if (which bootupctl | is-not-empty) { + assert equal (install $BOTH) "ostree" "image with both configurations" + } + + podman rmi $NATIVE $BOTH + tap ok +} diff --git a/tmt/tests/booted/test-install-outside-container.nu b/tmt/tests/booted/test-install-outside-container.nu index a228727f3..71ad75c8e 100644 --- a/tmt/tests/booted/test-install-outside-container.nu +++ b/tmt/tests/booted/test-install-outside-container.nu @@ -54,7 +54,7 @@ let base_args = $"bootc install to-disk --disable-selinux --via-loopback --sourc let install_cmd = if (tap is_composefs) { let st = bootc status --json | from json let bootloader = ($st.status.booted.composefs.bootloader | str downcase) - $"($base_args) --composefs-backend --bootloader=($bootloader) --filesystem ext4 ./disk.img" + $"($base_args) --bootloader=($bootloader) --filesystem ext4 ./disk.img" } else { $"($base_args) --filesystem xfs ./disk.img" } diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index c64d70c18..3953f6a9e 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -171,7 +171,6 @@ COMPOSEFS_BACKEND_PARAMS=() KARGS=("--karg=root=UUID=$ROOT_UUID") if [[ $is_composefs != "null" ]]; then - COMPOSEFS_BACKEND_PARAMS+=("--composefs-backend") COMPOSEFS_BACKEND_PARAMS+=("--bootloader" "${bootloader}") tune2fs -O verity /dev/BL/var02 diff --git a/tmt/tests/booted/test-multi-device-esp.nu b/tmt/tests/booted/test-multi-device-esp.nu index 526a54afc..f972b6b35 100644 --- a/tmt/tests/booted/test-multi-device-esp.nu +++ b/tmt/tests/booted/test-multi-device-esp.nu @@ -118,7 +118,7 @@ def setup_disk_with_root [ udevadm settle mkfs.vfat -F 32 $"($loop)p1" - mkfs.ext4 -q $"($loop)p2" + mkfs.ext4 -q -O verity $"($loop)p2" $loop } @@ -206,7 +206,7 @@ def test_single_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -259,7 +259,7 @@ def test_dual_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -314,7 +314,7 @@ def test_three_devices_partial_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -401,7 +401,7 @@ def test_no_esp_failure [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index b4a1af34d..c78d42d7d 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -224,3 +224,8 @@ check: - dosfstools - e2fsprogs test: nu booted/test-composefs-separate-boot.nu + +/test-60-install-composefs-native: + summary: Test that composefs-native images default to the composefs backend + duration: 45m + test: nu booted/test-install-composefs-native.nu