sparktest_api/
handlers.rs

1use crate::k8s::KubernetesClient;
2use axum::{extract::Path, http::StatusCode, response::Json, Json as JsonBody};
3use serde::{Deserialize, Serialize};
4use sparktest_core::*;
5use uuid::Uuid;
6
7#[derive(Serialize)]
8pub struct HealthResponse {
9    pub status: String,
10    pub timestamp: String,
11}
12
13#[derive(Deserialize)]
14pub struct CreateRunRequest {
15    pub name: String,
16    pub image: String,
17    pub commands: Vec<String>,
18}
19
20pub async fn health_check() -> Json<HealthResponse> {
21    Json(HealthResponse {
22        status: "healthy".to_string(),
23        timestamp: chrono::Utc::now().to_rfc3339(),
24    })
25}
26
27pub async fn get_runs() -> Result<Json<Vec<TestRun>>, StatusCode> {
28    // In a real implementation, this would fetch from database
29    Ok(Json(vec![]))
30}
31
32pub async fn create_run(
33    JsonBody(req): JsonBody<CreateRunRequest>,
34) -> Result<Json<TestRun>, StatusCode> {
35    // In a real implementation, this would create a run in the database
36    let run = TestRun {
37        id: Uuid::new_v4(),
38        name: req.name,
39        image: req.image,
40        commands: req.commands,
41        status: "pending".to_string(),
42        created_at: chrono::Utc::now(),
43        definition_id: None,
44        executor_id: None,
45        suite_id: None,
46        variables: None,
47        artifacts: None,
48        duration: None,
49        retries: None,
50        logs: None,
51        k8s_job_name: None,
52        pod_scheduled: None,
53        container_created: None,
54        container_started: None,
55        completed: None,
56        failed: None,
57    };
58
59    Ok(Json(run))
60}
61
62pub async fn get_run(Path(_id): Path<Uuid>) -> Result<Json<TestRun>, StatusCode> {
63    // In a real implementation, this would fetch from database
64    Err(StatusCode::NOT_FOUND)
65}
66
67pub async fn delete_run(Path(_id): Path<Uuid>) -> Result<StatusCode, StatusCode> {
68    // In a real implementation, this would delete from database
69    Ok(StatusCode::NO_CONTENT)
70}
71
72pub async fn k8s_health() -> Json<serde_json::Value> {
73    // Attempt to create Kubernetes client and check health
74    match KubernetesClient::new().await {
75        Ok(client) => match client.health_check().await {
76            Ok(is_healthy) => Json(serde_json::json!({
77                "kubernetes_connected": is_healthy,
78                "timestamp": chrono::Utc::now().to_rfc3339()
79            })),
80            Err(_) => Json(serde_json::json!({
81                "kubernetes_connected": false,
82                "timestamp": chrono::Utc::now().to_rfc3339(),
83                "error": "Kubernetes health check failed"
84            })),
85        },
86        Err(_) => Json(serde_json::json!({
87            "kubernetes_connected": false,
88            "timestamp": chrono::Utc::now().to_rfc3339(),
89            "error": "Could not create Kubernetes client"
90        })),
91    }
92}
93
94pub async fn get_job_logs(Path(job_name): Path<String>) -> Json<serde_json::Value> {
95    // Attempt to get real job logs from Kubernetes
96    match KubernetesClient::new().await {
97        Ok(client) => match client.get_job_logs(&job_name).await {
98            Ok(job_logs) => Json(serde_json::json!({
99                "job_name": job_logs.job_name,
100                "pod_name": job_logs.pod_name,
101                "logs": job_logs.logs,
102                "timestamp": job_logs.timestamp.to_rfc3339(),
103                "status": job_logs.status
104            })),
105            Err(e) => Json(serde_json::json!({
106                "job_name": job_name,
107                "error": format!("Failed to get job logs: {}", e),
108                "timestamp": chrono::Utc::now().to_rfc3339(),
109                "status": "error"
110            })),
111        },
112        Err(_) => Json(serde_json::json!({
113            "job_name": job_name,
114            "error": "Kubernetes client unavailable",
115            "timestamp": chrono::Utc::now().to_rfc3339(),
116            "status": "error"
117        })),
118    }
119}
120
121pub async fn get_job_status(Path(job_name): Path<String>) -> Json<serde_json::Value> {
122    // Attempt to get real job status from Kubernetes
123    match KubernetesClient::new().await {
124        Ok(client) => match client.get_job_status(&job_name).await {
125            Ok(status) => Json(serde_json::json!({
126                "job_name": job_name,
127                "status": status,
128                "timestamp": chrono::Utc::now().to_rfc3339()
129            })),
130            Err(e) => Json(serde_json::json!({
131                "job_name": job_name,
132                "status": "error",
133                "error": format!("Failed to get job status: {}", e),
134                "timestamp": chrono::Utc::now().to_rfc3339()
135            })),
136        },
137        Err(_) => Json(serde_json::json!({
138            "job_name": job_name,
139            "status": "error",
140            "error": "Kubernetes client unavailable",
141            "timestamp": chrono::Utc::now().to_rfc3339()
142        })),
143    }
144}
145
146pub async fn delete_job(Path(job_name): Path<String>) -> Json<serde_json::Value> {
147    // Attempt to delete real job from Kubernetes
148    match KubernetesClient::new().await {
149        Ok(client) => match client.delete_job(&job_name).await {
150            Ok(_) => Json(serde_json::json!({
151                "message": format!("Job {} deleted successfully", job_name),
152                "timestamp": chrono::Utc::now().to_rfc3339()
153            })),
154            Err(e) => Json(serde_json::json!({
155                "error": format!("Failed to delete job {}: {}", job_name, e),
156                "timestamp": chrono::Utc::now().to_rfc3339()
157            })),
158        },
159        Err(_) => Json(serde_json::json!({
160            "error": format!("Kubernetes client unavailable - cannot delete job {}", job_name),
161            "timestamp": chrono::Utc::now().to_rfc3339()
162        })),
163    }
164}
165
166pub async fn get_definitions() -> Result<Json<Vec<serde_json::Value>>, StatusCode> {
167    // For this demo, return empty list since we're focusing on the K8s integration
168    Ok(Json(vec![]))
169}
170
171pub async fn get_executors() -> Result<Json<Vec<serde_json::Value>>, StatusCode> {
172    // For this demo, return empty list since we're focusing on the K8s integration
173    Ok(Json(vec![]))
174}
175
176pub async fn get_suites() -> Result<Json<Vec<serde_json::Value>>, StatusCode> {
177    // For this demo, return empty list since we're focusing on the K8s integration
178    Ok(Json(vec![]))
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use axum::Json as JsonBody;
185
186    #[tokio::test]
187    async fn test_health_check() {
188        let response = health_check().await;
189        assert_eq!(response.0.status, "healthy");
190        assert!(!response.0.timestamp.is_empty());
191    }
192
193    #[tokio::test]
194    async fn test_get_runs() {
195        let result = get_runs().await;
196        assert!(result.is_ok());
197        let runs = result.unwrap().0;
198        assert_eq!(runs.len(), 0);
199    }
200
201    #[tokio::test]
202    async fn test_create_run() {
203        let request = CreateRunRequest {
204            name: "Test Run".to_string(),
205            image: "test:latest".to_string(),
206            commands: vec!["echo".to_string(), "hello".to_string()],
207        };
208
209        let result = create_run(JsonBody(request)).await;
210        assert!(result.is_ok());
211
212        let run = result.unwrap().0;
213        assert_eq!(run.name, "Test Run");
214        assert_eq!(run.image, "test:latest");
215        assert_eq!(run.status, "pending");
216        assert_eq!(run.commands.len(), 2);
217    }
218
219    #[tokio::test]
220    async fn test_get_run() {
221        let id = Uuid::new_v4();
222        let result = get_run(Path(id)).await;
223        assert!(result.is_err());
224        assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
225    }
226
227    #[tokio::test]
228    async fn test_delete_run() {
229        let id = Uuid::new_v4();
230        let result = delete_run(Path(id)).await;
231        assert!(result.is_ok());
232        assert_eq!(result.unwrap(), StatusCode::NO_CONTENT);
233    }
234
235    #[tokio::test]
236    async fn test_k8s_health() {
237        let response = k8s_health().await;
238        let value = response.0;
239        // In test environment, Kubernetes is typically not available
240        assert_eq!(value["kubernetes_connected"], false);
241        assert!(value["timestamp"].is_string());
242        assert!(value["error"].is_string());
243    }
244
245    #[tokio::test]
246    async fn test_get_job_logs() {
247        let job_name = "test-job".to_string();
248        let response = get_job_logs(Path(job_name.clone())).await;
249        let value = response.0;
250        assert_eq!(value["job_name"], job_name);
251        // In test environment, Kubernetes is not available, so expect error
252        assert_eq!(value["status"], "error");
253        assert!(value["error"].is_string());
254        assert!(value["timestamp"].is_string());
255    }
256
257    #[tokio::test]
258    async fn test_get_job_status() {
259        let job_name = "test-job".to_string();
260        let response = get_job_status(Path(job_name.clone())).await;
261        let value = response.0;
262        assert_eq!(value["job_name"], job_name);
263        // In test environment, Kubernetes is not available, so expect error
264        assert_eq!(value["status"], "error");
265        assert!(value["error"].is_string());
266        assert!(value["timestamp"].is_string());
267    }
268
269    #[tokio::test]
270    async fn test_delete_job() {
271        let job_name = "test-job".to_string();
272        let response = delete_job(Path(job_name.clone())).await;
273        let value = response.0;
274        // In test environment, Kubernetes is not available, so expect error
275        assert!(value["error"].is_string());
276        assert!(value["timestamp"].is_string());
277        let error_msg = value["error"].as_str().unwrap();
278        assert!(error_msg.contains("Kubernetes client unavailable"));
279    }
280}