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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
Expand Down Expand Up @@ -336,6 +344,12 @@ if [[ "${bootloader}" == "systemd" ]]; then
/run/packaging/switch-to-sdboot /run/sdboot-signed
fi

# Composefs test images are installed without --composefs-backend (see
# target-base), and so without --bootloader too, which bcvk only takes with it.
if [[ "${variant}" == composefs* ]]; then
printf '[install]\nbootloader = "%s"\n' "${bootloader}" > /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
Expand Down
7 changes: 3 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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}} \
Expand All @@ -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}} \
Expand Down Expand Up @@ -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}} \
Expand Down
18 changes: 9 additions & 9 deletions contrib/packaging/inject-baseconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
122 changes: 95 additions & 27 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<String>>,
}

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 {
Expand Down Expand Up @@ -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<String, String>,
pub(crate) ostree_prepareroot_config: HashMap<String, String>,
pub(crate) install_config: Option<config::InstallConfiguration>,
/// The parsed contents of the authorized_keys (not the file path)
pub(crate) root_ssh_authorized_keys: Option<String>,
Expand Down Expand Up @@ -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()?
Expand Down Expand Up @@ -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<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()?;

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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading