server/lib/tests/pull_through_apk.rs
//! **APK pull-through** (lane T8, `server/lib/src/pull_intake/apk.rs`): a Wolfi
//! (or Chainguard, or Alpine) repository as a `kind: apk` remote source.
//!
//! One real Holger in front of a local FAKE upstream that serves REAL Wolfi bytes
//! (`provenance/tests/fixtures/wolfi/`: the `bootstrap/stage3` APKINDEX, signed
//! `.SIGN.RSA.wolfi-signing.rsa.pub`, and `libffi-3.4.2-r0.apk` from it).
//!
//! What the tests measure: `GET <source>/<arch>/-/pull/<name>` is the client's "not
//! yet" (`503` + `Retry-After`); the upstream index is held against the PINNED key
//! before a package is read from it, and every package against the signed index
//! (control hash, size, data hash) before it reaches the chain; the package lands
//! INGESTED as `pull-through:<source>`, its provenance verified again at SCANNED, and
//! waits at POLICY_CHECKED for a person; Holger's own `APKINDEX.tar.gz`, signed with
//! Holger's key, lists only AVAILABLE packages; after the approval the `.apk` comes
//! from the SEALED store.
//!
//! `real_client_apk_add_through_holger` is the measurement with the REAL client: the
//! same Holger in front of the REAL Wolfi repository (`https://packages.wolfi.dev/os`,
//! its key pinned from the fixture), and `apk add` in `cgr.dev/chainguard/wolfi-base`
//! whose only repository is Holger and whose only key is Holger's. It is `#[ignore]`d
//! for a plain `cargo test` (it needs podman, the image and the network) and reached by
//! the demo's row (`cargo xtask demo tests`). The client reaches Holger's loopback port
//! through its own pasta network (`-T <port>`), never `--network host`. No podman FAILS
//! by name; it never skips.
use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use holger_provenance::apk::{verify_index, ApkKey};
use holger_server_lib::promote::intake::{
ArtifactState, EvidenceKind, EvidenceVerdict, IntakeChain, TransitionRequest,
};
use holger_server_lib::pull_intake::apk;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use traits::test_support::hold_port;
const FX: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../provenance/tests/fixtures"
);
const WOLFI_KEY: &str = "wolfi-signing.rsa.pub";
const LIBFFI: &str = "libffi-3.4.2-r0.apk";
fn fixture(p: &str) -> Vec<u8> {
let b = std::fs::read(Path::new(FX).join(p)).unwrap_or_else(|e| panic!("{p}: {e}"));
assert!(
!b.starts_with(b"version https://git-lfs"),
"{p} is an LFS pointer: run `git lfs pull`"
);
b
}
fn token(who: &str) -> String {
format!("{who}-token-of-sufficient-length-for-holger")
}
// ── the fake upstream ───────────────────────────────────────────────────────
struct Upstream {
base: String,
hits: Arc<Mutex<BTreeMap<String, usize>>>,
/// Paths the fake answers late: it waits this long before it counts the hit and replies.
slow: Arc<Mutex<BTreeMap<String, Duration>>>,
}
impl Upstream {
fn hits(&self, path: &str) -> usize {
self.hits.lock().unwrap().get(path).copied().unwrap_or(0)
}
/// Answer `path` only after `by`, the way a loaded box gets to it later: the request
/// is counted when the wait is over.
fn slow_down(&self, path: &str, by: Duration) {
self.slow.lock().unwrap().insert(path.to_string(), by);
}
}
struct Reply {
status: u16,
ctype: String,
body: Vec<u8>,
}
fn reply(status: u16, ctype: &str, body: Vec<u8>) -> Reply {
Reply {
status,
ctype: ctype.into(),
body,
}
}
fn flipped(mut b: Vec<u8>, from_end: usize) -> Vec<u8> {
let n = b.len() - from_end;
b[n] ^= 0x01;
b
}
/// `/<repo>/x86_64/…`: `wolfi` as published, `tampered` with a flipped byte in the
/// package's data, `forged` with a flipped byte in the signed index.
fn route(_base: &str, path: &str, _auth: Option<&str>) -> Reply {
let index = fixture("wolfi/stage3/APKINDEX.tar.gz");
let apk = fixture(&format!("wolfi/stage3/{LIBFFI}"));
match path {
"/wolfi/x86_64/APKINDEX.tar.gz"
| "/tampered/x86_64/APKINDEX.tar.gz"
| "/nokey/x86_64/APKINDEX.tar.gz" => reply(200, "application/gzip", index),
"/forged/x86_64/APKINDEX.tar.gz" => reply(200, "application/gzip", flipped(index, 100)),
p if p == format!("/wolfi/x86_64/{LIBFFI}")
|| p == format!("/forged/x86_64/{LIBFFI}")
|| p == format!("/nokey/x86_64/{LIBFFI}") =>
{
reply(200, "application/octet-stream", apk)
}
p if p == format!("/tampered/x86_64/{LIBFFI}") => {
reply(200, "application/octet-stream", flipped(apk, 20))
}
_ => reply(404, "text/plain", b"not found".to_vec()),
}
}
fn serve_one(
mut s: TcpStream,
base: &str,
hits: &Mutex<BTreeMap<String, usize>>,
slow: &Mutex<BTreeMap<String, Duration>>,
) {
let _ = s.set_read_timeout(Some(Duration::from_secs(10)));
let mut buf = Vec::new();
let mut chunk = [0u8; 4096];
while !buf.windows(4).any(|w| w == b"\r\n\r\n") {
match s.read(&mut chunk) {
Ok(0) | Err(_) => return,
Ok(n) => buf.extend_from_slice(&chunk[..n]),
}
}
let head = String::from_utf8_lossy(&buf).to_string();
let mut lines = head.lines();
let path = lines
.next()
.unwrap_or("")
.split_whitespace()
.nth(1)
.unwrap_or("/")
.to_string();
let auth = lines
.find_map(|l| {
l.strip_prefix("authorization: ")
.or_else(|| l.strip_prefix("Authorization: "))
})
.map(str::to_string);
let bare = path.split('?').next().unwrap_or("").to_string();
let late = slow.lock().unwrap().get(&bare).copied();
if let Some(by) = late {
std::thread::sleep(by);
}
*hits.lock().unwrap().entry(bare.clone()).or_default() += 1;
let r = route(base, &bare, auth.as_deref());
let out = format!(
"HTTP/1.1 {} X\r\nContent-Length: {}\r\nContent-Type: {}\r\nConnection: close\r\n\r\n",
r.status,
r.body.len(),
r.ctype
);
let _ = s.write_all(out.as_bytes());
let _ = s.write_all(&r.body);
}
fn upstream() -> Upstream {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", l.local_addr().unwrap());
let hits: Arc<Mutex<BTreeMap<String, usize>>> = Arc::default();
let slow: Arc<Mutex<BTreeMap<String, Duration>>> = Arc::default();
let (b2, h2, s2) = (base.clone(), Arc::clone(&hits), Arc::clone(&slow));
std::thread::spawn(move || {
for s in l.incoming().flatten() {
let (b3, h3, s3) = (b2.clone(), Arc::clone(&h2), Arc::clone(&s2));
std::thread::spawn(move || serve_one(s, &b3, &h3, &s3));
}
});
Upstream { base, hits, slow }
}
// ── one Holger ──────────────────────────────────────────────────────────────
struct Running {
holger: Holger,
_rt: tokio::runtime::Runtime,
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
http: String,
}
impl Running {
fn chain(&self) -> Arc<IntakeChain> {
self.holger.intake().expect("the intake chain")
}
}
/// `rules.apk_keys`: Wolfi's key, pinned.
fn wolfi_pinned() -> String {
format!(r#"(name: "{WOLFI_KEY}", pem_file: Some("{FX}/wolfi/wolfi-signing.rsa.pub")),"#)
}
fn start(sources: &str) -> Running {
start_with(sources, &wolfi_pinned())
}
fn start_with(sources: &str, keys: &str) -> Running {
let mut last = String::new();
for _ in 0..8 {
let (grpc, http) = (hold_port(), hold_port());
match try_start(grpc, http, sources, keys) {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("server never started; last error: {last}");
}
/// The four intake repositories, the chain, and `pull_through:` with the apk source.
fn try_start(grpc: u16, http: u16, sources: &str, keys: &str) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let d = data.path().display().to_string();
let repo = |name: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_data_dir: Some("{d}/{name}")),"#
)
};
let methods: Vec<String> = ["ida", "bob"]
.iter()
.map(|p| {
format!(
r#"StaticToken(subject: "{p}", token: Some("{}"))"#,
token(p)
)
})
.collect();
let ron = format!(
r#"(
repositories: [ {incoming} {quarantine} {approved} {production} ],
exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")) ],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
audit: (dir: "{d}/audit", enabled: true),
// Open reads by declaration: an estate build reads Gated by default.
access: (reads: Open, front: Open),
auth: Some((
methods: [ {methods} ],
duties: {{ "ida": [Importer], "bob": [SecurityApprover] }},
)),
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"), (name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"), (name: "production", repo: "production"),
],
intake: Some((
incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production",
rules: (sandbox: off),
approvals: 1,
scan: None,
)),
)),
pull_through: Some((
sources: [ {sources} ],
rules: (require_provenance: true, apk_keys: [ {keys} ]),
retry_after_secs: Some(1),
)),
)"#,
methods = methods.join(", "),
incoming = repo("incoming"),
quarantine = repo("quarantine"),
approved = repo("approved"),
production = repo("production"),
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
holger
.instantiate_backends()
.map_err(|e| format!("backends: {e}"))?;
wire_holger(&mut holger).map_err(|e| format!("wire: {e:#}"))?;
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.unwrap();
{
let _in = rt.enter();
holger.start().map_err(|e| format!("start: {e:#}"))?;
}
let r = Running {
holger,
_rt: rt,
_data: data,
_cfg: cfg,
http: format!("127.0.0.1:{http}"),
};
for _ in 0..200 {
if get(&r.http, "/healthz", &[]).status == 200 {
return Ok(r);
}
std::thread::sleep(Duration::from_millis(20));
}
Err("never healthy".into())
}
struct Got {
status: u16,
headers: BTreeMap<String, String>,
body: Vec<u8>,
}
impl Got {
fn text(&self) -> String {
String::from_utf8_lossy(&self.body).to_string()
}
}
fn get(addr: &str, path: &str, headers: &[(&str, &str)]) -> Got {
let Ok(mut s) = TcpStream::connect(addr) else {
return Got {
status: 0,
headers: BTreeMap::new(),
body: Vec::new(),
};
};
let mut req = format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n");
for (k, v) in headers {
req.push_str(&format!("{k}: {v}\r\n"));
}
req.push_str("\r\n");
s.write_all(req.as_bytes()).unwrap();
let mut all = Vec::new();
let _ = s.read_to_end(&mut all);
let split = all
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
.unwrap_or(all.len());
let head = String::from_utf8_lossy(&all[..split]).to_string();
let status = head
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let headers = head
.lines()
.skip(1)
.filter_map(|l| {
l.split_once(':')
.map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string()))
})
.collect();
Got {
status,
headers,
body: all[split..].to_vec(),
}
}
fn wait_state(chain: &IntakeChain, id: &traits::ArtifactId, want: ArtifactState) {
let t = Instant::now();
while t.elapsed() < Duration::from_secs(60) {
if chain.state(id) == want {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
let jobs = chain.pull().map(|p| p.debug_records()).unwrap_or_default();
panic!(
"{} never reached {want}; it stands at {} ({:#?}); pull records: {jobs}",
id.name,
chain.state(id),
chain.history(id)
);
}
/// Wait (bounded, as [`wait_state`]) until the fake upstream has seen `path` at least `n` times.
fn wait_hits(up: &Upstream, path: &str, n: usize, what: &str) {
let t = Instant::now();
while t.elapsed() < Duration::from_secs(60) {
if up.hits(path) >= n {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!(
"{what}: the upstream saw {path} {} time(s) in 60 s, wanted {n}",
up.hits(path)
);
}
/// A GET once the pull job is done: while the job still runs, the answer is 503.
fn get_settled(addr: &str, path: &str) -> Got {
let t = Instant::now();
loop {
let g = get(addr, path, &[]);
if g.status != 503 || t.elapsed() > Duration::from_secs(60) {
return g;
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn approve(chain: &IntakeChain, id: &traits::ArtifactId) {
chain
.advance(
TransitionRequest::new(id.clone(), ArtifactState::Approved, "bob")
.with_reason("reviewed"),
)
.unwrap_or_else(|e| panic!("bob approves {}: {e}", id.name));
}
/// The trust the tests pin: Wolfi's signing key.
fn wolfi_pem() -> (String, Vec<u8>) {
(
WOLFI_KEY.to_string(),
fixture("wolfi/wolfi-signing.rsa.pub"),
)
}
fn source(up: &Upstream, name: &str) -> String {
format!(
r#"(name: "{name}", kind: apk, url: "{base}/{name}", gates: (provenance: Some(require(apk_keys: ["{WOLFI_KEY}"])))),"#,
base = up.base
)
}
/// Holger's signing key for a source, as a client fetches it into `/etc/apk/keys`.
fn holger_key(r: &Running, source: &str) -> ApkKey {
let name = get(
&r.http,
&format!("/production/{source}/-/apk-key-name"),
&[],
);
assert_eq!(name.status, 200, "{}", name.text());
let name = name.text().trim().to_string();
assert!(
name.starts_with("holger-") && name.ends_with(".rsa.pub"),
"{name}"
);
let pem = get(&r.http, &format!("/production/{source}/{name}"), &[]);
assert_eq!(pem.status, 200, "{}", pem.text());
ApkKey::from_pem(&name, &pem.body).expect("an RSA public key in PEM")
}
/// Holger's index for `source`, verified with Holger's key: the (name, version)s it lists.
fn listed(r: &Running, source: &str) -> Vec<(String, String)> {
let key = holger_key(r, source);
let g = get(
&r.http,
&format!("/production/{source}/x86_64/APKINDEX.tar.gz"),
&[],
);
assert_eq!(g.status, 200, "{}", g.text());
let idx = verify_index(&g.body, &[key]).expect("Holger's index verifies with Holger's key");
assert_eq!(idx.alg, "rsa-sha256");
idx.entries
.into_iter()
.map(|e| (e.name, e.version))
.collect()
}
#[test]
fn an_apk_is_pulled_verified_held_and_listed_in_holgers_signed_index_only_once_available() {
let up = upstream();
// The dependency-closure walk runs on its own, after libffi's own intake, and under load
// it reaches the upstream later (ODEN-CI round 3: 1 in 10 red on oden). The fake answers
// glibc late on purpose, so the test sees that ordering on every run, not by luck.
up.slow_down(
"/wolfi/x86_64/glibc-2.36-r0.apk",
Duration::from_millis(1500),
);
let r = start(&source(&up, "wolfi"));
let chain = r.chain();
// 0. Holger's index lists nothing yet, and is signed by Holger.
assert_eq!(listed(&r, "wolfi"), []);
// 1. The client asks for libffi: not yet; a pull is on its way.
let first = get(&r.http, "/production/wolfi/x86_64/-/pull/libffi", &[]);
assert_eq!(first.status, 503, "{}", first.text());
assert!(
first.headers.contains_key("retry-after"),
"{:?}",
first.headers
);
// 2. libffi lands INGESTED as pull-through:wolfi and waits for a person, its
// provenance (the signed index) verified at SCANNED.
let id = apk::id("wolfi", "x86_64", "libffi", "3.4.2-r0");
assert_eq!(id.name, format!("wolfi/x86_64/{LIBFFI}"));
wait_state(&chain, &id, ArtifactState::PolicyChecked);
let h = chain.history(&id);
assert!(h.iter().all(|e| e.actor == "pull-through:wolfi"), "{h:#?}");
let prov = h[1]
.evidence
.iter()
.find(|e| e.kind == EvidenceKind::Provenance)
.cloned()
.expect("a provenance slot");
assert_eq!(
(prov.source.as_str(), prov.verdict),
("holger-provenance", EvidenceVerdict::Pass),
"{prov:?}"
);
assert_eq!(
h[0].sha256,
hex::encode(<sha2::Sha256 as sha2::Digest>::digest(fixture(&format!(
"wolfi/stage3/{LIBFFI}"
))))
);
// Its dependency (`so:libc.so.6`, glibc in that index) was asked for too; the
// fake does not serve it, so it is not in the chain and libffi is not held up by it.
// The walk runs on its own and may reach the upstream after libffi is PolicyChecked:
// wait for it (bounded), as for the state above.
wait_hits(
&up,
"/wolfi/x86_64/glibc-2.36-r0.apk",
1,
"the closure was walked",
);
assert!(chain
.status(&apk::id("wolfi", "x86_64", "glibc", "2.36-r0"))
.is_none());
// 3. While it waits: the pull says so, the .apk is not served, the index lists nothing.
let held = get_settled(&r.http, "/production/wolfi/x86_64/-/pull/libffi");
assert_eq!(held.status, 404, "{}", held.text());
assert!(
held.text().contains("waiting for approval"),
"{}",
held.text()
);
assert!(
held.text()
.contains(&format!("wolfi/x86_64/{LIBFFI}: POLICY_CHECKED")),
"the closure is named: {}",
held.text()
);
assert_eq!(
get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]).status,
404
);
assert_eq!(listed(&r, "wolfi"), []);
// 4. Approved: the index lists libffi with the upstream's stanza, the .apk is the
// sealed bytes, and the pull answers 200.
approve(&chain, &id);
assert_eq!(
listed(&r, "wolfi"),
[("libffi".to_string(), "3.4.2-r0".to_string())]
);
let g = get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]);
assert_eq!(g.status, 200, "{}", g.text());
assert_eq!(
g.headers.get("x-holger-intake-state").map(String::as_str),
Some("AVAILABLE")
);
assert_eq!(g.body, fixture(&format!("wolfi/stage3/{LIBFFI}")));
let ok = get(&r.http, "/production/wolfi/x86_64/-/pull/libffi", &[]);
assert_eq!(ok.status, 200, "{}", ok.text());
assert_eq!(
up.hits(&format!("/wolfi/x86_64/{LIBFFI}")),
1,
"the upstream gave the package once"
);
// The listed stanza is the upstream's, so a client holds the bytes to the same hashes.
let key = holger_key(&r, "wolfi");
let idx = verify_index(
&get(&r.http, "/production/wolfi/x86_64/APKINDEX.tar.gz", &[]).body,
&[key],
)
.unwrap();
let upstream = verify_index(
&fixture("wolfi/stage3/APKINDEX.tar.gz"),
&[ApkKey::from_pem(WOLFI_KEY, &wolfi_pem().1).unwrap()],
)
.unwrap();
let theirs = upstream
.entries
.iter()
.find(|e| e.name == "libffi")
.unwrap();
assert_eq!(idx.entries[0].stanza, theirs.stanza);
holger_provenance::apk::verify_package(&g.body, &idx)
.expect("the served .apk verifies against Holger's index");
}
#[test]
fn a_package_the_signed_index_does_not_name_byte_for_byte_is_never_ingested() {
let up = upstream();
let r = start(&source(&up, "tampered"));
let chain = r.chain();
assert_eq!(
get(&r.http, "/production/tampered/x86_64/-/pull/libffi", &[]).status,
503
);
let refused = get_settled(&r.http, "/production/tampered/x86_64/-/pull/libffi");
assert_eq!(refused.status, 403, "{}", refused.text());
assert!(
refused.text().contains("data segment"),
"{}",
refused.text()
);
assert!(
chain
.status(&apk::id("tampered", "x86_64", "libffi", "3.4.2-r0"))
.is_none(),
"never reached the chain"
);
assert_eq!(listed(&r, "tampered"), []);
}
#[test]
fn an_index_whose_signature_does_not_verify_is_refused_before_a_package_is_fetched() {
let up = upstream();
let r = start(&source(&up, "forged"));
assert_eq!(
get(&r.http, "/production/forged/x86_64/-/pull/libffi", &[]).status,
503
);
let refused = get_settled(&r.http, "/production/forged/x86_64/-/pull/libffi");
assert_eq!(refused.status, 403, "{}", refused.text());
assert!(refused.text().contains("signature"), "{}", refused.text());
assert_eq!(
up.hits(&format!("/forged/x86_64/{LIBFFI}")),
0,
"no package is read from an index that does not verify"
);
}
#[test]
fn with_no_pinned_apk_key_every_pull_is_refused_by_name() {
let up = upstream();
let r = start_with(&source(&up, "nokey"), "");
assert_eq!(
get(&r.http, "/production/nokey/x86_64/-/pull/libffi", &[]).status,
503
);
let refused = get_settled(&r.http, "/production/nokey/x86_64/-/pull/libffi");
assert_eq!(refused.status, 403, "{}", refused.text());
assert!(
refused.text().contains("no apk key is pinned"),
"{}",
refused.text()
);
assert_eq!(up.hits(&format!("/nokey/x86_64/{LIBFFI}")), 0);
}
#[test]
fn an_exact_apk_file_is_pulled_alone_and_an_unknown_name_is_404() {
let up = upstream();
let r = start(&source(&up, "wolfi"));
let chain = r.chain();
let first = get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]);
assert_eq!(first.status, 503, "{}", first.text());
let id = apk::id("wolfi", "x86_64", "libffi", "3.4.2-r0");
wait_state(&chain, &id, ArtifactState::PolicyChecked);
assert_eq!(
up.hits("/wolfi/x86_64/glibc-2.36-r0.apk"),
0,
"a file request pulls that file only"
);
assert_eq!(
get(
&r.http,
"/production/wolfi/x86_64/-/pull/no-such-package",
&[]
)
.status,
503
);
let unknown = get_settled(&r.http, "/production/wolfi/x86_64/-/pull/no-such-package");
assert_eq!(unknown.status, 404, "{}", unknown.text());
assert!(
unknown.text().contains("publishes no"),
"{}",
unknown.text()
);
assert_eq!(
get(&r.http, "/production/wolfi/x86_64/not-a-file", &[]).status,
404
);
}
// ── the real client ─────────────────────────────────────────────────────────
const CLIENT_IMAGE: &str = "cgr.dev/chainguard/wolfi-base:latest";
const WOLFI_OS: &str = "https://packages.wolfi.dev/os";
/// A closure of a few packages over wolfi-base; its binary answers `--version`.
const REAL_PKG: &str = "jq";
fn podman() -> std::path::PathBuf {
let path = std::env::var_os("PATH").unwrap_or_default();
std::env::split_paths(&path).map(|d| d.join("podman")).find(|p| p.is_file()).unwrap_or_else(|| {
panic!("podman not on PATH: this test drives the REAL apk in {CLIENT_IMAGE} and proves nothing without a container runtime; do not make it skip")
})
}
/// The image is present, or is pulled once. A failed pull is a failure.
fn ensure_image(podman: &Path, image: &str) {
let exists = std::process::Command::new(podman)
.args(["image", "exists", image])
.status()
.expect("podman runs");
if !exists.success() {
let pull = std::process::Command::new(podman)
.args(["pull", image])
.output()
.expect("podman runs");
assert!(
pull.status.success(),
"image not pullable: {image}\n{}",
String::from_utf8_lossy(&pull.stderr)
);
}
}
/// `apk` in the client image: Holger is its only repository, and (when `key` is given)
/// Holger's key its only key. Holger's loopback port is the client's own `127.0.0.1:<port>`
/// (pasta `-T`). Returns (success, stdout + stderr).
fn apk_client(
podman: &Path,
r: &Running,
source: &str,
key: Option<(&str, &Path)>,
script: &str,
) -> (bool, String) {
let port = r.http.rsplit(':').next().unwrap();
let mut cmd = std::process::Command::new(podman);
cmd.args(["run", "--rm", "--network", &format!("pasta:-T,{port}")]);
if let Some((name, file)) = key {
cmd.arg("-v")
.arg(format!("{}:/etc/apk/keys/{name}:ro", file.display()));
}
let sh = format!(
"rm -f /etc/apk/keys/wolfi-signing.rsa.pub /etc/apk/keys/chainguard-*; \
echo http://127.0.0.1:{port}/production/{source} > /etc/apk/repositories; {script}"
);
let out = cmd
.args([CLIENT_IMAGE, "sh", "-c", &sh])
.output()
.expect("podman run");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
eprintln!("--- apk ({script})\n{text}");
(out.status.success(), text)
}
/// The closure lines of a pull answer (` <coordinate>: <STATE>`).
fn closure_states(answer: &str) -> Vec<(String, String)> {
answer
.lines()
.filter_map(|l| {
l.strip_prefix(" ")?
.split_once(": ")
.map(|(n, s)| (n.to_string(), s.trim().to_string()))
})
.filter(|(n, _)| n.ends_with(".apk"))
.collect()
}
#[test]
#[ignore = "a real apk add in Wolfi against Holger; needs podman, the image and packages.wolfi.dev (cargo xtask demo tests)"]
fn real_client_apk_add_through_holger() {
let podman = podman();
ensure_image(&podman, CLIENT_IMAGE);
let src = format!(
r#"(name: "wolfi", kind: apk, url: "{WOLFI_OS}", gates: (provenance: Some(require(apk_keys: ["{WOLFI_KEY}"])))),"#
);
let r = start(&src);
let chain = r.chain();
// Holger's key, as a client puts it into /etc/apk/keys.
let key = holger_key(&r, "wolfi");
let keys = tempfile::tempdir().unwrap();
let key_file = keys.path().join(&key.name);
std::fs::write(
&key_file,
get(&r.http, &format!("/production/wolfi/{}", key.name), &[]).body,
)
.unwrap();
let keyed = Some((key.name.as_str(), key_file.as_path()));
// 1. Nothing AVAILABLE: apk trusts Holger's signed index, and it names no such package.
let (_, out) = apk_client(
&podman,
&r,
"wolfi",
keyed,
&format!("apk update 2>&1; apk add {REAL_PKG} 2>&1"),
);
assert!(
out.contains("holger wolfi x86_64: the AVAILABLE packages"),
"Holger's index was not read:\n{out}"
);
assert!(
!out.contains("BAD signature") && !out.contains("UNTRUSTED"),
"the client refused Holger's index:\n{out}"
);
assert!(
out.contains(&format!("{REAL_PKG} (no such package)")),
"before the pull apk add finds no such package:\n{out}"
);
// 2. The pull: not yet (503 + Retry-After), then waiting for a person, the closure named.
let pull = format!("/production/wolfi/x86_64/-/pull/{REAL_PKG}");
let first = get(&r.http, &pull, &[]);
assert_eq!(first.status, 503, "{}", first.text());
assert!(
first.headers.contains_key("retry-after"),
"{:?}",
first.headers
);
// 3. A person approves each package of the closure as it reaches POLICY_CHECKED,
// until the pull answers 200 and no package it brought in waits for a person.
let t = Instant::now();
let mut approved = Vec::new();
let mut last = String::new();
let done = loop {
assert!(
t.elapsed() < Duration::from_secs(600),
"the pull of {REAL_PKG} never settled; approved {approved:?}; last answer: {last}"
);
let g = get_settled(&r.http, &pull);
last = format!("{} {}", g.status, g.text());
let states = closure_states(&g.text());
// Approved is the person's word; the seal and the walk to AVAILABLE follow when
// the index (or the file) is next asked for.
let judged = |s: &str| matches!(s, "APPROVED" | "SEALED" | "PROMOTED" | "AVAILABLE");
if g.status == 200 && !states.is_empty() && states.iter().all(|(_, s)| judged(s)) {
break g.text();
}
assert!(
g.status == 404 || g.status == 200 || g.status == 503,
"the pull answered {}: {}",
g.status,
g.text()
);
if g.status == 404 {
assert!(g.text().contains("waiting for approval"), "{}", g.text());
}
for (n, s) in states.iter().filter(|(_, s)| s == "POLICY_CHECKED") {
assert!(n.starts_with("wolfi/x86_64/"), "{n} {s}");
approve(
&chain,
&traits::ArtifactId {
namespace: None,
name: n.clone(),
version: String::new(),
},
);
approved.push(n.clone());
}
std::thread::sleep(Duration::from_millis(200));
};
assert!(
approved
.iter()
.any(|n| n.starts_with(&format!("wolfi/x86_64/{REAL_PKG}-"))),
"{approved:?}"
);
for n in &approved {
let h = chain.history(&traits::ArtifactId {
namespace: None,
name: n.clone(),
version: String::new(),
});
assert_eq!(h[0].actor, "pull-through:wolfi", "{n}: {h:#?}");
let prov = h
.iter()
.flat_map(|e| e.evidence.iter())
.find(|e| e.kind == EvidenceKind::Provenance)
.cloned()
.expect("a provenance slot");
assert_eq!(prov.verdict, EvidenceVerdict::Pass, "{n}: {prov:?}");
}
eprintln!("the pull: {done}");
// 4. apk add installs it from Holger's AVAILABLE packages, and the binary runs.
let (ok, out) = apk_client(
&podman,
&r,
"wolfi",
keyed,
&format!("apk update && apk add {REAL_PKG} && {REAL_PKG} --version"),
);
assert!(
ok && out.contains("OK: "),
"apk add {REAL_PKG} from Holger:\n{out}"
);
assert!(
out.lines().any(|l| l.starts_with(&format!("{REAL_PKG}-"))),
"the binary answers --version:\n{out}"
);
// The index walked the whole approved closure on to AVAILABLE.
let after = get(&r.http, &pull, &[]);
assert_eq!(after.status, 200, "{}", after.text());
let states = closure_states(&after.text());
assert!(
states.iter().all(|(_, s)| s == "AVAILABLE"),
"{}",
after.text()
);
// 5. Without Holger's key the client refuses Holger's index.
let (_, out) = apk_client(&podman, &r, "wolfi", None, "apk update 2>&1");
assert!(
out.contains("UNTRUSTED"),
"a client without Holger's key took the index:\n{out}"
);
}