1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

Move install tests shell script into Rust

A few things going on here:

- Rewrite logic from shell script into Rust (using xshell, so
  it's still convenient to fork commands)
- Make the test logic take an externally-built container image
  instead of using a `-v bootc:/usr/bin/bootc` bind mount
- Build the container image using our stock hack/Containerfile
  in Github Actions instead of building for c9s in GHA
- This all hence starts to make the logic reusable outside
  of Github Actions too; the container build is a known standard thing.

Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters
2024-05-16 14:51:39 -04:00
parent d65013c208
commit 65136e03e5
10 changed files with 331 additions and 92 deletions

View File

@@ -68,26 +68,6 @@ jobs:
with:
name: bootc.tar.zst
path: target/bootc.tar.zst
build-c9s:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
runs-on: ubuntu-latest
container: quay.io/centos/centos:stream9
steps:
- run: dnf -y install git-core
- uses: actions/checkout@v4
- name: Install deps
run: ./ci/installdeps.sh
- name: Cache Dependencies
uses: Swatinem/rust-cache@v2
with:
key: "build-c9s"
- name: Build
run: make test-bin-archive
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: bootc-c9s.tar.zst
path: target/bootc.tar.zst
cargo-deny:
runs-on: ubuntu-latest
steps:
@@ -127,78 +107,24 @@ jobs:
run: sudo tar -C / -xvf bootc.tar.zst
- name: Integration tests
run: bootc internal-tests run-container-integration
privtest-alongside:
install-tests:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
name: "Test install-alongside"
needs: [build-c9s]
runs-on: ubuntu-latest
name: "Test install"
# For a not-ancient podman
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure host skopeo is disabled
run: sudo rm -f /bin/skopeo /usr/bin/skopeo
- name: Download
uses: actions/download-artifact@v4
with:
name: bootc-c9s.tar.zst
- name: Install
run: tar -xvf bootc.tar.zst
- name: Integration tests
run: |
set -xeuo pipefail
image=quay.io/centos-bootc/centos-bootc-dev:stream9
echo 'ssh-ed25519 ABC0123 testcase@example.com' > test_authorized_keys
sudo podman run --rm --privileged -v ./test_authorized_keys:/test_authorized_keys --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
${image} bootc install to-filesystem --acknowledge-destructive \
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
ls -al /boot/loader/
sudo grep foo=bar /boot/loader/entries/*.conf
grep authorized_keys /ostree/deploy/default/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf
# TODO fix https://github.com/containers/bootc/pull/137
sudo chattr -i /ostree/deploy/default/deploy/*
sudo rm /ostree/deploy/default -rf
sudo podman run --rm --privileged --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
${image} bootc install to-existing-root --acknowledge-destructive
sudo podman run --rm --privileged -v /:/target -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable ${image} bootc internal-tests verify-selinux /target/ostree --warn
install-to-existing-root:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
name: "Test install-to-existing-root"
needs: [build-c9s]
runs-on: ubuntu-latest
steps:
- name: Download
uses: actions/download-artifact@v4
with:
name: bootc-c9s.tar.zst
- name: Install
run: tar -xvf bootc.tar.zst
- name: Integration tests
run: |
set -xeuo pipefail
# We should be able to install to-existing-root with no install config,
# so we bind mount an empty directory over /usr/lib/bootc/install.
empty=$(mktemp -d)
image=quay.io/centos-bootc/centos-bootc-dev:stream9
sudo podman run --rm --privileged --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc -v ${empty}:/usr/lib/bootc/install --pid=host --security-opt label=disable \
${image} bootc install to-existing-root
install-to-loopback:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
name: "Test install to-disk --via-loopback"
needs: [build-c9s]
runs-on: ubuntu-latest
steps:
- name: Download
uses: actions/download-artifact@v4
with:
name: bootc-c9s.tar.zst
- name: Install
run: tar -xvf bootc.tar.zst
- name: Integration tests
run: |
set -xeuo pipefail
image=quay.io/centos-bootc/centos-bootc-dev:stream9
tmpdisk=$(mktemp -p /var/tmp)
truncate -s 20G ${tmpdisk}
sudo podman run --rm --privileged --env RUST_LOG=debug -v /dev:/dev -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
-v ${tmpdisk}:/disk ${image} bootc install to-disk --via-loopback /disk
set -xeu
sudo podman build -t localhost/bootc -f hack/Containerfile .
# Nondestructive but privileged tests
cargo run -p tests-integration host-privileged localhost/bootc
# Finally the install-alongside suite
cargo run -p tests-integration install-alongside localhost/bootc
docs:
if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }}
runs-on: ubuntu-latest

84
Cargo.lock generated
View File

@@ -609,6 +609,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "escape8259"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee"
dependencies = [
"rustversion",
]
[[package]]
name = "fastrand"
version = "2.0.1"
@@ -932,6 +941,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
version = "0.4.3"
@@ -1105,6 +1120,18 @@ dependencies = [
"uuid",
]
[[package]]
name = "libtest-mimic"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58"
dependencies = [
"clap",
"escape8259",
"termcolor",
"threadpool",
]
[[package]]
name = "libz-sys"
version = "1.1.15"
@@ -1253,6 +1280,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
@@ -1661,6 +1698,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.16"
@@ -1937,6 +1980,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
@@ -1947,6 +1999,20 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "tests-integration"
version = "0.1.0"
dependencies = [
"anyhow",
"camino",
"cap-std-ext",
"clap",
"fn-error-context",
"libtest-mimic",
"tempfile",
"xshell",
]
[[package]]
name = "thiserror"
version = "1.0.56"
@@ -1977,6 +2043,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "time"
version = "0.3.34"
@@ -2340,6 +2415,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["cli", "lib", "xtask"]
members = ["cli", "lib", "xtask", "tests-integration"]
resolver = "2"
[profile.dev]

View File

@@ -7,7 +7,7 @@ WORKDIR /build
RUN mkdir -p /build/target/dev-rootfs # This can hold arbitrary extra content
# See https://www.reddit.com/r/rust/comments/126xeyx/exploring_the_problem_of_faster_cargo_docker/
# We aren't using the full recommendations there, just the simple bits.
RUN --mount=type=cache,target=/build/target --mount=type=cache,target=/var/roothome make bin-archive && mkdir -p /out && cp target/bootc.tar.zst /out
RUN --mount=type=cache,target=/build/target --mount=type=cache,target=/var/roothome make test-bin-archive && mkdir -p /out && cp target/bootc.tar.zst /out
FROM quay.io/centos-bootc/centos-bootc:stream9
COPY --from=build /out/bootc.tar.zst /tmp

View File

@@ -15,8 +15,7 @@ pub fn generate_manpages(directory: &Utf8Path) -> Result<()> {
fn generate_one(directory: &Utf8Path, cmd: Command) -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let name = cmd.get_name();
let bin_name = cmd.get_bin_name()
.unwrap_or_else(|| name);
let bin_name = cmd.get_bin_name().unwrap_or_else(|| name);
let path = directory.join(format!("{name}.8"));
println!("Generating {path}...");
@@ -37,12 +36,13 @@ fn generate_one(directory: &Utf8Path, cmd: Command) -> Result<()> {
for subcmd in cmd.get_subcommands().filter(|c| !c.is_hide_set()) {
let subname = format!("{}-{}", name, subcmd.get_name());
let bin_name = format!("{} {}", bin_name, subcmd.get_name());
let bin_name = format!("{} {}", bin_name, subcmd.get_name());
// SAFETY: Latest clap 4 requires names are &'static - this is
// not long-running production code, so we just leak the names here.
let subname = &*std::boxed::Box::leak(subname.into_boxed_str());
let bin_name = &*std::boxed::Box::leak(bin_name.into_boxed_str());
let subcmd = subcmd.clone()
let subcmd = subcmd
.clone()
.name(subname)
.alias(subname)
.bin_name(bin_name)

View File

@@ -0,0 +1,21 @@
# Our integration tests
[package]
name = "tests-integration"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2021"
publish = false
[[bin]]
name = "tests-integration"
path = "src/tests-integration.rs"
[dependencies]
anyhow = "1.0.82"
camino = "1.1.6"
cap-std-ext = "4"
clap = { version= "4.5.4", features = ["derive","cargo"] }
fn-error-context = "0.2.1"
libtest-mimic = "0.7.3"
tempfile = "3.10.1"
xshell = { version = "0.2.6" }

View File

@@ -0,0 +1,24 @@
# Integration tests crate
This crate holds integration tests (as distinct from the regular
Rust unit tests run as part of `cargo test`).
## Building and running
`cargo run -p tests-integration`
will work. Note that at the current time all test suites target
an externally built bootc-compatible container image. See
how things are set up in e.g. Github Actions, where we first
run a `podman build` with the bootc git sources.
## Available suites
### `host-privileged`
This suite will run the target container image in a way that expects
full privileges, but is *not* destructive.
### `install-alongside`
This suite is *DESTRUCTIVE*, executing the bootc `install to-existing-root`
style flow using the host root. Run it in a transient virtual machine.

View File

@@ -0,0 +1,27 @@
use anyhow::Result;
use fn_error_context::context;
use libtest_mimic::Trial;
use xshell::cmd;
/// 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 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(())
})];
libtest_mimic::run(&testargs, tests.into()).exit()
}

View File

@@ -0,0 +1,125 @@
use std::os::fd::AsRawFd;
use std::path::Path;
use anyhow::Result;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use fn_error_context::context;
use libtest_mimic::Trial;
use xshell::{cmd, Shell};
pub(crate) const BASE_ARGS: &[&str] = &[
"podman",
"run",
"--rm",
"--privileged",
"-v",
"/dev:/dev",
"-v",
"/var/lib/containers:/var/lib/containers",
"--pid=host",
"--security-opt",
"label=disable",
];
// Clear out and delete any ostree roots
fn reset_root(sh: &Shell) -> Result<()> {
// TODO fix https://github.com/containers/bootc/pull/137
if !Path::new("/ostree/deploy/default").exists() {
return Ok(());
}
cmd!(
sh,
"sudo /bin/sh -c 'chattr -i /ostree/deploy/default/deploy/*'"
)
.run()?;
cmd!(sh, "sudo rm /ostree/deploy/default -rf").run()?;
Ok(())
}
fn find_deployment_root() -> Result<Dir> {
let _stateroot = "default";
let d = Dir::open_ambient_dir(
"/ostree/deploy/default/deploy",
cap_std::ambient_authority(),
)?;
for child in d.entries()? {
let child = child?;
if !child.file_type()?.is_dir() {
continue;
}
return Ok(child.open_dir()?);
}
anyhow::bail!("Failed to find deployment root")
}
#[context("Install tests")]
pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) -> Result<()> {
// Force all of these tests to be serial because they mutate global state
testargs.test_threads = Some(1);
// 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();
// Handy defaults
let target_args = &["-v", "/:/target"];
// We always need this as we assume we're operating on a local image
let generic_inst_args = ["--skip-fetch-check"];
let tests = [
Trial::test("loopback install", move || {
let sh = &xshell::Shell::new()?;
reset_root(sh)?;
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 {generic_inst_args...} /disk").run()?;
Ok(())
}),
Trial::test(
"replace=alongside with ssh keys and a karg, and SELinux disabled",
move || {
let sh = &xshell::Shell::new()?;
reset_root(sh)?;
let tmpd = &sh.create_temp_dir()?;
let tmp_keys = tmpd.path().join("test_authorized_keys");
let tmp_keys = tmp_keys.to_str().unwrap();
std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 testcase@example.com")?;
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {tmp_keys}:/test_authorized_keys {image} bootc install to-filesystem {generic_inst_args...} --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target").run()?;
cmd!(
sh,
"sudo /bin/sh -c 'grep foo=bar /boot/loader/entries/*.conf'"
)
.run()?;
let deployment = &find_deployment_root()?;
let cwd = sh.push_dir(format!("/proc/self/fd/{}", deployment.as_raw_fd()));
cmd!(
sh,
"grep authorized_keys etc/tmpfiles.d/bootc-root-ssh.conf"
)
.run()?;
drop(cwd);
Ok(())
},
),
Trial::test("Install and verify selinux state", move || {
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()?;
Ok(())
}),
Trial::test("without an install config", move || {
let sh = &xshell::Shell::new()?;
reset_root(sh)?;
let empty = sh.create_temp_dir()?;
let empty = empty.path().to_str().unwrap();
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {empty}:/usr/lib/bootc/install {image} bootc install to-existing-root {generic_inst_args...}").run()?;
Ok(())
}),
];
libtest_mimic::run(&testargs, tests.into()).exit()
}

View File

@@ -0,0 +1,32 @@
use clap::Parser;
mod hostpriv;
mod install;
#[derive(Debug, Parser)]
#[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")]
pub(crate) enum Opt {
InstallAlongside {
/// Source container image reference
image: String,
#[clap(flatten)]
testargs: libtest_mimic::Arguments,
},
HostPrivileged {
image: String,
#[clap(flatten)]
testargs: libtest_mimic::Arguments,
},
}
fn main() {
let opt = Opt::parse();
let r = match opt {
Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs),
Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs),
};
if let Err(e) = r {
eprintln!("error: {e:?}");
std::process::exit(1);
}
}