Files
cloudlysis/control/api/tests/e2e_control_plane_fleet_docker.rs
Vlad Durnea 1298d9a3df
Some checks failed
ci / rust (push) Failing after 2m34s
ci / ui (push) Failing after 30s
Monorepo consolidation: workspace, shared types, transport plans, docker/swam assets
2026-03-30 11:40:42 +03:00

184 lines
5.5 KiB
Rust

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<String>,
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);
}