Skip to content

Commit 77685f0

Browse files
committed
Various composefs enhancements
- Change the install logic to detect UKIs and automatically enable composefs - Change the install logic to detect absence of bootupd and default to installing systemd-boot - Move sealing bits to the toplevel - Add Justfile entrypoints - Add basic end-to-end CI coverage (install + run) using our integration tests - Change lints to ignore `/boot/EFI` Signed-off-by: Colin Walters <[email protected]>
1 parent e1fb77b commit 77685f0

File tree

21 files changed

+600
-58
lines changed

21 files changed

+600
-58
lines changed

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,24 @@ jobs:
192192
with:
193193
name: tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ env.ARCH }}-${{ matrix.tmt_plan }}
194194
path: /var/tmp/tmt
195+
# This variant does composefs testing
196+
test-integration-cfs:
197+
strategy:
198+
fail-fast: false
199+
matrix:
200+
test_os: [centos-10]
201+
202+
runs-on: ubuntu-24.04
203+
204+
steps:
205+
- uses: actions/checkout@v4
206+
- name: Bootc Ubuntu Setup
207+
uses: ./.github/actions/bootc-ubuntu-setup
208+
with:
209+
libvirt: true
210+
211+
- name: Build container
212+
run: just build-sealed
213+
214+
- name: Test
215+
run: just test-composefs

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,21 @@ RUN --mount=type=cache,target=/build/target --mount=type=cache,target=/var/rooth
9191

9292
# The final image that derives from the original base and adds the release binaries
9393
FROM base
94+
# Set this to 1 to default to systemd-boot
95+
ARG sdboot=0
9496
RUN <<EORUN
9597
set -xeuo pipefail
9698
# Ensure we've flushed out prior state (i.e. files no longer shipped from the old version);
9799
# and yes, we may need to go to building an RPM in this Dockerfile by default.
98100
rm -vf /usr/lib/systemd/system/multi-user.target.wants/bootc-*
101+
if test "$sdboot" = 1; then
102+
dnf -y install systemd-boot-unsigned
103+
# And uninstall bootupd
104+
rpm -e bootupd
105+
rm /usr/lib/bootupd/updates -rf
106+
dnf clean all
107+
rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/*
108+
fi
99109
EORUN
100110
# Create a layer that is our new binaries
101111
COPY --from=build /out/ /

Dockerfile.cfsuki

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Override via --build-arg=base=<image> to use a different base
2+
ARG base=localhost/bootc
3+
# This is where we get the tools to build the UKI
4+
ARG buildroot=quay.io/fedora/fedora:42
5+
FROM $base AS base
6+
7+
FROM $buildroot as buildroot-base
8+
RUN <<EORUN
9+
set -xeuo pipefail
10+
dnf install -y systemd-ukify sbsigntools systemd-boot-unsigned
11+
dnf clean all
12+
EORUN
13+
14+
FROM buildroot-base as kernel
15+
# Must be passed
16+
ARG COMPOSEFS_FSVERITY
17+
RUN --mount=type=secret,id=key \
18+
--mount=type=secret,id=cert \
19+
--mount=type=bind,from=base,target=/target \
20+
<<EOF
21+
set -eux
22+
23+
# Should be generated externally
24+
test -n "${COMPOSEFS_FSVERITY}"
25+
26+
# Inject the composefs kernel argument and specify a root with the x86_64 DPS UUID.
27+
# TODO: Discoverable partition fleshed out, or drop root UUID as systemd-stub extension
28+
# TODO: https://github.com/containers/composefs-rs/issues/183
29+
cmdline="composefs=${COMPOSEFS_FSVERITY} root=UUID=4f68bce3-e8cd-4db1-96e7-fbcaf984b709 console=ttyS0,114800n8 enforcing=0 rw"
30+
31+
kver=$(cd /target/usr/lib/modules && echo *)
32+
ukify build \
33+
--linux "/target/usr/lib/modules/$kver/vmlinuz" \
34+
--initrd "/target/usr/lib/modules/$kver/initramfs.img" \
35+
--uname="${kver}" \
36+
--cmdline "${cmdline}" \
37+
--os-release "@/target/usr/lib/os-release" \
38+
--signtool sbsign \
39+
--secureboot-private-key "/run/secrets/key" \
40+
--secureboot-certificate "/run/secrets/cert" \
41+
--measure \
42+
--json pretty \
43+
--output "/boot/$kver.efi"
44+
# Sign systemd-boot as well
45+
sdboot="/usr/lib/systemd/boot/efi/systemd-bootx64.efi"
46+
sbsign \
47+
--key "/run/secrets/key" \
48+
--cert "/run/secrets/cert" \
49+
"${sdboot}"
50+
mv "${sdboot}.signed" "${sdboot}"
51+
EOF
52+
53+
FROM base as final
54+
55+
RUN --mount=type=bind,from=kernel,target=/run/kernel <<EOF
56+
set -xeuo pipefail
57+
kver=$(cd /usr/lib/modules && echo *)
58+
mkdir -p /boot/EFI/Linux
59+
# We put the UKI in /boot for now due to composefs verity not being the
60+
# same due to mtime of /usr/lib/modules being changed
61+
target=/boot/EFI/Linux/$kver.efi
62+
cp /run/kernel/boot/$kver.efi $target
63+
# And remove the defaults
64+
rm -v /usr/lib/modules/${kver}/{vmlinuz,initramfs.img}
65+
# Symlink into the /usr/lib/modules location
66+
ln -sr $target /usr/lib/modules/${kver}/$(basename $kver.efi)
67+
bootc container lint --fatal-warnings
68+
EOF
69+
70+
FROM base as final-final
71+
COPY --from=final /boot /boot
72+
# Override the default
73+
LABEL containers.bootc=sealed

Justfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@
1313
build *ARGS:
1414
podman build --jobs=4 -t localhost/bootc {{ARGS}} .
1515

16+
# Build a sealed image from current sources. This will default to
17+
# generating Secure Boot keys in target/test-secureboot.
18+
build-sealed *ARGS:
19+
podman build --build-arg=sdboot=1 --jobs=4 -t localhost/bootc-unsealed {{ARGS}} .
20+
./tests/build-sealed localhost/bootc-unsealed localhost/bootc
21+
1622
# This container image has additional testing content and utilities
1723
build-integration-test-image *ARGS:
1824
cd hack && podman build --jobs=4 -t localhost/bootc-integration -f Containerfile {{ARGS}} .
1925
# Keep these in sync with what's used in hack/lbi
2026
podman pull -q --retry 5 --retry-delay 5s quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.access.redhat.com/ubi9/podman:latest
2127

28+
test-composefs: build-sealed
29+
cargo run --release -p tests-integration -- composefs-bcvk localhost/bootc
30+
2231
# Only used by ci.yml right now
2332
build-install-test-image: build-integration-test-image
2433
cd hack && podman build -t localhost/bootc-integration-install -f Containerfile.drop-lbis

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,23 @@ use rustix::path::Arg;
2929
use schemars::JsonSchema;
3030
use serde::{Deserialize, Serialize};
3131

32-
use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
3332
use crate::bootc_composefs::status::get_sorted_uki_boot_entries;
3433
use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
3534
use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
3635
use crate::parsers::grub_menuconfig::MenuEntry;
3736
use crate::spec::ImageReference;
3837
use crate::task::Task;
3938
use crate::{bootc_composefs::repo::open_composefs_repo, store::ComposefsFilesystem};
39+
use crate::{
40+
bootc_composefs::state::{get_booted_bls, write_composefs_state},
41+
bootloader::esp_in,
42+
};
4043
use crate::{
4144
composefs_consts::{
4245
BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST,
4346
STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
4447
},
45-
install::{dps_uuid::DPS_UUID, ESP_GUID, RW_KARG},
48+
install::{dps_uuid::DPS_UUID, RW_KARG},
4649
spec::{Bootloader, Host},
4750
};
4851

@@ -51,7 +54,7 @@ use crate::install::{RootSetup, State};
5154
/// Contains the EFP's filesystem UUID. Used by grub
5255
pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
5356
/// The EFI Linux directory
54-
const EFI_LINUX: &str = "EFI/Linux";
57+
pub(crate) const EFI_LINUX: &str = "EFI/Linux";
5558

5659
/// Timeout for systemd-boot bootloader menu
5760
const SYSTEMD_TIMEOUT: &str = "timeout 5";
@@ -126,15 +129,31 @@ fi
126129
)
127130
}
128131

132+
/// Returns `true` if detect the target rootfs carries a UKI.
133+
pub(crate) fn container_root_has_uki(root: &Dir) -> Result<bool> {
134+
let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
135+
return Ok(false);
136+
};
137+
let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
138+
return Ok(false);
139+
};
140+
for entry in efi_linux.entries()? {
141+
let entry = entry?;
142+
let name = entry.file_name();
143+
let name = Path::new(&name);
144+
let extension = name.extension().and_then(|v| v.to_str());
145+
if extension == Some("efi") {
146+
return Ok(true);
147+
}
148+
}
149+
Ok(false)
150+
}
151+
129152
pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
130153
let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
131-
let esp = device_info
132-
.partitions
133-
.into_iter()
134-
.find(|p| p.parttype.as_str() == ESP_GUID)
135-
.ok_or(anyhow::anyhow!("ESP not found for device: {device}"))?;
154+
let esp = crate::bootloader::esp_in(&device_info)?;
136155

137-
Ok((esp.node, esp.uuid))
156+
Ok((esp.node.clone(), esp.uuid.clone()))
138157
}
139158

140159
pub fn get_sysroot_parent_dev() -> Result<String> {
@@ -360,23 +379,14 @@ pub(crate) fn setup_composefs_bls_boot(
360379
};
361380

362381
// Locate ESP partition device
363-
let esp_part = root_setup
364-
.device_info
365-
.partitions
366-
.iter()
367-
.find(|p| p.parttype.as_str() == ESP_GUID)
368-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
382+
let esp_part = esp_in(&root_setup.device_info)?;
369383

370384
(
371385
root_setup.physical_root_path.clone(),
372386
esp_part.node.clone(),
373387
cmdline_options,
374388
fs,
375-
state
376-
.composefs_options
377-
.as_ref()
378-
.map(|opts| opts.bootloader.clone())
379-
.unwrap_or(Bootloader::default()),
389+
state.detected_bootloader.clone(),
380390
)
381391
}
382392

@@ -829,17 +839,12 @@ pub(crate) fn setup_composefs_uki_boot(
829839
anyhow::bail!("ComposeFS options not found");
830840
};
831841

832-
let esp_part = root_setup
833-
.device_info
834-
.partitions
835-
.iter()
836-
.find(|p| p.parttype.as_str() == ESP_GUID)
837-
.ok_or_else(|| anyhow!("ESP partition not found"))?;
842+
let esp_part = esp_in(&root_setup.device_info)?;
838843

839844
(
840845
root_setup.physical_root_path.clone(),
841846
esp_part.node.clone(),
842-
cfs_opts.bootloader.clone(),
847+
state.detected_bootloader.clone(),
843848
cfs_opts.insecure,
844849
cfs_opts.uki_addon.as_ref(),
845850
)
@@ -944,13 +949,20 @@ pub(crate) fn setup_composefs_boot(
944949
if cfg!(target_arch = "s390x") {
945950
// TODO: Integrate s390x support into install_via_bootupd
946951
crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
947-
} else {
952+
} else if state.detected_bootloader == Bootloader::Grub {
948953
crate::bootloader::install_via_bootupd(
949954
&root_setup.device_info,
950955
&root_setup.physical_root_path,
951956
&state.config_opts,
952957
None,
953958
)?;
959+
} else {
960+
crate::bootloader::install_systemd_boot(
961+
&root_setup.device_info,
962+
&root_setup.physical_root_path,
963+
&state.config_opts,
964+
None,
965+
)?;
954966
}
955967

956968
let repo = open_composefs_repo(&root_setup.physical_root)?;
@@ -1001,3 +1013,34 @@ pub(crate) fn setup_composefs_boot(
10011013

10021014
Ok(())
10031015
}
1016+
1017+
#[cfg(test)]
1018+
mod tests {
1019+
use super::*;
1020+
use cap_std_ext::cap_std;
1021+
1022+
#[test]
1023+
fn test_root_has_uki() -> Result<()> {
1024+
// Test case 1: No boot directory
1025+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1026+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1027+
1028+
// Test case 2: boot directory exists but no EFI/Linux
1029+
tempdir.create_dir(crate::install::BOOT)?;
1030+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1031+
1032+
// Test case 3: boot/EFI/Linux exists but no .efi files
1033+
tempdir.create_dir_all("boot/EFI/Linux")?;
1034+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1035+
1036+
// Test case 4: boot/EFI/Linux exists with non-.efi file
1037+
tempdir.atomic_write("boot/EFI/Linux/readme.txt", b"some file")?;
1038+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1039+
1040+
// Test case 5: boot/EFI/Linux exists with .efi file
1041+
tempdir.atomic_write("boot/EFI/Linux/bootx64.efi", b"fake efi binary")?;
1042+
assert_eq!(container_root_has_uki(&tempdir)?, true);
1043+
1044+
Ok(())
1045+
}
1046+
}

0 commit comments

Comments
 (0)