Skip to content

Commit 9be5830

Browse files
authored
Merge pull request #593 from cgwalters/testing-tmt
tests-integration: Add basic local tmt flow
2 parents 78c10ef + 36be46a commit 9be5830

File tree

8 files changed

+201
-3
lines changed

8 files changed

+201
-3
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ bin-archive: all
3232
test-bin-archive: all
3333
$(MAKE) install-with-tests DESTDIR=tmp-install && $(TAR_REPRODUCIBLE) --zstd -C tmp-install -cf target/bootc.tar.zst . && rm tmp-install -rf
3434

35-
install-kola-tests:
36-
install -D -t $(DESTDIR)$(prefix)/lib/coreos-assembler/tests/kola/bootc tests/kolainst/*
35+
test-tmt:
36+
cargo xtask test-tmt
3737

3838
validate:
3939
cargo fmt

plans/integration.fmf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This tmt test just demonstrates local tmt usage.
2+
# We'll hopefully expand it to do more interesting things in the
3+
# future and unify with the other test plans.
4+
provision:
5+
how: virtual
6+
# Generated by `cargo xtask `
7+
image: file://./target/testbootc-cloud.qcow2
8+
summary: Basic smoke test
9+
execute:
10+
how: tmt
11+
script: bootc status

tests-integration/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ camino = "1.1.6"
1616
cap-std-ext = "4"
1717
clap = { version= "4.5.4", features = ["derive","cargo"] }
1818
fn-error-context = "0.2.1"
19+
indoc = "2.0.5"
1920
libtest-mimic = "0.7.3"
21+
oci-spec = "0.6.5"
2022
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] }
2123
serde = { features = ["derive"], version = "1.0.199" }
2224
serde_json = "1.0.116"

tests-integration/src/runvm.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use anyhow::{Context, Result};
2+
use camino::{Utf8Path, Utf8PathBuf};
3+
use clap::Subcommand;
4+
use fn_error_context::context;
5+
use xshell::{cmd, Shell};
6+
7+
const BUILDER_ANNOTATION: &str = "bootc.diskimage-builder";
8+
const TEST_IMAGE: &str = "localhost/bootc";
9+
const TESTVMDIR: &str = "testvm";
10+
const DISK_CACHE: &str = "disk.qcow2";
11+
const IMAGEID_XATTR: &str = "user.bootc.container-image-digest";
12+
13+
#[derive(Debug, Subcommand)]
14+
#[clap(rename_all = "kebab-case")]
15+
pub(crate) enum Opt {
16+
PrepareTmt {
17+
#[clap(long)]
18+
/// The container image to spawn, otherwise one will be built
19+
testimage: Option<String>,
20+
},
21+
CreateQcow2 {
22+
/// Input container image
23+
container: String,
24+
/// Write disk to this path
25+
disk: Utf8PathBuf,
26+
},
27+
}
28+
29+
struct TestContext {
30+
sh: xshell::Shell,
31+
targetdir: Utf8PathBuf,
32+
}
33+
34+
fn image_digest(sh: &Shell, cimage: &str) -> Result<String> {
35+
let key = "{{ .Digest }}";
36+
let r = cmd!(sh, "podman inspect --type image --format {key} {cimage}").read()?;
37+
Ok(r)
38+
}
39+
40+
fn builder_from_image(sh: &Shell, cimage: &str) -> Result<String> {
41+
let mut inspect: serde_json::Value =
42+
serde_json::from_str(&cmd!(sh, "podman inspect --type image {cimage}").read()?)?;
43+
let inspect = inspect
44+
.as_array_mut()
45+
.and_then(|v| v.pop())
46+
.ok_or_else(|| anyhow::anyhow!("Failed to parse inspect output"))?;
47+
let config = inspect
48+
.get("Config")
49+
.ok_or_else(|| anyhow::anyhow!("Missing config"))?;
50+
let config: oci_spec::image::Config =
51+
serde_json::from_value(config.clone()).context("Parsing config")?;
52+
let builder = config
53+
.labels()
54+
.as_ref()
55+
.and_then(|l| l.get(BUILDER_ANNOTATION))
56+
.ok_or_else(|| anyhow::anyhow!("Missing {BUILDER_ANNOTATION}"))?;
57+
Ok(builder.to_owned())
58+
}
59+
60+
#[context("Running bootc-image-builder")]
61+
fn run_bib(sh: &Shell, cimage: &str, tmpdir: &Utf8Path, diskpath: &Utf8Path) -> Result<()> {
62+
let diskpath: Utf8PathBuf = sh.current_dir().join(diskpath).try_into()?;
63+
let digest = image_digest(sh, cimage)?;
64+
println!("{cimage} digest={digest}");
65+
if diskpath.try_exists()? {
66+
let mut buf = [0u8; 2048];
67+
if rustix::fs::getxattr(diskpath.as_std_path(), IMAGEID_XATTR, &mut buf)
68+
.context("Reading xattr")
69+
.is_ok()
70+
{
71+
let buf = String::from_utf8_lossy(&buf);
72+
if &*buf == digest.as_str() {
73+
println!("Existing disk {diskpath} matches container digest {digest}");
74+
return Ok(());
75+
} else {
76+
println!("Cache miss; previous digest={buf}");
77+
}
78+
}
79+
}
80+
let builder = if let Ok(b) = std::env::var("BOOTC_BUILDER") {
81+
b
82+
} else {
83+
builder_from_image(sh, cimage)?
84+
};
85+
let _g = sh.push_dir(tmpdir);
86+
let bibwork = "bib-work";
87+
sh.remove_path(bibwork)?;
88+
sh.create_dir(bibwork)?;
89+
let _g = sh.push_dir(bibwork);
90+
let pwd = sh.current_dir();
91+
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()?;
92+
let tmp_disk: Utf8PathBuf = sh
93+
.current_dir()
94+
.join("qcow2/disk.qcow2")
95+
.try_into()
96+
.unwrap();
97+
rustix::fs::setxattr(
98+
tmp_disk.as_std_path(),
99+
IMAGEID_XATTR,
100+
digest.as_bytes(),
101+
rustix::fs::XattrFlags::empty(),
102+
)
103+
.context("Setting xattr")?;
104+
cmd!(sh, "mv -Tf {tmp_disk} {diskpath}").run()?;
105+
cmd!(sh, "rm -rf {bibwork}").run()?;
106+
Ok(())
107+
}
108+
109+
/// Given the input container image reference, create a disk
110+
/// image in the target directory.
111+
#[context("Creating disk")]
112+
fn create_disk(ctx: &TestContext, cimage: &str) -> Result<Utf8PathBuf> {
113+
let sh = &ctx.sh;
114+
let targetdir = ctx.targetdir.as_path();
115+
let _targetdir_guard = sh.push_dir(targetdir);
116+
sh.create_dir(TESTVMDIR)?;
117+
let output_disk: Utf8PathBuf = sh
118+
.current_dir()
119+
.join(TESTVMDIR)
120+
.join(DISK_CACHE)
121+
.try_into()
122+
.unwrap();
123+
124+
let bibwork = "bib-work";
125+
sh.remove_path(bibwork)?;
126+
sh.create_dir(bibwork)?;
127+
128+
run_bib(sh, cimage, bibwork.into(), &output_disk)?;
129+
130+
Ok(output_disk)
131+
}
132+
133+
pub(crate) fn run(opt: Opt) -> Result<()> {
134+
let ctx = &{
135+
let sh = xshell::Shell::new()?;
136+
let mut targetdir: Utf8PathBuf = cmd!(sh, "git rev-parse --show-toplevel").read()?.into();
137+
targetdir.push("target");
138+
TestContext { targetdir, sh }
139+
};
140+
match opt {
141+
Opt::PrepareTmt { mut testimage } => {
142+
let testimage = if let Some(i) = testimage.take() {
143+
i
144+
} else {
145+
cmd!(
146+
&ctx.sh,
147+
"podman build --build-arg=variant=tmt -t {TEST_IMAGE} -f hack/Containerfile ."
148+
)
149+
.run()?;
150+
TEST_IMAGE.to_string()
151+
};
152+
153+
let disk = create_disk(ctx, &testimage)?;
154+
println!("Created: {disk}");
155+
Ok(())
156+
}
157+
Opt::CreateQcow2 { container, disk } => {
158+
let g = ctx.sh.push_dir(&ctx.targetdir);
159+
ctx.sh.remove_path("tmp")?;
160+
ctx.sh.create_dir("tmp")?;
161+
drop(g);
162+
run_bib(&ctx.sh, &container, "tmp".into(), &disk)
163+
}
164+
}
165+
}

tests-integration/src/tests-integration.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use clap::Parser;
1010
mod container;
1111
mod hostpriv;
1212
mod install;
13+
mod runvm;
1314
mod selinux;
1415

1516
#[derive(Debug, Parser)]
@@ -32,6 +33,8 @@ pub(crate) enum Opt {
3233
#[clap(flatten)]
3334
testargs: libtest_mimic::Arguments,
3435
},
36+
#[clap(subcommand)]
37+
RunVM(runvm::Opt),
3538
/// Extra helper utility to verify SELinux label presence
3639
#[clap(name = "verify-selinux")]
3740
VerifySELinux {
@@ -48,6 +51,7 @@ fn main() {
4851
Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs),
4952
Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs),
5053
Opt::Container { testargs } => container::run(testargs),
54+
Opt::RunVM(opts) => runvm::run(opts),
5155
Opt::VerifySELinux { rootfs, warn } => {
5256
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority()).unwrap();
5357
let mut path = PathBuf::from(".");

tests/integration/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Integration test includes two scenarios, `RPM build` and `bootc install/upgrade`
1414
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
1515
```
1616

17-
#### Run Integartion Test
17+
#### Run Integration Test
1818

1919
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.
2020

xtask/src/xtask.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
2323
("package", package),
2424
("package-srpm", package_srpm),
2525
("custom-lints", custom_lints),
26+
("test-tmt", test_tmt),
2627
];
2728

2829
fn try_main() -> Result<()> {
@@ -142,6 +143,13 @@ fn man2markdown(sh: &Shell) -> Result<()> {
142143
Ok(())
143144
}
144145

146+
#[context("test-integration")]
147+
fn test_tmt(sh: &Shell) -> Result<()> {
148+
cmd!(sh, "cargo run -p tests-integration run-vm prepare-tmt").run()?;
149+
cmd!(sh, "tmt run plans -n integration").run()?;
150+
Ok(())
151+
}
152+
145153
/// Return a string formatted version of the git commit timestamp, up to the minute
146154
/// but not second because, well, we're not going to build more than once a second.
147155
#[context("Finding git timestamp")]

0 commit comments

Comments
 (0)