Skip to content

Commit 10b2832

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 <[email protected]>
1 parent de68063 commit 10b2832

File tree

17 files changed

+460
-24
lines changed

17 files changed

+460
-24
lines changed

.github/actions/bootc-ubuntu-setup/action.yml

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ runs:
2525
- name: Free up disk space on runner
2626
shell: bash
2727
run: |
28+
set -xeuo pipefail
2829
sudo df -h
2930
unwanted=('^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*'
3031
azure-cli google-chrome-stable firefox mono-devel)
3132
for x in ${unwanted[@]}; do
32-
sudo apt-get remove -y $x > /dev/null
33+
sudo apt-get remove -y $x
3334
done
3435
# Start other removal operations in parallel
35-
sudo docker image prune --all --force > /dev/null &
36-
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android &
37-
# Wait for all background processes to complete
38-
wait
36+
sudo docker image prune --all --force
37+
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android
3938
sudo df -h
4039
# This is the default on e.g. Fedora derivatives, but not Debian
4140
- name: Enable unprivileged /dev/kvm access
4241
shell: bash
4342
run: |
43+
set -xeuo pipefail
4444
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
4545
sudo udevadm control --reload-rules
4646
sudo udevadm trigger --name-match=kvm
@@ -65,5 +65,17 @@ runs:
6565
if: ${{ inputs.libvirt == 'true' }}
6666
shell: bash
6767
run: |
68-
set -eux
69-
sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm qemu-utils libvirt-daemon-system
68+
set -xeuo pipefail
69+
sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm virtiofsd libvirt-daemon-system
70+
# Something in the stack is overriding this, but we want session right now for bcvk
71+
echo LIBVIRT_DEFAULT_URI=qemu:///session >> $GITHUB_ENV
72+
td=$(mktemp -d)
73+
cd $td
74+
# Install bcvk
75+
curl -LO https://github.com/bootc-dev/bcvk/releases/download/v0.5.1/bcvk-x86_64-unknown-linux-gnu.tar.gz
76+
echo '1c9bb9e2b1e39d64c93b847350dd028832da27e7f7b0296b14ebfc2fb66b5c2c bcvk-x86_64-unknown-linux-gnu.tar.gz' > sums
77+
sha256sum -c sums
78+
tar zxvf bcvk*.tar.gz
79+
sudo install -T bcvk-$(arch)-*linux-gnu /usr/bin/bcvk
80+
cd -
81+
rm -rf "$td"

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ on:
1919

2020
env:
2121
CARGO_TERM_COLOR: always
22+
# Something seems to be setting this in the default GHA runners, which breaks bcvk
23+
# as the default runner user doesn't have access
24+
LIBVIRT_DEFAULT_URI: "qemu:///session"
2225

2326
concurrency:
2427
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -189,3 +192,29 @@ jobs:
189192
with:
190193
name: tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ env.ARCH }}-${{ matrix.tmt_plan }}
191194
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: Setup upterm session
215+
uses: owenthereal/action-upterm@v1
216+
with:
217+
limit-access-to-users: cgwalters
218+
219+
- name: Test
220+
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.cfsuki

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
sbsign \
45+
--key "/run/secrets/key" \
46+
--cert "/run/secrets/cert" \
47+
"/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
48+
--output "/boot/systemd-bootx64.efi"
49+
EOF
50+
51+
FROM base as final
52+
53+
RUN --mount=type=bind,from=kernel,target=/run/kernel <<EOF
54+
kver=$(cd /usr/lib/modules && echo *)
55+
mkdir -p /boot/EFI/Linux
56+
# We put the UKI in /boot for now due to composefs verity not being the
57+
# same due to mtime of /usr/lib/modules being changed
58+
target=/boot/EFI/Linux/$kver.efi
59+
cp /run/kernel/boot/$kver.efi $target
60+
# And remove the defaults
61+
rm -v /usr/lib/modules/${kver}/{vmlinuz,initramfs.img}
62+
# Symlink into the /usr/lib/modules location
63+
ln -sr $target /usr/lib/modules/${kver}/$(basename $kver.efi)
64+
bootc container lint --fatal-warnings
65+
EOF
66+
67+
FROM base as final-final
68+
COPY --from=final /boot /boot
69+
# Override the default
70+
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 --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: 52 additions & 1 deletion
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
@@ -1001,3 +1021,34 @@ pub(crate) fn setup_composefs_boot(
10011021

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

crates/lib/src/cli.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ use std::ffi::{CString, OsStr, OsString};
66
use std::io::Seek;
77
use std::os::unix::process::CommandExt;
88
use std::process::Command;
9+
use std::sync::Arc;
910

1011
use anyhow::{ensure, Context, Result};
11-
use camino::Utf8PathBuf;
12+
use camino::{Utf8Path, Utf8PathBuf};
1213
use cap_std_ext::cap_std;
1314
use cap_std_ext::cap_std::fs::Dir;
1415
use clap::Parser;
1516
use clap::ValueEnum;
17+
use composefs_boot::BootOps as _;
1618
use etc_merge::{compute_diff, print_diff};
1719
use fn_error_context::context;
1820
use indoc::indoc;
@@ -23,11 +25,13 @@ use ostree_ext::composefs::fsverity::FsVerityHashValue;
2325
use ostree_ext::composefs::splitstream::SplitStreamWriter;
2426
use ostree_ext::container as ostree_container;
2527
use ostree_ext::container_utils::ostree_booted;
28+
use ostree_ext::containers_image_proxy::ImageProxyConfig;
2629
use ostree_ext::keyfileext::KeyFileExt;
2730
use ostree_ext::ostree;
2831
use ostree_ext::sysroot::SysrootLock;
2932
use schemars::schema_for;
3033
use serde::{Deserialize, Serialize};
34+
use tempfile::tempdir_in;
3135

3236
#[cfg(feature = "composefs-backend")]
3337
use crate::bootc_composefs::{
@@ -40,9 +44,11 @@ use crate::bootc_composefs::{
4044
};
4145
use crate::deploy::RequiredHostSpec;
4246
use crate::lints;
47+
use crate::podstorage::set_additional_image_store;
4348
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
4449
use crate::spec::Host;
4550
use crate::spec::ImageReference;
51+
use crate::store::ComposefsRepository;
4652
use crate::utils::sigpolicy_from_opt;
4753

4854
/// Shared progress options
@@ -315,6 +321,12 @@ pub(crate) enum ContainerOpts {
315321
#[clap(long)]
316322
no_truncate: bool,
317323
},
324+
/// Output the bootable composefs digest.
325+
#[clap(hide = true)]
326+
ComputeComposefsDigest {
327+
/// Identifier for image; if not provided, the running image will be used.
328+
image: Option<String>,
329+
},
318330
}
319331

320332
/// Subcommands which operate on images.
@@ -1335,6 +1347,55 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13351347
)?;
13361348
Ok(())
13371349
}
1350+
ContainerOpts::ComputeComposefsDigest { image } => {
1351+
// Allocate a tempdir
1352+
let td = tempdir_in("/var/tmp")?;
1353+
let td = td.path();
1354+
let td = &Dir::open_ambient_dir(td, cap_std::ambient_authority())?;
1355+
1356+
td.create_dir("repo")?;
1357+
let repo = td.open_dir("repo")?;
1358+
let mut repo =
1359+
ComposefsRepository::open_path(&repo, ".").context("Init cfs repo")?;
1360+
// We don't need to hard require verity on the *host* system, we're just computing a checksum here
1361+
repo.set_insecure(true);
1362+
let repo = &Arc::new(repo);
1363+
1364+
let mut proxycfg = ImageProxyConfig::default();
1365+
1366+
let image = if let Some(image) = image {
1367+
image
1368+
} else {
1369+
let host_container_store = Utf8Path::new("/run/host-container-storage");
1370+
// If no image is provided, assume that we're running in a container in privileged mode
1371+
// with access to the container storage.
1372+
let container_info = crate::containerenv::get_container_execution_info(&root)?;
1373+
let iid = container_info.imageid;
1374+
tracing::debug!("Computing digest of {iid}");
1375+
1376+
if !host_container_store.try_exists()? {
1377+
anyhow::bail!("Must be readonly mount of host container store: {host_container_store}");
1378+
}
1379+
// And ensure we're finding the image in the host storage
1380+
let mut cmd = Command::new("skopeo");
1381+
set_additional_image_store(&mut cmd, "/run/host-container-storage");
1382+
proxycfg.skopeo_cmd = Some(cmd);
1383+
iid
1384+
};
1385+
1386+
let imgref = format!("containers-storage:{image}");
1387+
let (imgid, verity) = composefs_oci::pull(repo, &imgref, None, Some(proxycfg))
1388+
.await
1389+
.context("Pulling image")?;
1390+
let imgid = hex::encode(imgid);
1391+
let mut fs = composefs_oci::image::create_filesystem(repo, &imgid, Some(&verity))
1392+
.context("Populating fs")?;
1393+
fs.transform_for_boot(&repo).context("Preparing for boot")?;
1394+
let id = fs.compute_image_id();
1395+
println!("{}", id.to_hex());
1396+
1397+
Ok(())
1398+
}
13381399
},
13391400
Opt::Image(opts) => match opts {
13401401
ImageOpts::List {

crates/lib/src/generator.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ ExecStart=bootc internals fixup-etc-fstab\n\
139139
#[cfg(test)]
140140
mod tests {
141141
use camino::Utf8Path;
142-
use cap_std_ext::cmdext::CapStdExtCommandExt as _;
143142

144143
use super::*;
145144

0 commit comments

Comments
 (0)