Skip to content

Commit

Permalink
tests-integration: Add basic local tmt flow
Browse files Browse the repository at this point in the history
Part of #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 <[email protected]>
  • Loading branch information
cgwalters committed Jun 10, 2024
1 parent 3bd0752 commit bd5676b
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 3 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions plans/integration.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This tmt
provision:
how: virtual
# Generated by `cargo xtask `
image: file://./target/testbootc-cloud.qcow2
summary: Basic smoke test
execute:
how: tmt
script: bootc status
2 changes: 2 additions & 0 deletions tests-integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
165 changes: 165 additions & 0 deletions tests-integration/src/runvm.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
4 changes: 4 additions & 0 deletions tests-integration/src/tests-integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clap::Parser;
mod container;
mod hostpriv;
mod install;
mod runvm;
mod selinux;

#[derive(Debug, Parser)]
Expand All @@ -29,6 +30,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 {
Expand All @@ -45,6 +48,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(".");
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 8 additions & 0 deletions xtask/src/xtask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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")]
Expand Down

0 comments on commit bd5676b

Please sign in to comment.