184 lines
5.5 KiB
Rust
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);
|
|
}
|