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

tests-integration: Add basic local tmt flow

Part of https://github.com/containers/bootc/issues/543

I'm not totally happy with this, but it does demonstrate the basic
wiring flow of:

- `cargo xtask test-tmt`

That will do a container build, make a disk image from it,
and run a "hello world" tmt test.

A lot more to do here including wiring up our existing tests
into this, and deduplicating with the other integration tests.

A key aspect too will be exploring workflows that e.g. expose
a registry locally.

Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters
2024-06-10 14:07:11 -04:00
parent 508f8c4782
commit 36be46a9e3
8 changed files with 201 additions and 3 deletions

8
Cargo.lock generated
View File

@@ -954,6 +954,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "instant"
version = "0.1.12"
@@ -1922,7 +1928,9 @@ dependencies = [
"cap-std-ext",
"clap",
"fn-error-context",
"indoc",
"libtest-mimic",
"oci-spec",
"rustix",
"serde",
"serde_json",

View File

@@ -32,8 +32,8 @@ bin-archive: all
test-bin-archive: all
$(MAKE) install-with-tests DESTDIR=tmp-install && $(TAR_REPRODUCIBLE) --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/*
test-tmt:
cargo xtask test-tmt
validate:
cargo fmt

11
plans/integration.fmf Normal file
View File

@@ -0,0 +1,11 @@
# This tmt test just demonstrates local tmt usage.
# We'll hopefully expand it to do more interesting things in the
# future and unify with the other test plans.
provision:
how: virtual
# Generated by `cargo xtask `
image: file://./target/testbootc-cloud.qcow2
summary: Basic smoke test
execute:
how: tmt
script: bootc status

View File

@@ -16,7 +16,9 @@ camino = "1.1.6"
cap-std-ext = "4"
clap = { version= "4.5.4", features = ["derive","cargo"] }
fn-error-context = "0.2.1"
indoc = "2.0.5"
libtest-mimic = "0.7.3"
oci-spec = "0.6.5"
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] }
serde = { features = ["derive"], version = "1.0.199" }
serde_json = "1.0.116"

View File

@@ -0,0 +1,165 @@
use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use clap::Subcommand;
use fn_error_context::context;
use xshell::{cmd, Shell};
const BUILDER_ANNOTATION: &str = "bootc.diskimage-builder";
const TEST_IMAGE: &str = "localhost/bootc";
const TESTVMDIR: &str = "testvm";
const DISK_CACHE: &str = "disk.qcow2";
const IMAGEID_XATTR: &str = "user.bootc.container-image-digest";
#[derive(Debug, Subcommand)]
#[clap(rename_all = "kebab-case")]
pub(crate) enum Opt {
PrepareTmt {
#[clap(long)]
/// The container image to spawn, otherwise one will be built
testimage: Option<String>,
},
CreateQcow2 {
/// Input container image
container: String,
/// Write disk to this path
disk: Utf8PathBuf,
},
}
struct TestContext {
sh: xshell::Shell,
targetdir: Utf8PathBuf,
}
fn image_digest(sh: &Shell, cimage: &str) -> Result<String> {
let key = "{{ .Digest }}";
let r = cmd!(sh, "podman inspect --type image --format {key} {cimage}").read()?;
Ok(r)
}
fn builder_from_image(sh: &Shell, cimage: &str) -> Result<String> {
let mut inspect: serde_json::Value =
serde_json::from_str(&cmd!(sh, "podman inspect --type image {cimage}").read()?)?;
let inspect = inspect
.as_array_mut()
.and_then(|v| v.pop())
.ok_or_else(|| anyhow::anyhow!("Failed to parse inspect output"))?;
let config = inspect
.get("Config")
.ok_or_else(|| anyhow::anyhow!("Missing config"))?;
let config: oci_spec::image::Config =
serde_json::from_value(config.clone()).context("Parsing config")?;
let builder = config
.labels()
.as_ref()
.and_then(|l| l.get(BUILDER_ANNOTATION))
.ok_or_else(|| anyhow::anyhow!("Missing {BUILDER_ANNOTATION}"))?;
Ok(builder.to_owned())
}
#[context("Running bootc-image-builder")]
fn run_bib(sh: &Shell, cimage: &str, tmpdir: &Utf8Path, diskpath: &Utf8Path) -> Result<()> {
let diskpath: Utf8PathBuf = sh.current_dir().join(diskpath).try_into()?;
let digest = image_digest(sh, cimage)?;
println!("{cimage} digest={digest}");
if diskpath.try_exists()? {
let mut buf = [0u8; 2048];
if rustix::fs::getxattr(diskpath.as_std_path(), IMAGEID_XATTR, &mut buf)
.context("Reading xattr")
.is_ok()
{
let buf = String::from_utf8_lossy(&buf);
if &*buf == digest.as_str() {
println!("Existing disk {diskpath} matches container digest {digest}");
return Ok(());
} else {
println!("Cache miss; previous digest={buf}");
}
}
}
let builder = if let Ok(b) = std::env::var("BOOTC_BUILDER") {
b
} else {
builder_from_image(sh, cimage)?
};
let _g = sh.push_dir(tmpdir);
let bibwork = "bib-work";
sh.remove_path(bibwork)?;
sh.create_dir(bibwork)?;
let _g = sh.push_dir(bibwork);
let pwd = sh.current_dir();
cmd!(sh, "podman run --rm --privileged -v /var/lib/containers/storage:/var/lib/containers/storage --security-opt label=type:unconfined_t -v {pwd}:/output {builder} --type qcow2 --local {cimage}").run()?;
let tmp_disk: Utf8PathBuf = sh
.current_dir()
.join("qcow2/disk.qcow2")
.try_into()
.unwrap();
rustix::fs::setxattr(
tmp_disk.as_std_path(),
IMAGEID_XATTR,
digest.as_bytes(),
rustix::fs::XattrFlags::empty(),
)
.context("Setting xattr")?;
cmd!(sh, "mv -Tf {tmp_disk} {diskpath}").run()?;
cmd!(sh, "rm -rf {bibwork}").run()?;
Ok(())
}
/// Given the input container image reference, create a disk
/// image in the target directory.
#[context("Creating disk")]
fn create_disk(ctx: &TestContext, cimage: &str) -> Result<Utf8PathBuf> {
let sh = &ctx.sh;
let targetdir = ctx.targetdir.as_path();
let _targetdir_guard = sh.push_dir(targetdir);
sh.create_dir(TESTVMDIR)?;
let output_disk: Utf8PathBuf = sh
.current_dir()
.join(TESTVMDIR)
.join(DISK_CACHE)
.try_into()
.unwrap();
let bibwork = "bib-work";
sh.remove_path(bibwork)?;
sh.create_dir(bibwork)?;
run_bib(sh, cimage, bibwork.into(), &output_disk)?;
Ok(output_disk)
}
pub(crate) fn run(opt: Opt) -> Result<()> {
let ctx = &{
let sh = xshell::Shell::new()?;
let mut targetdir: Utf8PathBuf = cmd!(sh, "git rev-parse --show-toplevel").read()?.into();
targetdir.push("target");
TestContext { targetdir, sh }
};
match opt {
Opt::PrepareTmt { mut testimage } => {
let testimage = if let Some(i) = testimage.take() {
i
} else {
cmd!(
&ctx.sh,
"podman build --build-arg=variant=tmt -t {TEST_IMAGE} -f hack/Containerfile ."
)
.run()?;
TEST_IMAGE.to_string()
};
let disk = create_disk(ctx, &testimage)?;
println!("Created: {disk}");
Ok(())
}
Opt::CreateQcow2 { container, disk } => {
let g = ctx.sh.push_dir(&ctx.targetdir);
ctx.sh.remove_path("tmp")?;
ctx.sh.create_dir("tmp")?;
drop(g);
run_bib(&ctx.sh, &container, "tmp".into(), &disk)
}
}
}

View File

@@ -10,6 +10,7 @@ use clap::Parser;
mod container;
mod hostpriv;
mod install;
mod runvm;
mod selinux;
#[derive(Debug, Parser)]
@@ -32,6 +33,8 @@ pub(crate) enum Opt {
#[clap(flatten)]
testargs: libtest_mimic::Arguments,
},
#[clap(subcommand)]
RunVM(runvm::Opt),
/// Extra helper utility to verify SELinux label presence
#[clap(name = "verify-selinux")]
VerifySELinux {
@@ -48,6 +51,7 @@ fn main() {
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::RunVM(opts) => runvm::run(opts),
Opt::VerifySELinux { rootfs, warn } => {
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority()).unwrap();
let mut path = PathBuf::from(".");

View File

@@ -14,7 +14,7 @@ Integration test includes two scenarios, `RPM build` and `bootc install/upgrade`
podman run --rm --privileged -v ./:/workdir:z -e TEST_OS=$TEST_OS -e ARCH=$ARCH -e RHEL_REGISTRY_URL=$RHEL_REGISTRY_URL -e DOWNLOAD_NODE=$DOWNLOAD_NODE --workdir /workdir quay.io/fedora/fedora:40 ./tests/integration/mockbuild.sh
```
#### Run Integartion Test
#### Run Integration Test
Run on a shared test infrastructure using the [`testing farm`](https://docs.testing-farm.io/Testing%20Farm/0.1/cli.html) tool. For example, running on AWS.

View File

@@ -23,6 +23,7 @@ const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
("package", package),
("package-srpm", package_srpm),
("custom-lints", custom_lints),
("test-tmt", test_tmt),
];
fn try_main() -> Result<()> {
@@ -142,6 +143,13 @@ fn man2markdown(sh: &Shell) -> Result<()> {
Ok(())
}
#[context("test-integration")]
fn test_tmt(sh: &Shell) -> Result<()> {
cmd!(sh, "cargo run -p tests-integration run-vm prepare-tmt").run()?;
cmd!(sh, "tmt run plans -n integration").run()?;
Ok(())
}
/// Return a string formatted version of the git commit timestamp, up to the minute
/// but not second because, well, we're not going to build more than once a second.
#[context("Finding git timestamp")]