Skip to content

Commit 272875a

Browse files
committed
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>
1 parent d65013c commit 272875a

File tree

7 files changed

+205
-92
lines changed

7 files changed

+205
-92
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,6 @@ jobs:
6868
with:
6969
name: bootc.tar.zst
7070
path: target/bootc.tar.zst
71-
build-c9s:
72-
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
73-
runs-on: ubuntu-latest
74-
container: quay.io/centos/centos:stream9
75-
steps:
76-
- run: dnf -y install git-core
77-
- uses: actions/checkout@v4
78-
- name: Install deps
79-
run: ./ci/installdeps.sh
80-
- name: Cache Dependencies
81-
uses: Swatinem/rust-cache@v2
82-
with:
83-
key: "build-c9s"
84-
- name: Build
85-
run: make test-bin-archive
86-
- name: Upload binary
87-
uses: actions/upload-artifact@v4
88-
with:
89-
name: bootc-c9s.tar.zst
90-
path: target/bootc.tar.zst
9171
cargo-deny:
9272
runs-on: ubuntu-latest
9373
steps:
@@ -127,78 +107,21 @@ jobs:
127107
run: sudo tar -C / -xvf bootc.tar.zst
128108
- name: Integration tests
129109
run: bootc internal-tests run-container-integration
130-
privtest-alongside:
110+
install-tests:
131111
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
132-
name: "Test install-alongside"
133-
needs: [build-c9s]
134-
runs-on: ubuntu-latest
112+
name: "Test install"
113+
# For a not-ancient podman
114+
runs-on: ubuntu-24.04
135115
steps:
116+
- name: Checkout repository
117+
uses: actions/checkout@v4
136118
- name: Ensure host skopeo is disabled
137119
run: sudo rm -f /bin/skopeo /usr/bin/skopeo
138-
- name: Download
139-
uses: actions/download-artifact@v4
140-
with:
141-
name: bootc-c9s.tar.zst
142-
- name: Install
143-
run: tar -xvf bootc.tar.zst
144-
- name: Integration tests
145-
run: |
146-
set -xeuo pipefail
147-
image=quay.io/centos-bootc/centos-bootc-dev:stream9
148-
echo 'ssh-ed25519 ABC0123 testcase@example.com' > test_authorized_keys
149-
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 \
150-
${image} bootc install to-filesystem --acknowledge-destructive \
151-
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
152-
ls -al /boot/loader/
153-
sudo grep foo=bar /boot/loader/entries/*.conf
154-
grep authorized_keys /ostree/deploy/default/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf
155-
# TODO fix https://github.com/containers/bootc/pull/137
156-
sudo chattr -i /ostree/deploy/default/deploy/*
157-
sudo rm /ostree/deploy/default -rf
158-
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 \
159-
${image} bootc install to-existing-root --acknowledge-destructive
160-
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
161-
install-to-existing-root:
162-
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
163-
name: "Test install-to-existing-root"
164-
needs: [build-c9s]
165-
runs-on: ubuntu-latest
166-
steps:
167-
- name: Download
168-
uses: actions/download-artifact@v4
169-
with:
170-
name: bootc-c9s.tar.zst
171-
- name: Install
172-
run: tar -xvf bootc.tar.zst
173-
- name: Integration tests
174-
run: |
175-
set -xeuo pipefail
176-
# We should be able to install to-existing-root with no install config,
177-
# so we bind mount an empty directory over /usr/lib/bootc/install.
178-
empty=$(mktemp -d)
179-
image=quay.io/centos-bootc/centos-bootc-dev:stream9
180-
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 \
181-
${image} bootc install to-existing-root
182-
install-to-loopback:
183-
if: ${{ !contains(github.event.pull_request.labels.*.name, 'control/skip-ci') }}
184-
name: "Test install to-disk --via-loopback"
185-
needs: [build-c9s]
186-
runs-on: ubuntu-latest
187-
steps:
188-
- name: Download
189-
uses: actions/download-artifact@v4
190-
with:
191-
name: bootc-c9s.tar.zst
192-
- name: Install
193-
run: tar -xvf bootc.tar.zst
194120
- name: Integration tests
195121
run: |
196-
set -xeuo pipefail
197-
image=quay.io/centos-bootc/centos-bootc-dev:stream9
198-
tmpdisk=$(mktemp -p /var/tmp)
199-
truncate -s 20G ${tmpdisk}
200-
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 \
201-
-v ${tmpdisk}:/disk ${image} bootc install to-disk --via-loopback /disk
122+
set -xeu
123+
sudo podman build -t localhost/bootc -f hack/Containerfile .
124+
cargo run -p tests-integration run-install-tests localhost/bootc
202125
docs:
203126
if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }}
204127
runs-on: ubuntu-latest

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["cli", "lib", "xtask"]
2+
members = ["cli", "lib", "xtask", "tests-integration"]
33
resolver = "2"
44

55
[profile.dev]

hack/Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ WORKDIR /build
77
RUN mkdir -p /build/target/dev-rootfs # This can hold arbitrary extra content
88
# See https://www.reddit.com/r/rust/comments/126xeyx/exploring_the_problem_of_faster_cargo_docker/
99
# We aren't using the full recommendations there, just the simple bits.
10-
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
10+
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
1111

1212
FROM quay.io/centos-bootc/centos-bootc:stream9
1313
COPY --from=build /out/bootc.tar.zst /tmp

lib/src/docgen.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ pub fn generate_manpages(directory: &Utf8Path) -> Result<()> {
1515
fn generate_one(directory: &Utf8Path, cmd: Command) -> Result<()> {
1616
let version = env!("CARGO_PKG_VERSION");
1717
let name = cmd.get_name();
18-
let bin_name = cmd.get_bin_name()
19-
.unwrap_or_else(|| name);
18+
let bin_name = cmd.get_bin_name().unwrap_or_else(|| name);
2019
let path = directory.join(format!("{name}.8"));
2120
println!("Generating {path}...");
2221

@@ -37,12 +36,13 @@ fn generate_one(directory: &Utf8Path, cmd: Command) -> Result<()> {
3736

3837
for subcmd in cmd.get_subcommands().filter(|c| !c.is_hide_set()) {
3938
let subname = format!("{}-{}", name, subcmd.get_name());
40-
let bin_name = format!("{} {}", bin_name, subcmd.get_name());
39+
let bin_name = format!("{} {}", bin_name, subcmd.get_name());
4140
// SAFETY: Latest clap 4 requires names are &'static - this is
4241
// not long-running production code, so we just leak the names here.
4342
let subname = &*std::boxed::Box::leak(subname.into_boxed_str());
4443
let bin_name = &*std::boxed::Box::leak(bin_name.into_boxed_str());
45-
let subcmd = subcmd.clone()
44+
let subcmd = subcmd
45+
.clone()
4646
.name(subname)
4747
.alias(subname)
4848
.bin_name(bin_name)

tests-integration/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Our integration tests
2+
[package]
3+
name = "tests-integration"
4+
version = "0.1.0"
5+
license = "MIT OR Apache-2.0"
6+
edition = "2021"
7+
publish = false
8+
9+
[[bin]]
10+
name = "tests-integration"
11+
path = "src/tests-integration.rs"
12+
13+
[dependencies]
14+
anyhow = "1.0.82"
15+
camino = "1.1.6"
16+
cap-std-ext = "4"
17+
clap = { version= "4.5.4", features = ["derive","cargo"] }
18+
fn-error-context = "0.2.1"
19+
tempfile = "3.10.1"
20+
xshell = { version = "0.2.6" }
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use std::os::fd::AsRawFd;
2+
use std::path::Path;
3+
4+
use anyhow::Result;
5+
use cap_std_ext::cap_std;
6+
use cap_std_ext::cap_std::fs::Dir;
7+
use clap::Parser;
8+
use fn_error_context::context;
9+
10+
use xshell::{cmd, Shell};
11+
12+
#[derive(Debug, Parser, PartialEq, Eq)]
13+
#[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")]
14+
pub(crate) enum Opt {
15+
RunInstallTests {
16+
/// Source container image reference
17+
image: String,
18+
},
19+
}
20+
21+
fn main() {
22+
if let Err(e) = try_main() {
23+
eprintln!("error: {e:?}");
24+
std::process::exit(1);
25+
}
26+
}
27+
28+
fn try_main() -> Result<()> {
29+
let opt = Opt::parse();
30+
match opt {
31+
Opt::RunInstallTests { image } => run_install_tests(image.as_str()),
32+
}
33+
}
34+
35+
// Clear out and delete any ostree roots
36+
fn reset_root(sh: &Shell) -> Result<()> {
37+
// TODO fix https://github.com/containers/bootc/pull/137
38+
if !Path::new("/ostree/deploy/default").exists() {
39+
return Ok(());
40+
}
41+
cmd!(
42+
sh,
43+
"sudo /bin/sh -c 'chattr -i /ostree/deploy/default/deploy/*'"
44+
)
45+
.run()?;
46+
cmd!(sh, "sudo rm /ostree/deploy/default -rf").run()?;
47+
Ok(())
48+
}
49+
50+
fn find_deployment_root() -> Result<Dir> {
51+
let _stateroot = "default";
52+
let d = Dir::open_ambient_dir(
53+
"/ostree/deploy/default/deploy",
54+
cap_std::ambient_authority(),
55+
)?;
56+
for child in d.entries()? {
57+
let child = child?;
58+
if !child.file_type()?.is_dir() {
59+
continue;
60+
}
61+
return Ok(child.open_dir()?);
62+
}
63+
anyhow::bail!("Failed to find deployment root")
64+
}
65+
66+
fn run_test<F>(sh: &Shell, desc: &str, f: F) -> Result<()>
67+
where
68+
F: FnOnce(&Shell) -> Result<()>,
69+
{
70+
reset_root(sh)?;
71+
println!("test: {desc}");
72+
match f(sh) {
73+
Ok(r) => {
74+
println!("ok: {desc}");
75+
Ok(r)
76+
}
77+
Err(e) => {
78+
eprintln!("FAILED: {desc}");
79+
Err(e)
80+
}
81+
}
82+
}
83+
84+
#[context("Install tests")]
85+
fn run_install_tests(image: &str) -> Result<()> {
86+
let sh = &xshell::Shell::new()?;
87+
88+
let base_args = [
89+
"podman",
90+
"run",
91+
"--rm",
92+
"--privileged",
93+
"-v",
94+
"/dev:/dev",
95+
"-v",
96+
"/var/lib/containers:/var/lib/containers",
97+
"--pid=host",
98+
"--security-opt",
99+
"label=disable",
100+
];
101+
let image_install = [image, "bootc", "install"];
102+
let target_args = ["-v", "/:/target"];
103+
// We always need this as we assume we're operating on a local image
104+
let generic_inst_args = ["--skip-fetch-check"];
105+
106+
run_test(sh, "loopback install", |sh| {
107+
let size = 10 * 1000 * 1000 * 1000;
108+
let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?;
109+
tmpdisk.as_file_mut().set_len(size)?;
110+
let tmpdisk = tmpdisk.into_temp_path();
111+
let tmpdisk = tmpdisk.to_str().unwrap();
112+
cmd!(sh, "sudo {base_args...} -v {tmpdisk}:/disk {image_install...} to-disk --via-loopback {generic_inst_args...} /disk").run()?;
113+
Ok(())
114+
})?;
115+
116+
run_test(
117+
sh,
118+
"replace=alongside with ssh keys and a karg, and SELinux disabled",
119+
|sh| {
120+
let tmpd = &sh.create_temp_dir()?;
121+
let tmp_keys = tmpd.path().join("test_authorized_keys");
122+
let tmp_keys = tmp_keys.to_str().unwrap();
123+
std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 testcase@example.com")?;
124+
cmd!(sh, "sudo {base_args...} {target_args...} -v {tmp_keys}:/test_authorized_keys {image_install...} to-filesystem {generic_inst_args...} --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target").run()?;
125+
126+
cmd!(
127+
sh,
128+
"sudo /bin/sh -c 'grep foo=bar /boot/loader/entries/*.conf'"
129+
)
130+
.run()?;
131+
let deployment = &find_deployment_root()?;
132+
let cwd = sh.push_dir(format!("/proc/self/fd/{}", deployment.as_raw_fd()));
133+
cmd!(
134+
sh,
135+
"grep authorized_keys etc/tmpfiles.d/bootc-root-ssh.conf"
136+
)
137+
.run()?;
138+
drop(cwd);
139+
Ok(())
140+
},
141+
)?;
142+
143+
run_test(sh, "Install and verify selinux state", |sh| {
144+
cmd!(sh, "sudo {base_args...} {target_args...} {image_install...} to-existing-root --acknowledge-destructive {generic_inst_args...}").run()?;
145+
cmd!(sh, "sudo podman run --rm --privileged --pid=host {target_args...} {image} bootc internal-tests verify-selinux /target/ostree --warn").run()?;
146+
Ok(())
147+
})?;
148+
149+
run_test(sh, "without an install config", |sh| {
150+
let empty = sh.create_temp_dir()?;
151+
let empty = empty.path().to_str().unwrap();
152+
cmd!(sh, "sudo {base_args...} {target_args...} -v {empty}:/usr/lib/bootc/install {image_install...} to-existing-root {generic_inst_args...}").run()?;
153+
Ok(())
154+
})?;
155+
156+
Ok(())
157+
}

0 commit comments

Comments
 (0)