#[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}" ); }