Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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()),
    )
}