From 9be6b5a0b024eb73b4655bc3a5ef3102bdb4720a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 31 May 2024 16:01:41 -0400 Subject: [PATCH] tests: Drop `internal-testing-api`, move to tests-integration Previous work started moving our tests into an external binary; this is just cleaner because it can test things how a user would test. Also, we started using `libtest-mimic` to have a "real" test scaffolding that e.g. allows selecting individual tests to run, etc. Complete the picture here by moving the remaining bits into the tests-integration binary. We now run the `tests-integration` binary in two ways in e.g. Github Actions: - It's compiled directly on the Ubuntu runner, and orchestrates things itself - It's built in our default container image (Fedora) but as an external `/usr/bin/bootc-integration-tests` binary Also while we're here, drop the kola tests. Signed-off-by: Colin Walters --- .github/workflows/ci.yml | 16 +- Cargo.lock | 3 + Makefile | 10 +- ci/clean-gha-runner.sh | 13 ++ lib/Cargo.toml | 3 +- lib/src/cli.rs | 28 --- lib/src/lib.rs | 3 - lib/src/privtests.rs | 212 --------------------- tests-integration/Cargo.toml | 3 + tests-integration/src/container.rs | 52 +++++ tests-integration/src/hostpriv.rs | 43 +++-- tests-integration/src/install.rs | 6 +- tests-integration/src/selinux.rs | 35 ++++ tests-integration/src/tests-integration.rs | 26 +++ tests/kolainst/basic | 47 ----- tests/kolainst/install | 54 ------ 16 files changed, 183 insertions(+), 371 deletions(-) create mode 100755 ci/clean-gha-runner.sh delete mode 100644 lib/src/privtests.rs create mode 100644 tests-integration/src/container.rs create mode 100644 tests-integration/src/selinux.rs delete mode 100755 tests/kolainst/basic delete mode 100755 tests/kolainst/install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60409a62..4f2aae6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,9 +57,7 @@ jobs: - name: Build container (fedora) run: sudo podman build --build-arg=base=quay.io/fedora/fedora-bootc:40 -t localhost/bootc -f hack/Containerfile . - name: Container integration - run: sudo podman run --rm localhost/bootc bootc internal-tests run-container-integration - - name: Privileged tests - run: sudo podman run --rm --privileged -v /run/systemd:/run/systemd -v /:/run/host --pid=host localhost/bootc bootc internal-tests run-privileged-integration + run: sudo podman run --rm localhost/bootc bootc-integration-tests container cargo-deny: runs-on: ubuntu-latest steps: @@ -78,14 +76,22 @@ jobs: uses: actions/checkout@v4 - name: Ensure host skopeo is disabled run: sudo rm -f /bin/skopeo /usr/bin/skopeo + - name: Free up disk space on runner + run: sudo ./ci/clean-gha-runner.sh - name: Integration tests run: | set -xeu sudo podman build -t localhost/bootc -f hack/Containerfile . + export CARGO_INCREMENTAL=0 # because we aren't caching the test runner bits + cargo build --release -p tests-integration + df -h / + sudo install -m 0755 target/release/tests-integration /usr/bin/bootc-integration-tests + rm target -rf + df -h / # Nondestructive but privileged tests - cargo run -p tests-integration host-privileged localhost/bootc + sudo bootc-integration-tests host-privileged localhost/bootc # Finally the install-alongside suite - cargo run -p tests-integration install-alongside localhost/bootc + sudo bootc-integration-tests install-alongside localhost/bootc docs: if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }} runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index cb57a15c..669892cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2016,6 +2016,9 @@ dependencies = [ "clap", "fn-error-context", "libtest-mimic", + "rustix", + "serde", + "serde_json", "tempfile", "xshell", ] diff --git a/Makefile b/Makefile index f760dac0..3eb78927 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,6 @@ prefix ?= /usr all: cargo build --release -all-test: - cargo build --release --all-features - install: install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/ @@ -22,11 +19,14 @@ install: done install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer +install-with-tests: install + install -D -m 0755 target/release/tests-integration $(DESTDIR)$(prefix)/bin/bootc-integration-tests + bin-archive: all $(MAKE) install DESTDIR=tmp-install && tar --zstd -C tmp-install -cf target/bootc.tar.zst . && rm tmp-install -rf -test-bin-archive: all-test - $(MAKE) install DESTDIR=tmp-install && tar --zstd -C tmp-install -cf target/bootc.tar.zst . && rm tmp-install -rf +test-bin-archive: all + $(MAKE) install-with-tests DESTDIR=tmp-install && tar --zstd -C tmp-install -cf target/bootc.tar.zst . && rm tmp-install -rf install-kola-tests: install -D -t $(DESTDIR)$(prefix)/lib/coreos-assembler/tests/kola/bootc tests/kolainst/* diff --git a/ci/clean-gha-runner.sh b/ci/clean-gha-runner.sh new file mode 100755 index 00000000..b6bac90c --- /dev/null +++ b/ci/clean-gha-runner.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -xeuo pipefail +df -h +docker image prune --all --force > /dev/null +rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android +apt-get remove -y '^aspnetcore-.*' > /dev/null +apt-get remove -y '^dotnet-.*' > /dev/null +apt-get remove -y '^llvm-.*' > /dev/null +apt-get remove -y 'php.*' > /dev/null +apt-get remove -y '^mongodb-.*' > /dev/null +apt-get remove -y '^mysql-.*' > /dev/null1 +apt-get remove -y azure-cli google-chrome-stable firefox mono-devel >/dev/null +df -h diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 31c97489..0e861662 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -54,5 +54,4 @@ default = ["install"] install = [] # Implementation detail of man page generation. docgen = ["clap_mangen"] -# This feature should only be enabled in CI environments. -internal-testing-api = ["xshell"] + diff --git a/lib/src/cli.rs b/lib/src/cli.rs index cbbfe358..0ad782ba 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -161,28 +161,6 @@ impl InternalsOpts { const GENERATOR_BIN: &'static str = "bootc-systemd-generator"; } -/// Options for internal testing -#[derive(Debug, clap::Subcommand, PartialEq, Eq)] -pub(crate) enum TestingOpts { - /// Execute integration tests that require a privileged container - RunPrivilegedIntegration {}, - /// Execute integration tests that target a not-privileged ostree container - RunContainerIntegration {}, - /// Block device setup for testing - PrepTestInstallFilesystem { blockdev: Utf8PathBuf }, - /// e2e test of install to-filesystem - TestInstallFilesystem { - image: String, - blockdev: Utf8PathBuf, - }, - #[clap(name = "verify-selinux")] - VerifySELinux { - root: String, - #[clap(long)] - warn: bool, - }, -} - /// Deploy and transactionally in-place with bootable container images. /// /// The `bootc` project currently uses ostree-containers as a backend @@ -302,10 +280,6 @@ pub(crate) enum Opt { #[clap(subcommand)] #[clap(hide = true)] Internals(InternalsOpts), - /// Internal integration testing helpers. - #[clap(hide(true), subcommand)] - #[cfg(feature = "internal-testing-api")] - InternalTests(TestingOpts), #[clap(hide(true))] #[cfg(feature = "docgen")] Man(ManOpts), @@ -689,8 +663,6 @@ async fn run_from_opt(opt: Opt) -> Result<()> { } InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root), }, - #[cfg(feature = "internal-testing-api")] - Opt::InternalTests(opts) => crate::privtests::run(opts).await, #[cfg(feature = "docgen")] Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory), } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b04d3364..19faff0f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -29,9 +29,6 @@ mod status; mod task; mod utils; -#[cfg(feature = "internal-testing-api")] -mod privtests; - #[cfg(feature = "install")] mod blockdev; #[cfg(feature = "install")] diff --git a/lib/src/privtests.rs b/lib/src/privtests.rs deleted file mode 100644 index cdc66d6f..00000000 --- a/lib/src/privtests.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::os::fd::AsRawFd; -use std::path::{Path, PathBuf}; -use std::process::Command; - -use anyhow::{Context, Result}; -use camino::Utf8Path; -use cap_std_ext::cap_std; -use cap_std_ext::cap_std::fs::Dir; -use fn_error_context::context; -use rustix::fd::AsFd; -use xshell::{cmd, Shell}; - -use crate::blockdev::LoopbackDevice; -use crate::install::config::InstallConfiguration; - -use super::cli::TestingOpts; -use super::spec::Host; - -const IMGSIZE: u64 = 20 * 1024 * 1024 * 1024; - -fn init_ostree(sh: &Shell, rootfs: &Utf8Path) -> Result<()> { - cmd!(sh, "ostree admin init-fs --modern {rootfs}").run()?; - Ok(()) -} - -#[context("bootc status")] -fn run_bootc_status() -> Result<()> { - let sh = Shell::new()?; - - let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; - rustix::fs::ftruncate(tmpdisk.as_file_mut().as_fd(), IMGSIZE)?; - let loopdev = LoopbackDevice::new(tmpdisk.path())?; - let devpath = loopdev.path(); - println!("Using {devpath:?}"); - - let td = tempfile::tempdir()?; - let td = td.path(); - let td: &Utf8Path = td.try_into()?; - - cmd!(sh, "mkfs.xfs {devpath}").run()?; - cmd!(sh, "mount {devpath} {td}").run()?; - - init_ostree(&sh, td)?; - - // Basic sanity test of `bootc status` on an uninitialized root - let _g = sh.push_env("OSTREE_SYSROOT", td); - cmd!(sh, "bootc status").run()?; - - Ok(()) -} - -// This needs nontrivial work for loopback devices -// #[context("bootc install")] -// fn run_bootc_install() -> Result<()> { -// let sh = Shell::new()?; -// let loopdev = LoopbackDevice::new_temp(&sh)?; -// let devpath = &loopdev.dev; -// println!("Using {devpath:?}"); - -// let selinux_enabled = crate::lsm::selinux_enabled()?; -// let selinux_opt = if selinux_enabled { -// "" -// } else { -// "--disable-selinux" -// }; - -// cmd!(sh, "bootc install {selinux_opt} {devpath}").run()?; - -// Ok(()) -// } - -/// Tests run an ostree-based host -#[context("Privileged container tests")] -pub(crate) fn impl_run_host() -> Result<()> { - run_bootc_status()?; - println!("ok bootc status"); - //run_bootc_install()?; - //println!("ok bootc install"); - println!("ok host privileged testing"); - Ok(()) -} - -#[context("Container tests")] -pub(crate) fn impl_run_container() -> Result<()> { - let sh = Shell::new()?; - let host: Host = serde_yaml::from_str(&cmd!(sh, "bootc status").read()?)?; - assert!(matches!(host.status.ty, None)); - println!("ok status"); - - for c in ["upgrade", "update"] { - let o = Command::new("bootc").arg(c).output()?; - let st = o.status; - assert!(!st.success()); - let stderr = String::from_utf8(o.stderr)?; - assert!( - stderr.contains("this command requires a booted host system"), - "stderr: {stderr}", - ); - } - println!("ok upgrade/update are errors in container"); - - let config = cmd!(sh, "bootc install print-configuration").read()?; - let mut config: InstallConfiguration = - serde_json::from_str(&config).context("Parsing install config")?; - // Just verify we parsed the config, if any - drop(config); - - println!("ok container integration testing"); - Ok(()) -} - -#[context("Prep test install filesystem")] -fn prep_test_install_filesystem(blockdev: &Utf8Path) -> Result { - let sh = Shell::new()?; - // Arbitrarily larger partition offsets - let efipn = "5"; - let bootpn = "6"; - let rootpn = "7"; - let mountpoint_dir = tempfile::tempdir()?; - let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap(); - // Create the partition setup; we add some random empty partitions for 2,3,4 just to exercise things - cmd!( - sh, - "sgdisk -Z {blockdev} -n 1:0:+1M -c 1:BIOS-BOOT -t 1:21686148-6449-6E6F-744E-656564454649 -n 2:0:+3M -n 3:0:+2M -n 4:0:+5M -n {efipn}:0:+127M -c {efipn}:EFI-SYSTEM -t ${efipn}:C12A7328-F81F-11D2-BA4B-00A0C93EC93B -n {bootpn}:0:+510M -c {bootpn}:boot -n {rootpn}:0:0 -c {rootpn}:root -t {rootpn}:0FC63DAF-8483-4772-8E79-3D69D8477DE4" - ) - .run()?; - // Create filesystems and mount - cmd!(sh, "mkfs.ext4 {blockdev}{bootpn}").run()?; - cmd!(sh, "mkfs.ext4 {blockdev}{rootpn}").run()?; - cmd!(sh, "mkfs.fat {blockdev}{efipn}").run()?; - cmd!(sh, "mount {blockdev}{rootpn} {mountpoint}").run()?; - cmd!(sh, "mkdir {mountpoint}/boot").run()?; - cmd!(sh, "mount {blockdev}{bootpn} {mountpoint}/boot").run()?; - let efidir = crate::bootloader::EFI_DIR; - cmd!(sh, "mkdir {mountpoint}/boot/{efidir}").run()?; - cmd!(sh, "mount {blockdev}{efipn} {mountpoint}/boot/{efidir}").run()?; - - Ok(mountpoint_dir) -} - -#[context("Test install filesystem")] -fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> { - let sh = Shell::new()?; - - let mountpoint_dir = prep_test_install_filesystem(blockdev)?; - let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap(); - - // And run the install - cmd!(sh, "podman run --rm --privileged --pid=host --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install to-filesystem /target-root").run()?; - - cmd!(sh, "umount -R {mountpoint}").run()?; - - Ok(()) -} - -fn verify_selinux_label_exists(root: &Dir, path: &Path, warn: bool) -> Result<()> { - let mut buf = [0u8; 1024]; - let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd()); - let fdpath = &Path::new(&fdpath).join(path); - match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) { - // Ignore EOPNOTSUPPORTED - Ok(_) | Err(rustix::io::Errno::OPNOTSUPP) => Ok(()), - Err(rustix::io::Errno::NODATA) if warn => { - eprintln!("No SELinux label found for: {path:?}"); - Ok(()) - } - Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")), - } -} - -fn verify_selinux_recurse(root: &Dir, path: &mut PathBuf, warn: bool) -> Result<()> { - for ent in root.read_dir(&path)? { - let ent = ent?; - let name = ent.file_name(); - path.push(name); - verify_selinux_label_exists(root, &path, warn)?; - let file_type = ent.file_type()?; - if file_type.is_dir() { - verify_selinux_recurse(root, path, warn)?; - } - path.pop(); - } - Ok(()) -} - -pub(crate) async fn run(opts: TestingOpts) -> Result<()> { - match opts { - TestingOpts::RunPrivilegedIntegration {} => { - crate::cli::ensure_self_unshared_mount_namespace().await?; - tokio::task::spawn_blocking(impl_run_host).await? - } - TestingOpts::RunContainerIntegration {} => { - tokio::task::spawn_blocking(impl_run_container).await? - } - TestingOpts::PrepTestInstallFilesystem { blockdev } => { - tokio::task::spawn_blocking(move || prep_test_install_filesystem(&blockdev).map(|_| ())) - .await? - } - TestingOpts::TestInstallFilesystem { image, blockdev } => { - crate::cli::ensure_self_unshared_mount_namespace().await?; - tokio::task::spawn_blocking(move || test_install_filesystem(&image, &blockdev)).await? - } - // This one is currently executed mainly from Github Actions - TestingOpts::VerifySELinux { root, warn } => { - let rootfs = cap_std::fs::Dir::open_ambient_dir(root, cap_std::ambient_authority()) - .context("Opening dir")?; - let mut path = PathBuf::from("."); - tokio::task::spawn_blocking(move || verify_selinux_recurse(&rootfs, &mut path, warn)) - .await? - } - } -} diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index f6821ed6..95807497 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -17,5 +17,8 @@ cap-std-ext = "4" clap = { version= "4.5.4", features = ["derive","cargo"] } fn-error-context = "0.2.1" libtest-mimic = "0.7.3" +rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] } +serde = { features = ["derive"], version = "1.0.199" } +serde_json = "1.0.116" tempfile = "3.10.1" xshell = { version = "0.2.6" } diff --git a/tests-integration/src/container.rs b/tests-integration/src/container.rs new file mode 100644 index 00000000..9d2fdf4b --- /dev/null +++ b/tests-integration/src/container.rs @@ -0,0 +1,52 @@ +use std::process::Command; + +use anyhow::{Context, Result}; +use fn_error_context::context; +use libtest_mimic::Trial; +use xshell::{cmd, Shell}; + +fn new_test(description: &'static str, f: fn() -> anyhow::Result<()>) -> libtest_mimic::Trial { + Trial::test(description, move || f().map_err(Into::into)) +} + +pub(crate) fn test_bootc_status() -> Result<()> { + let sh = Shell::new()?; + let host: serde_json::Value = serde_json::from_str(&cmd!(sh, "bootc status --json").read()?)?; + assert!(host.get("status").unwrap().get("ty").is_none()); + Ok(()) +} + +pub(crate) fn test_bootc_upgrade() -> Result<()> { + for c in ["upgrade", "update"] { + let o = Command::new("bootc").arg(c).output()?; + let st = o.status; + assert!(!st.success()); + let stderr = String::from_utf8(o.stderr)?; + assert!( + stderr.contains("this command requires a booted host system"), + "stderr: {stderr}", + ); + } + Ok(()) +} + +pub(crate) fn test_bootc_install_config() -> Result<()> { + let sh = &xshell::Shell::new()?; + let config = cmd!(sh, "bootc install print-configuration").read()?; + let config: serde_json::Value = + serde_json::from_str(&config).context("Parsing install config")?; + // Just verify we parsed the config, if any + drop(config); + Ok(()) +} +/// Tests that should be run in a default container image. +#[context("Container tests")] +pub(crate) fn run(testargs: libtest_mimic::Arguments) -> Result<()> { + let tests = [ + new_test("bootc upgrade", test_bootc_upgrade), + new_test("install config", test_bootc_install_config), + new_test("status", test_bootc_status), + ]; + + libtest_mimic::run(&testargs, tests.into()).exit() +} diff --git a/tests-integration/src/hostpriv.rs b/tests-integration/src/hostpriv.rs index c7cf210f..c3e9cbab 100644 --- a/tests-integration/src/hostpriv.rs +++ b/tests-integration/src/hostpriv.rs @@ -3,25 +3,42 @@ use fn_error_context::context; use libtest_mimic::Trial; use xshell::cmd; +struct TestState { + image: String, +} + +fn new_test( + state: &'static TestState, + description: &'static str, + f: fn(&'static str) -> anyhow::Result<()>, +) -> libtest_mimic::Trial { + Trial::test(description, move || f(&state.image).map_err(Into::into)) +} + +fn test_loopback_install(image: &'static str) -> Result<()> { + let base_args = super::install::BASE_ARGS; + let sh = &xshell::Shell::new()?; + let size = 10 * 1000 * 1000 * 1000; + let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; + tmpdisk.as_file_mut().set_len(size)?; + let tmpdisk = tmpdisk.into_temp_path(); + let tmpdisk = tmpdisk.to_str().unwrap(); + cmd!(sh, "sudo {base_args...} -v {tmpdisk}:/disk {image} bootc install to-disk --via-loopback --skip-fetch-check /disk").run()?; + Ok(()) +} + /// Tests that require real root (e.g. CAP_SYS_ADMIN) to do things like /// create loopback devices, but are *not* destructive. At the current time /// these tests are defined to reference a bootc container image. #[context("Hostpriv tests")] pub(crate) fn run_hostpriv(image: &str, testargs: libtest_mimic::Arguments) -> Result<()> { - // Just leak the image name so we get a static reference as required by the test framework - let image: &'static str = String::from(image).leak(); - let base_args = super::install::BASE_ARGS; + let state = Box::new(TestState { + image: image.to_string(), + }); + // Make this static because the tests require it + let state: &'static TestState = Box::leak(state); - let tests = [Trial::test("loopback install", move || { - let sh = &xshell::Shell::new()?; - let size = 10 * 1000 * 1000 * 1000; - let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; - tmpdisk.as_file_mut().set_len(size)?; - let tmpdisk = tmpdisk.into_temp_path(); - let tmpdisk = tmpdisk.to_str().unwrap(); - cmd!(sh, "sudo {base_args...} -v {tmpdisk}:/disk {image} bootc install to-disk --via-loopback --skip-fetch-check /disk").run()?; - Ok(()) - })]; + let tests = [new_test(&state, "loopback install", test_loopback_install)]; libtest_mimic::run(&testargs, tests.into()).exit() } diff --git a/tests-integration/src/install.rs b/tests-integration/src/install.rs index 0339cd84..5419f35f 100644 --- a/tests-integration/src/install.rs +++ b/tests-integration/src/install.rs @@ -1,5 +1,5 @@ -use std::os::fd::AsRawFd; use std::path::Path; +use std::{os::fd::AsRawFd, path::PathBuf}; use anyhow::Result; use cap_std_ext::cap_std; @@ -108,7 +108,9 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) let sh = &xshell::Shell::new()?; reset_root(sh)?; cmd!(sh, "sudo {BASE_ARGS...} {target_args...} {image} bootc install to-existing-root --acknowledge-destructive {generic_inst_args...}").run()?; - cmd!(sh, "sudo podman run --rm --privileged --pid=host {target_args...} {image} bootc internal-tests verify-selinux /target/ostree --warn").run()?; + let root = &Dir::open_ambient_dir("/ostree", cap_std::ambient_authority()).unwrap(); + let mut path = PathBuf::from("."); + crate::selinux::verify_selinux_recurse(root, &mut path, true)?; Ok(()) }), Trial::test("without an install config", move || { diff --git a/tests-integration/src/selinux.rs b/tests-integration/src/selinux.rs new file mode 100644 index 00000000..783a61b6 --- /dev/null +++ b/tests-integration/src/selinux.rs @@ -0,0 +1,35 @@ +use std::os::fd::AsRawFd; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use cap_std_ext::cap_std::fs::Dir; + +fn verify_selinux_label_exists(root: &Dir, path: &Path, warn: bool) -> Result<()> { + let mut buf = [0u8; 1024]; + let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd()); + let fdpath = &Path::new(&fdpath).join(path); + match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) { + // Ignore EOPNOTSUPPORTED + Ok(_) | Err(rustix::io::Errno::OPNOTSUPP) => Ok(()), + Err(rustix::io::Errno::NODATA) if warn => { + eprintln!("No SELinux label found for: {path:?}"); + Ok(()) + } + Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")), + } +} + +pub(crate) fn verify_selinux_recurse(root: &Dir, path: &mut PathBuf, warn: bool) -> Result<()> { + for ent in root.read_dir(&path)? { + let ent = ent?; + let name = ent.file_name(); + path.push(name); + verify_selinux_label_exists(root, &path, warn)?; + let file_type = ent.file_type()?; + if file_type.is_dir() { + verify_selinux_recurse(root, path, warn)?; + } + path.pop(); + } + Ok(()) +} diff --git a/tests-integration/src/tests-integration.rs b/tests-integration/src/tests-integration.rs index 250f96f0..860456a9 100644 --- a/tests-integration/src/tests-integration.rs +++ b/tests-integration/src/tests-integration.rs @@ -1,7 +1,13 @@ +use std::path::PathBuf; + +use camino::Utf8PathBuf; +use cap_std_ext::cap_std::{self, fs::Dir}; use clap::Parser; +mod container; mod hostpriv; mod install; +mod selinux; #[derive(Debug, Parser)] #[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")] @@ -17,6 +23,20 @@ pub(crate) enum Opt { #[clap(flatten)] testargs: libtest_mimic::Arguments, }, + /// Tests which should be executed inside an existing bootc container image. + /// These should be nondestructive. + Container { + #[clap(flatten)] + testargs: libtest_mimic::Arguments, + }, + /// Extra helper utility to verify SELinux label presence + #[clap(name = "verify-selinux")] + VerifySELinux { + /// Path to target root + rootfs: Utf8PathBuf, + #[clap(long)] + warn: bool, + }, } fn main() { @@ -24,6 +44,12 @@ fn main() { let r = match opt { Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs), Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs), + Opt::Container { testargs } => container::run(testargs), + Opt::VerifySELinux { rootfs, warn } => { + let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority()).unwrap(); + let mut path = PathBuf::from("."); + selinux::verify_selinux_recurse(root, &mut path, warn) + } }; if let Err(e) = r { eprintln!("error: {e:?}"); diff --git a/tests/kolainst/basic b/tests/kolainst/basic deleted file mode 100755 index bef28c21..00000000 --- a/tests/kolainst/basic +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# Verify basic bootc functionality. -## kola: -## timeoutMin: 30 -## tags: "needs-internet" -# -# Copyright (C) 2022 Red Hat, Inc. - -set -xeuo pipefail - -cd $(mktemp -d) - -case "${AUTOPKGTEST_REBOOT_MARK:-}" in - "") - bootc status > status.txt - grep 'Version:' status.txt - bootc status --json > status.json - image=$(jq '.status.booted.image.image' < status.json) - echo "booted into $image" - echo "ok status test" - - # Switch should be idempotent - # (also TODO, get rid of the crazy .image.image.image nesting) - name=$(echo "${image}" | jq -r '.image') - bootc switch $name - staged=$(bootc status --json | jq .status.staged) - test "$staged" = "null" - - host_ty=$(jq -r '.status.type' < status.json) - test "${host_ty}" = "bootcHost" - # Now fake things out with an empty /run - unshare -m /bin/sh -c 'mount -t tmpfs tmpfs /run; bootc status --json > status-no-run.json' - host_ty_norun=$(jq -r '.status.type' < status-no-run.json) - test "${host_ty_norun}" = "null" - - test "null" = $(jq '.status.staged' < status.json) - # Should be a no-op - bootc update - test "null" = $(jq '.status.staged' < status.json) - - test '!' -w /usr - bootc usroverlay - test -w /usr - echo "ok usroverlay" - ;; - *) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;; -esac diff --git a/tests/kolainst/install b/tests/kolainst/install deleted file mode 100755 index 51cf8698..00000000 --- a/tests/kolainst/install +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Verify install path -## kola: -## timeoutMin: 30 -## tags: "needs-internet" -## platforms: qemu # additionalDisks is only supported on qemu -## additionalDisks: ["20G"] -# -# Copyright (C) 2022 Red Hat, Inc. - -set -xeuo pipefail - -IMAGE=quay.io/centos-bootc/fedora-bootc:eln-1708320930 -# TODO: better detect this, e.g. look for an empty device -DEV=/dev/vda - -# Always work out of a temporary directory -cd $(mktemp -d) - -case "${AUTOPKGTEST_REBOOT_MARK:-}" in - "") - mkdir -p ~/.config/containers - cp -a /etc/ostree/auth.json ~/.config/containers - mkdir -p usr/{lib,bin} - cp -a /usr/lib/bootc usr/lib - cp -a /usr/bin/bootc usr/bin - cat > Dockerfile << EOF - FROM ${IMAGE} - COPY usr usr -EOF - podman build -t localhost/testimage . - podman run --rm --privileged --pid=host --env RUST_LOG=error,bootc_lib::install=debug \ - localhost/testimage bootc install to-disk --skip-fetch-check --karg=foo=bar ${DEV} - # In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot; - # but for now let's just sanity test that the install command executes. - lsblk ${DEV} - mount /dev/vda3 /var/mnt - grep foo=bar /var/mnt/loader/entries/*.conf - grep localtestkarg=somevalue /var/mnt/loader/entries/*.conf - grep -Ee '^linux /boot/ostree' /var/mnt/loader/entries/*.conf - umount /var/mnt - echo "ok install" - mount /dev/vda4 /var/mnt - ls -dZ /var/mnt |grep ':root_t:' - umount /var/mnt - - # Now test install to-filesystem - # Wipe the device - ls ${DEV}* | tac | xargs wipefs -af - # This prepares the device and also runs podman directliy - bootc internal-tests test-install-filesystem ${IMAGE} ${DEV} - ;; - *) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;; -esac