Skip to content

Commit 1c4b1d9

Browse files
committed
Various composefs enhancements
- Change the install logic to detect UKIs and automatically enable composefs - 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 <walters@verbum.org>
1 parent e1fb77b commit 1c4b1d9

File tree

21 files changed

+564
-29
lines changed

21 files changed

+564
-29
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: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use crate::install::{RootSetup, State};
5151
/// Contains the EFP's filesystem UUID. Used by grub
5252
pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
5353
/// The EFI Linux directory
54-
const EFI_LINUX: &str = "EFI/Linux";
54+
pub(crate) const EFI_LINUX: &str = "EFI/Linux";
5555

5656
/// Timeout for systemd-boot bootloader menu
5757
const SYSTEMD_TIMEOUT: &str = "timeout 5";
@@ -126,6 +126,26 @@ fi
126126
)
127127
}
128128

129+
/// Returns `true` if detect the target rootfs carries a UKI.
130+
pub(crate) fn container_root_has_uki(root: &Dir) -> Result<bool> {
131+
let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
132+
return Ok(false);
133+
};
134+
let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
135+
return Ok(false);
136+
};
137+
for entry in efi_linux.entries()? {
138+
let entry = entry?;
139+
let name = entry.file_name();
140+
let name = Path::new(&name);
141+
let extension = name.extension().and_then(|v| v.to_str());
142+
if extension == Some("efi") {
143+
return Ok(true);
144+
}
145+
}
146+
Ok(false)
147+
}
148+
129149
pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
130150
let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
131151
let esp = device_info
@@ -372,11 +392,7 @@ pub(crate) fn setup_composefs_bls_boot(
372392
esp_part.node.clone(),
373393
cmdline_options,
374394
fs,
375-
state
376-
.composefs_options
377-
.as_ref()
378-
.map(|opts| opts.bootloader.clone())
379-
.unwrap_or(Bootloader::default()),
395+
state.detected_bootloader.clone(),
380396
)
381397
}
382398

@@ -839,7 +855,7 @@ pub(crate) fn setup_composefs_uki_boot(
839855
(
840856
root_setup.physical_root_path.clone(),
841857
esp_part.node.clone(),
842-
cfs_opts.bootloader.clone(),
858+
state.detected_bootloader.clone(),
843859
cfs_opts.insecure,
844860
cfs_opts.uki_addon.as_ref(),
845861
)
@@ -944,13 +960,20 @@ pub(crate) fn setup_composefs_boot(
944960
if cfg!(target_arch = "s390x") {
945961
// TODO: Integrate s390x support into install_via_bootupd
946962
crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
947-
} else {
963+
} else if state.detected_bootloader == Bootloader::Grub {
948964
crate::bootloader::install_via_bootupd(
949965
&root_setup.device_info,
950966
&root_setup.physical_root_path,
951967
&state.config_opts,
952968
None,
953969
)?;
970+
} else {
971+
crate::bootloader::install_systemd_boot(
972+
&root_setup.device_info,
973+
&root_setup.physical_root_path,
974+
&state.config_opts,
975+
None,
976+
)?;
954977
}
955978

956979
let repo = open_composefs_repo(&root_setup.physical_root)?;
@@ -1001,3 +1024,34 @@ pub(crate) fn setup_composefs_boot(
10011024

10021025
Ok(())
10031026
}
1027+
1028+
#[cfg(test)]
1029+
mod tests {
1030+
use super::*;
1031+
use cap_std_ext::cap_std;
1032+
1033+
#[test]
1034+
fn test_root_has_uki() -> Result<()> {
1035+
// Test case 1: No boot directory
1036+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1037+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1038+
1039+
// Test case 2: boot directory exists but no EFI/Linux
1040+
tempdir.create_dir(crate::install::BOOT)?;
1041+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1042+
1043+
// Test case 3: boot/EFI/Linux exists but no .efi files
1044+
tempdir.create_dir_all("boot/EFI/Linux")?;
1045+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1046+
1047+
// Test case 4: boot/EFI/Linux exists with non-.efi file
1048+
tempdir.atomic_write("boot/EFI/Linux/readme.txt", b"some file")?;
1049+
assert_eq!(container_root_has_uki(&tempdir)?, false);
1050+
1051+
// Test case 5: boot/EFI/Linux exists with .efi file
1052+
tempdir.atomic_write("boot/EFI/Linux/bootx64.efi", b"fake efi binary")?;
1053+
assert_eq!(container_root_has_uki(&tempdir)?, true);
1054+
1055+
Ok(())
1056+
}
1057+
}

crates/lib/src/bootloader.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,26 @@ use fn_error_context::context;
77

88
use bootc_blockdev::PartitionTable;
99
use bootc_mount as mount;
10+
use bootc_mount::tempmount::TempMount;
11+
12+
use crate::{install::ESP_GUID, utils};
1013

1114
/// The name of the mountpoint for efi (as a subdirectory of /boot, or at the toplevel)
1215
pub(crate) const EFI_DIR: &str = "efi";
1316

17+
/// Determine if the invoking environment contains bootupd, and if there are bootupd-based
18+
/// updates in the target root.
19+
#[context("Querying for bootupd")]
20+
pub(crate) fn supports_bootupd(deployment_path: Option<&str>) -> Result<bool> {
21+
if !utils::have_executable("bootupd")? {
22+
return Ok(false);
23+
};
24+
let deployment_path = Utf8Path::new(deployment_path.unwrap_or("/"));
25+
let updates = deployment_path.join("usr/lib/bootupd/updates");
26+
let r = updates.try_exists()?;
27+
Ok(r)
28+
}
29+
1430
#[context("Installing bootloader")]
1531
pub(crate) fn install_via_bootupd(
1632
device: &PartitionTable,
@@ -40,6 +56,30 @@ pub(crate) fn install_via_bootupd(
4056
.run_inherited_with_cmd_context()
4157
}
4258

59+
#[context("Installing bootloader")]
60+
pub(crate) fn install_systemd_boot(
61+
device: &PartitionTable,
62+
_rootfs: &Utf8Path,
63+
_configopts: &crate::install::InstallConfigOpts,
64+
_deployment_path: Option<&str>,
65+
) -> Result<()> {
66+
let esp_part = device
67+
.partitions
68+
.iter()
69+
.find(|p| p.parttype.as_str() == ESP_GUID)
70+
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
71+
72+
let esp_mount = TempMount::mount_dev(&esp_part.node).context("Mounting ESP")?;
73+
let esp_path = Utf8Path::from_path(esp_mount.dir.path())
74+
.ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
75+
76+
println!("Installing bootloader via systemd-boot");
77+
Command::new("bootctl")
78+
.args(["install", "--esp-path", esp_path.as_str()])
79+
.log_debug()
80+
.run_inherited_with_cmd_context()
81+
}
82+
4383
#[context("Installing bootloader using zipl")]
4484
pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> {
4585
// Identify the target boot partition from UUID

0 commit comments

Comments
 (0)