mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 15:45:53 +01:00
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 <walters@verbum.org>
This commit is contained in:
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2016,6 +2016,9 @@ dependencies = [
|
||||
"clap",
|
||||
"fn-error-context",
|
||||
"libtest-mimic",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"xshell",
|
||||
]
|
||||
|
||||
10
Makefile
10
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/*
|
||||
|
||||
13
ci/clean-gha-runner.sh
Executable file
13
ci/clean-gha-runner.sh
Executable file
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<tempfile::TempDir> {
|
||||
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?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
52
tests-integration/src/container.rs
Normal file
52
tests-integration/src/container.rs
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
35
tests-integration/src/selinux.rs
Normal file
35
tests-integration/src/selinux.rs
Normal file
@@ -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(())
|
||||
}
|
||||
@@ -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:?}");
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user