138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
#[tokio::test]
|
|
async fn platform_drift_docker_test_is_gated() {
|
|
use tower::ServiceExt;
|
|
|
|
let enabled = std::env::var("CONTROL_TEST_DOCKER").ok();
|
|
if enabled.as_deref() != Some("1") {
|
|
eprintln!("skipping: set CONTROL_TEST_DOCKER=1 to enable docker drift tests");
|
|
return;
|
|
}
|
|
|
|
// We only run the "real" drift check when Swarm is available locally.
|
|
// If Swarm isn't active, we skip to keep CI/dev machines happy.
|
|
let info = std::process::Command::new("docker")
|
|
.args(["info", "--format", "{{.Swarm.LocalNodeState}}"])
|
|
.output();
|
|
let Ok(info) = info else {
|
|
eprintln!("skipping: docker not available");
|
|
return;
|
|
};
|
|
if !info.status.success() {
|
|
eprintln!("skipping: docker info failed");
|
|
return;
|
|
}
|
|
let state = String::from_utf8_lossy(&info.stdout).trim().to_string();
|
|
if state != "active" {
|
|
eprintln!("skipping: docker swarm not active (LocalNodeState={state})");
|
|
return;
|
|
}
|
|
|
|
// Create a short-lived service so drift can see an "extra" observed service.
|
|
let name = format!("cloudlysis-drift-extra-{}", uuid::Uuid::new_v4());
|
|
let create = std::process::Command::new("docker")
|
|
.args([
|
|
"service",
|
|
"create",
|
|
"--name",
|
|
&name,
|
|
"--restart-condition",
|
|
"none",
|
|
"busybox:1.36",
|
|
"sh",
|
|
"-c",
|
|
"sleep 60",
|
|
])
|
|
.output()
|
|
.expect("docker service create");
|
|
if !create.status.success() {
|
|
eprintln!("skipping: failed to create swarm service (maybe permissions?)");
|
|
return;
|
|
}
|
|
|
|
// Ensure cleanup even if assertion fails.
|
|
struct Cleanup(String);
|
|
impl Drop for Cleanup {
|
|
fn drop(&mut self) {
|
|
let _ = std::process::Command::new("docker")
|
|
.args(["service", "rm", &self.0])
|
|
.output();
|
|
}
|
|
}
|
|
let _cleanup = Cleanup(name.clone());
|
|
|
|
// Now call drift via a minimal in-process app configured for docker-cli swarm observation.
|
|
let handle = metrics_exporter_prometheus::PrometheusBuilder::new()
|
|
.install_recorder()
|
|
.expect("failed to install prometheus recorder");
|
|
|
|
let app = api::build_app(api::AppState {
|
|
prometheus: handle,
|
|
auth: api::AuthConfig {
|
|
hs256_secret: Some(b"test_secret".to_vec()),
|
|
},
|
|
jobs: api::JobStore::default(),
|
|
audit: api::AuditStore::default(),
|
|
tenant_locks: api::TenantLocks::default(),
|
|
config_locks: api::ConfigLocks::default(),
|
|
http: reqwest::Client::new(),
|
|
placement: api::PlacementStore::new(
|
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.and_then(|p| p.parent())
|
|
.unwrap()
|
|
.join("config/placement/dev.json"),
|
|
),
|
|
billing: api::billing::BillingStore::new(
|
|
std::env::temp_dir().join("billing-drift-test.json"),
|
|
),
|
|
billing_provider: std::sync::Arc::new(api::billing::MockProvider),
|
|
billing_enforcement_enabled: false,
|
|
config: api::ConfigRegistry::new(None, None),
|
|
fleet_services: vec![],
|
|
swarm: api::SwarmStore::new_docker_cli(),
|
|
docs: None,
|
|
});
|
|
|
|
// Auth token (control:read).
|
|
let exp = (std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
+ 60) as usize;
|
|
let token = jsonwebtoken::encode(
|
|
&jsonwebtoken::Header::default(),
|
|
&serde_json::json!({
|
|
"sub": "user_1",
|
|
"session_id": "sess_1",
|
|
"permissions": ["control:read"],
|
|
"exp": exp
|
|
}),
|
|
&jsonwebtoken::EncodingKey::from_secret(b"test_secret"),
|
|
)
|
|
.unwrap();
|
|
|
|
let res = app
|
|
.oneshot(
|
|
axum::http::Request::builder()
|
|
.uri("/admin/v1/platform/drift")
|
|
.header(axum::http::header::AUTHORIZATION, format!("Bearer {token}"))
|
|
.body(axum::body::Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.status(), axum::http::StatusCode::OK);
|
|
let body = axum::body::to_bytes(res.into_body(), 1024 * 1024)
|
|
.await
|
|
.unwrap();
|
|
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
|
let items = v.get("items").and_then(|x| x.as_array()).unwrap();
|
|
assert!(
|
|
items.iter().any(|i| {
|
|
i.get("kind").and_then(|k| k.as_str()) == Some("extra")
|
|
&& i.get("service").and_then(|s| s.as_str()) == Some(name.as_str())
|
|
}),
|
|
"expected drift to include extra service {name}, got: {v}"
|
|
);
|
|
}
|