From 7f37f9a87e140f9aeef66d17f1ad63e591e44467 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 16 May 2024 14:51:39 -0400 Subject: [PATCH] Move install tests shell script into Rust This also starts to make the logic reusable outside of Github Actions. Signed-off-by: Colin Walters --- .github/workflows/ci.yml | 95 ++---------- Cargo.lock | 13 ++ Cargo.toml | 2 +- lib/src/docgen.rs | 8 +- lib/src/kernel.rs | 2 +- tests-integration/Cargo.toml | 20 +++ tests-integration/src/tests-integration.rs | 169 +++++++++++++++++++++ 7 files changed, 217 insertions(+), 92 deletions(-) create mode 100644 tests-integration/Cargo.toml create mode 100644 tests-integration/src/tests-integration.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a04ff0e..b9f23aec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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,21 @@ 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 . + cargo run -p tests-integration run-install-tests 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 9e81ac5d..1dd91764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,6 +1947,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tests-integration" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "cap-std-ext", + "clap", + "fn-error-context", + "tempfile", + "xshell", +] + [[package]] name = "thiserror" version = "1.0.56" diff --git a/Cargo.toml b/Cargo.toml index 0c3ab591..371c1e2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cli", "lib", "xtask"] +members = ["cli", "lib", "xtask", "tests-integration"] resolver = "2" [profile.dev] diff --git a/lib/src/docgen.rs b/lib/src/docgen.rs index 06ea39b2..a76191e4 100644 --- a/lib/src/docgen.rs +++ b/lib/src/docgen.rs @@ -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) diff --git a/lib/src/kernel.rs b/lib/src/kernel.rs index ecc0994c..7f6aeff9 100644 --- a/lib/src/kernel.rs +++ b/lib/src/kernel.rs @@ -40,7 +40,7 @@ pub(crate) fn find_first_cmdline_arg<'a>( #[test] fn test_find_first() { let kargs = &["foo=bar", "root=/dev/vda", "blah", "root=/dev/other"]; - let kargs = || kargs.iter().map(|&s| s); + let kargs = || kargs.iter().copied(); assert_eq!(find_first_cmdline_arg(kargs(), "root"), Some("/dev/vda")); assert_eq!(find_first_cmdline_arg(kargs(), "nonexistent"), None); } diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml new file mode 100644 index 00000000..a20c6f39 --- /dev/null +++ b/tests-integration/Cargo.toml @@ -0,0 +1,20 @@ +# 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" +tempfile = "3.10.1" +xshell = { version = "0.2.6" } diff --git a/tests-integration/src/tests-integration.rs b/tests-integration/src/tests-integration.rs new file mode 100644 index 00000000..942352b3 --- /dev/null +++ b/tests-integration/src/tests-integration.rs @@ -0,0 +1,169 @@ +use std::path::Path; +use std::{iter::once, os::fd::AsRawFd}; + +use anyhow::Result; +use cap_std_ext::cap_std; +use cap_std_ext::cap_std::fs::Dir; +use clap::Parser; +use fn_error_context::context; + +use xshell::{cmd, Shell}; + +#[derive(Debug, Parser, PartialEq, Eq)] +#[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")] +pub(crate) enum Opt { + RunInstallTests { + /// Source container image reference + image: String, + }, +} + +fn main() { + if let Err(e) = try_main() { + eprintln!("error: {e:?}"); + std::process::exit(1); + } +} + +fn try_main() -> Result<()> { + let opt = Opt::parse(); + match opt { + Opt::RunInstallTests { image } => run_install_tests(image.as_str()), + } +} + +// 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, "chattr -i /ostree/deploy/default/deploy/*").run()?; + cmd!(sh, "sudo rm /ostree/deploy/default -rf").run()?; + Ok(()) +} + +fn run_install<'a, 'b>( + sh: &Shell, + podman_args: impl Iterator, + image: &str, + install_args: impl Iterator, +) -> Result<()> { + cmd!(sh, "sudo podman run --rm --privileged -v /dev:/dev -v /var/lib/containers:/var/lib/containers --pid=host --security-opt label=disable {podman_args...} {image} bootc install {install_args...}").run()?; + Ok(()) +} + +fn find_deployment_root() -> Result { + 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")] +fn run_install_tests(image: &str) -> Result<()> { + let sh = &xshell::Shell::new()?; + + 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"]; + + reset_root(sh)?; + println!("test: replace=alongside with ssh keys and a karg, and SELinux disabled"); + { + let tmpd = &sh.create_temp_dir()?; + let tmp_keys = tmpd.path().join("test_authorized_keys"); + std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 testcase@example.com")?; + let tmp_keys = format!("{}:/test_authorized_keys", tmp_keys.to_str().unwrap()); + let install_args = [ + "to-filesystem", + "--acknowledge-destructive", + "--karg=foo=bar", + "--disable-selinux", + "--replace=alongside", + "--root-ssh-authorized-keys=/test_authorized_keys", + "/target", + ]; + run_install( + sh, + target_args.into_iter().chain(["-v", tmp_keys.as_str()]), + image, + install_args.into_iter().chain(generic_inst_args), + )?; + + 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); + } + println!("ok install with tmpfiles and karg"); + + reset_root(sh)?; + println!("test: Install and verify selinux state"); + run_install( + sh, + target_args.into_iter(), + image, + ["to-existing-root", "--acknowledge-destructive"] + .into_iter() + .chain(generic_inst_args), + )?; + cmd!(sh, "sudo podman run --rm --privileged --pid=host {image} bootc internal-tests verify-selinux /target/ostree --warn").run()?; + println!("ok install selinux"); + + reset_root(sh)?; + println!("test: without an install config"); + { + let empty = sh.create_temp_dir()?; + let empty_arg = format!("{}:/usr/lib/bootc/install", empty.path().to_str().unwrap()); + run_install( + sh, + target_args.into_iter().chain(["-v", empty_arg.as_str()]), + image, + ["bootc", "install", "to-existing-root"] + .into_iter() + .chain(generic_inst_args), + )?; + } + + reset_root(sh)?; + println!("test: a loopback install"); + { + let size = 20_1000_1000_1000; + let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; + tmpdisk.as_file_mut().set_len(size)?; + let tmpdisk = tmpdisk.path().to_str().unwrap(); + let diskarg = format!("{tmpdisk}:/disk"); + run_install( + sh, + ["-v", diskarg.as_str()].into_iter(), + image, + ["to-disk", "--via-loopback"] + .into_iter() + .chain(generic_inst_args) + .chain(once("/disk")) + .into_iter(), + )?; + } + println!("ok install to-disk --via-loopback"); + + Ok(()) +}