feat(billing): implement tenant subscription entitlements system (milestones 0-6)
This commit is contained in:
137
control/api/tests/drift_docker_gated.rs
Normal file
137
control/api/tests/drift_docker_gated.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
#[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}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user