use jsonwebtoken::{EncodingKey, Header, encode}; use serde::Serialize; use std::{fs, net::TcpListener, time::Duration}; #[derive(Serialize)] struct Claims { sub: String, session_id: String, permissions: Vec, exp: usize, } fn free_port() -> u16 { TcpListener::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap() .port() } fn token(secret: &[u8], perms: &[&str]) -> String { let exp = (std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() + 60) as usize; encode( &Header::default(), &Claims { sub: "op_1".to_string(), session_id: "sess_1".to_string(), permissions: perms.iter().map(|p| (*p).to_string()).collect(), exp, }, &EncodingKey::from_secret(secret), ) .unwrap() } async fn wait_ready(url: &str) { let client = reqwest::Client::new(); let start = tokio::time::Instant::now(); loop { let ok = client .get(format!("{url}/ready")) .send() .await .map(|r| r.status().is_success()) .unwrap_or(false); if ok { return; } if start.elapsed() > Duration::from_secs(10) { panic!("control-api did not become ready"); } tokio::time::sleep(Duration::from_millis(100)).await; } } #[tokio::test] #[ignore] async fn control_plane_can_see_the_fleet_via_docker_stubs() { let enabled = std::env::var("CONTROL_TEST_DOCKER").ok(); assert_eq!(enabled.as_deref(), Some("1")); let nginx_conf = r#" server { listen 80; server_name _; location = /health { return 200 "ok\n"; } location = /ready { return 200 "ready\n"; } location = /metrics { return 200 "stub_build_info{service=\"stub\",version=\"dev\",git_sha=\"000\"} 1\n"; } } "#; let mut conf_path = std::env::temp_dir(); conf_path.push(format!( "cloudlysis-control-nginx-{}.conf", uuid::Uuid::new_v4() )); fs::write(&conf_path, nginx_conf).unwrap(); let gateway_port = free_port(); let runner_port = free_port(); let aggregate_port = free_port(); let projection_port = free_port(); async fn run_stub(name: &str, port: u16, conf: &std::path::Path) -> String { let out = tokio::process::Command::new("docker") .args(["run", "-d", "--rm"]) .args(["-p", &format!("{port}:80")]) .args([ "-v", &format!("{}:/etc/nginx/conf.d/default.conf:ro", conf.display()), ]) .arg("nginx:1.29-alpine") .output() .await .expect("failed to run docker"); assert!( out.status.success(), "{name} stub failed: {}", String::from_utf8_lossy(&out.stderr) ); String::from_utf8_lossy(&out.stdout).trim().to_string() } let gateway_id = run_stub("gateway", gateway_port, &conf_path).await; let runner_id = run_stub("runner", runner_port, &conf_path).await; let aggregate_id = run_stub("aggregate", aggregate_port, &conf_path).await; let projection_id = run_stub("projection", projection_port, &conf_path).await; let secret = b"e2e_secret"; let api_port = free_port(); let api_url = format!("http://127.0.0.1:{api_port}"); let mut placement_path = std::env::temp_dir(); placement_path.push(format!( "cloudlysis-control-placement-{}.json", uuid::Uuid::new_v4() )); fs::write( &placement_path, r#"{"revision":"e2e","aggregate_placement":{"placements":[]},"projection_placement":{"placements":[]},"runner_placement":{"placements":[]}}"#, ) .unwrap(); let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_api")) .env("CONTROL_API_ADDR", format!("127.0.0.1:{api_port}")) .env("CONTROL_GATEWAY_JWT_HS256_SECRET", "e2e_secret") .env("CONTROL_PLACEMENT_PATH", placement_path.to_string_lossy().to_string()) .env( "CONTROL_FLEET_SERVICES", format!( "gateway=http://127.0.0.1:{gateway_port},aggregate=http://127.0.0.1:{aggregate_port},projection=http://127.0.0.1:{projection_port},runner=http://127.0.0.1:{runner_port}" ), ) .spawn() .expect("failed to spawn control-api"); wait_ready(&api_url).await; let client = reqwest::Client::new(); let t = token(secret, &["control:read"]); let res = client .get(format!("{api_url}/admin/v1/fleet/snapshot")) .header(reqwest::header::AUTHORIZATION, format!("Bearer {t}")) .send() .await .unwrap(); assert!(res.status().is_success()); let v: serde_json::Value = res.json().await.unwrap(); let services = v.get("services").and_then(|x| x.as_array()).unwrap(); assert!( services.len() >= 5, "expected at least 5 services (including control-api), got {}", services.len() ); let res = client .get(format!("{api_url}/admin/v1/tenants")) .header(reqwest::header::AUTHORIZATION, format!("Bearer {t}")) .send() .await .unwrap(); assert!(res.status().is_success()); let _ = child.kill().await; for id in [gateway_id, runner_id, aggregate_id, projection_id] { let _ = tokio::process::Command::new("docker") .args(["stop", &id]) .output() .await; } let _ = fs::remove_file(&conf_path); let _ = fs::remove_file(&placement_path); }