bench-scenarios/examples/art_probe.rs
//! Rust instrumentation probe for Artifactory OSS container startup.
//! Pivoted to Artifactory **6.x** (single-Tomcat on 8081 — no Access/router/keys
//! microservices that make 7.x OSS un-bootable in a throwaway container). Keeps the
//! live container-log streaming and the readiness/auth/create-repo timeline.
//!
//! Run: `cargo run -p holger-bench-scenarios --example art_probe --features artifactory`
use std::time::{Duration, Instant};
use testcontainers::{
core::{ContainerPort, WaitFor},
runners::AsyncRunner,
GenericImage,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
const IMAGE: &str = "releases-docker.jfrog.io/jfrog/artifactory-oss";
const TAG: &str = "6.23.13"; // last 6.x OSS — single Tomcat, admin/password, no keys
#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
let image = GenericImage::new(IMAGE, TAG)
.with_exposed_port(ContainerPort::Tcp(8081))
.with_wait_for(WaitFor::message_on_stdout("Artifactory"));
eprintln!("[probe] starting {IMAGE}:{TAG} …");
let c = image.start().await?;
// PERFECT INSTRUMENTATION: stream the container's combined logs to a host file.
let log_path = "/mnt/work4t/bench-scratch/art_container_live.log";
{
let mut out = BufReader::new(c.stdout(true)).lines();
let mut err = BufReader::new(c.stderr(true)).lines();
let p = log_path.to_string();
tokio::spawn(async move {
if let Ok(mut f) = tokio::fs::File::create(&p).await {
loop {
tokio::select! {
Ok(Some(l)) = out.next_line() => { let _ = f.write_all(format!("{l}\n").as_bytes()).await; }
Ok(Some(l)) = err.next_line() => { let _ = f.write_all(format!("ERR {l}\n").as_bytes()).await; }
else => break,
}
let _ = f.flush().await;
}
}
});
}
eprintln!("[probe] streaming container logs -> {log_path}");
let port = c.get_host_port_ipv4(ContainerPort::Tcp(8081)).await?;
let base = format!("http://127.0.0.1:{port}");
eprintln!("[probe] artifactory 8081 -> {base}");
let client = tls_roots(reqwest::Client::builder())
.timeout(Duration::from_secs(5))
.build()?;
let code = |r: Result<reqwest::Response, _>| r.map(|x| x.status().as_u16()).unwrap_or(0);
let start = Instant::now();
while start.elapsed() < Duration::from_secs(240) {
let t = start.elapsed().as_secs();
let authed = code(
client
.get(format!("{base}/artifactory/api/system/ping"))
.basic_auth("admin", Some("password"))
.send()
.await,
);
eprintln!("[{t:>3}s] authed_ping={authed}");
if authed == 200 {
eprintln!("[probe] *** READY at {t}s — trying create-repo variants ***");
// Try several body/header variants to find what 6.x accepts for a generic local repo.
let variants: Vec<(&str, reqwest::header::HeaderValue, serde_json::Value)> = vec![
("json+key+packageType", "application/json".parse().unwrap(),
serde_json::json!({"key":"probe-raw","rclass":"local","packageType":"generic"})),
("json+no-key", "application/json".parse().unwrap(),
serde_json::json!({"rclass":"local","packageType":"generic"})),
("json+layout", "application/json".parse().unwrap(),
serde_json::json!({"rclass":"local","packageType":"generic","repoLayoutRef":"simple-default"})),
("jfrog-mediatype", "application/vnd.org.jfrog.artifactory.repositories.LocalRepositoryConfiguration+json".parse().unwrap(),
serde_json::json!({"key":"probe-raw","rclass":"local","packageType":"generic"})),
];
for (name, ct, b) in &variants {
let resp = client
.put(format!("{base}/artifactory/api/repositories/probe-raw"))
.basic_auth("admin", Some("password"))
.header(reqwest::header::CONTENT_TYPE, ct.clone())
.body(serde_json::to_string(b).unwrap())
.send()
.await;
match resp {
Ok(r) => {
let st = r.status().as_u16();
let txt = r.text().await.unwrap_or_default();
eprintln!(
"[create-repo:{name}] status={st} body={}",
txt.chars().take(160).collect::<String>()
);
}
Err(e) => eprintln!("[create-repo:{name}] err={e}"),
}
// delete between attempts so each is a fresh create
let _ = client
.delete(format!("{base}/artifactory/api/repositories/probe-raw"))
.basic_auth("admin", Some("password"))
.send()
.await;
}
break;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
eprintln!("[probe] done");
Ok(())
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}