374 lines
No EOL
13 KiB
Rust
374 lines
No EOL
13 KiB
Rust
use crate::*;
|
|
use crate::event_log::{BuildEventLog, BuildEventLogError, create_build_event_log};
|
|
use aide::{
|
|
axum::{
|
|
routing::{get, post, delete},
|
|
ApiRouter,
|
|
},
|
|
openapi::OpenApi,
|
|
};
|
|
use axum::{Extension, response::Response, http::StatusCode};
|
|
use axum_jsonschema::Json;
|
|
use serde::{Deserialize, Serialize};
|
|
use schemars::JsonSchema;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use uuid::Uuid;
|
|
|
|
pub mod handlers;
|
|
|
|
#[derive(Clone)]
|
|
pub struct BuildGraphService {
|
|
pub event_log: Arc<dyn BuildEventLog>,
|
|
pub event_log_uri: String,
|
|
pub active_builds: Arc<RwLock<HashMap<String, BuildRequestState>>>,
|
|
pub graph_label: String,
|
|
pub job_lookup_path: String,
|
|
pub candidate_jobs: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct BuildRequestState {
|
|
pub build_request_id: String,
|
|
pub status: BuildRequestStatus,
|
|
pub requested_partitions: Vec<String>,
|
|
pub created_at: i64,
|
|
pub updated_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildRequest {
|
|
pub partitions: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildRequestResponse {
|
|
pub build_request_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildStatusResponse {
|
|
pub build_request_id: String,
|
|
pub status: String,
|
|
pub requested_partitions: Vec<String>,
|
|
pub created_at: i64,
|
|
pub updated_at: i64,
|
|
pub events: Vec<BuildEventSummary>,
|
|
pub job_graph: Option<serde_json::Value>,
|
|
pub mermaid_diagram: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildEventSummary {
|
|
pub event_id: String,
|
|
pub timestamp: i64,
|
|
pub event_type: String,
|
|
pub message: String,
|
|
pub build_request_id: String, // Build request ID for navigation
|
|
// Navigation-relevant fields (populated based on event type)
|
|
pub job_label: Option<String>, // For job events
|
|
pub partition_ref: Option<String>, // For partition events
|
|
pub delegated_build_id: Option<String>, // For delegation events
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct PartitionStatusResponse {
|
|
pub partition_ref: String,
|
|
pub status: String,
|
|
pub last_updated: Option<i64>,
|
|
pub build_requests: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct PartitionEventsResponse {
|
|
pub partition_ref: String,
|
|
pub events: Vec<BuildEventSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct AnalyzeRequest {
|
|
pub partitions: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct AnalyzeResponse {
|
|
pub job_graph: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct ErrorResponse {
|
|
pub error: String,
|
|
}
|
|
|
|
// List endpoints request/response types
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildsListResponse {
|
|
pub builds: Vec<BuildSummary>,
|
|
pub total_count: u32,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct BuildSummary {
|
|
pub build_request_id: String,
|
|
pub status: String,
|
|
pub requested_partitions: Vec<String>,
|
|
pub created_at: i64,
|
|
pub updated_at: i64,
|
|
}
|
|
|
|
// TODO snake cased response
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct PartitionsListResponse {
|
|
pub partitions: Vec<PartitionSummary>,
|
|
pub total_count: u32,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct PartitionSummary {
|
|
pub partition_ref: String,
|
|
pub status: String,
|
|
pub updated_at: i64,
|
|
pub build_request_id: Option<String>,
|
|
}
|
|
|
|
// TODO camel cased results
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct ActivityResponse {
|
|
pub active_builds_count: u32,
|
|
pub recent_builds: Vec<BuildSummary>,
|
|
pub recent_partitions: Vec<PartitionSummary>,
|
|
pub total_partitions_count: u32,
|
|
pub system_status: String,
|
|
pub graph_name: String,
|
|
}
|
|
|
|
// Job-related request/response types
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct JobsListResponse {
|
|
pub jobs: Vec<JobSummary>,
|
|
pub total_count: u32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct JobSummary {
|
|
pub job_label: String,
|
|
pub success_rate: f64,
|
|
pub avg_duration_ms: Option<i64>,
|
|
pub recent_runs: u32,
|
|
pub last_run: Option<i64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct JobMetricsResponse {
|
|
pub job_label: String,
|
|
pub success_rate: f64,
|
|
pub avg_duration_ms: Option<i64>,
|
|
pub total_runs: u32,
|
|
pub recent_runs: Vec<JobRunSummary>,
|
|
pub daily_stats: Vec<JobDailyStats>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct JobRunSummary {
|
|
pub build_request_id: String,
|
|
pub partitions: Vec<String>,
|
|
pub status: String,
|
|
pub duration_ms: Option<i64>,
|
|
pub started_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct JobDailyStats {
|
|
pub date: String,
|
|
pub success_rate: f64,
|
|
pub avg_duration_ms: Option<i64>,
|
|
pub total_runs: u32,
|
|
}
|
|
|
|
impl BuildGraphService {
|
|
pub async fn new(
|
|
event_log_uri: &str,
|
|
graph_label: String,
|
|
job_lookup_path: String,
|
|
candidate_jobs: HashMap<String, String>,
|
|
) -> Result<Self, BuildEventLogError> {
|
|
let event_log = create_build_event_log(event_log_uri).await?;
|
|
|
|
Ok(Self {
|
|
event_log: Arc::from(event_log),
|
|
event_log_uri: event_log_uri.to_string(),
|
|
active_builds: Arc::new(RwLock::new(HashMap::new())),
|
|
graph_label,
|
|
job_lookup_path,
|
|
candidate_jobs,
|
|
})
|
|
}
|
|
|
|
pub fn generate_openapi_spec(&self) -> OpenApi {
|
|
let mut api = OpenApi::default();
|
|
|
|
// Create API router with all routes to generate OpenAPI spec
|
|
let _ = ApiRouter::new()
|
|
.api_route("/api/v1/builds", post(handlers::submit_build_request))
|
|
.api_route("/api/v1/builds", get(handlers::list_build_requests))
|
|
.api_route("/api/v1/builds/:build_request_id", get(handlers::get_build_status))
|
|
.api_route("/api/v1/builds/:build_request_id", delete(handlers::cancel_build_request))
|
|
.api_route("/api/v1/partitions", get(handlers::list_partitions))
|
|
.api_route("/api/v1/partitions/:ref/status", get(handlers::get_partition_status))
|
|
.api_route("/api/v1/partitions/:ref/events", get(handlers::get_partition_events))
|
|
.api_route("/api/v1/jobs", get(handlers::list_jobs))
|
|
.api_route("/api/v1/jobs/:label", get(handlers::get_job_metrics))
|
|
.api_route("/api/v1/activity", get(handlers::get_activity_summary))
|
|
.api_route("/api/v1/analyze", post(handlers::analyze_build_graph))
|
|
.finish_api(&mut api);
|
|
|
|
api
|
|
}
|
|
|
|
pub fn create_router(self) -> axum::Router {
|
|
let mut api = OpenApi::default();
|
|
|
|
let api_router = ApiRouter::new()
|
|
.api_route("/api/v1/builds", post(handlers::submit_build_request))
|
|
.api_route("/api/v1/builds", get(handlers::list_build_requests))
|
|
.api_route("/api/v1/builds/:build_request_id", get(handlers::get_build_status))
|
|
.api_route("/api/v1/builds/:build_request_id", delete(handlers::cancel_build_request))
|
|
.api_route("/api/v1/partitions", get(handlers::list_partitions))
|
|
.api_route("/api/v1/partitions/:ref/status", get(handlers::get_partition_status))
|
|
.api_route("/api/v1/partitions/:ref/events", get(handlers::get_partition_events))
|
|
.api_route("/api/v1/jobs", get(handlers::list_jobs))
|
|
.api_route("/api/v1/jobs/:label", get(handlers::get_job_metrics))
|
|
.api_route("/api/v1/activity", get(handlers::get_activity_summary))
|
|
.api_route("/api/v1/analyze", post(handlers::analyze_build_graph))
|
|
.route("/api/v1/openapi.json", get(Self::openapi_spec))
|
|
.with_state(Arc::new(self))
|
|
.finish_api(&mut api);
|
|
|
|
let static_router = axum::Router::new()
|
|
.route("/", axum::routing::get(Self::serve_index))
|
|
.route("/static/*file", axum::routing::get(Self::serve_static));
|
|
|
|
axum::Router::new()
|
|
.merge(api_router)
|
|
.merge(static_router)
|
|
.layer(Extension(api))
|
|
.layer(axum::middleware::from_fn(Self::cors_middleware))
|
|
}
|
|
|
|
pub async fn openapi_spec(Extension(api): Extension<OpenApi>) -> Json<OpenApi> {
|
|
Json(api)
|
|
}
|
|
|
|
pub async fn serve_index() -> Response {
|
|
let index_path = Self::get_runfile_path("databuild+/databuild/dashboard/index.html");
|
|
|
|
match std::fs::read_to_string(&index_path) {
|
|
Ok(content) => Response::builder()
|
|
.header("content-type", "text/html")
|
|
.body(content.into())
|
|
.unwrap(),
|
|
Err(_) => Response::builder()
|
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
.body("Failed to load dashboard".into())
|
|
.unwrap(),
|
|
}
|
|
}
|
|
|
|
pub async fn serve_static(axum::extract::Path(file): axum::extract::Path<String>) -> Response {
|
|
let file_path = Self::get_runfile_path(&format!("databuild+/databuild/dashboard/{}", file));
|
|
|
|
match std::fs::read(file_path) {
|
|
Ok(content) => {
|
|
let content_type = match file.split('.').last() {
|
|
Some("html") => "text/html",
|
|
Some("css") => "text/css",
|
|
Some("js") => "application/javascript",
|
|
Some("png") => "image/png",
|
|
Some("jpg") | Some("jpeg") => "image/jpeg",
|
|
Some("svg") => "image/svg+xml",
|
|
Some("ico") => "image/x-icon",
|
|
_ => "application/octet-stream",
|
|
};
|
|
|
|
Response::builder()
|
|
.header("content-type", content_type)
|
|
.body(content.into())
|
|
.unwrap()
|
|
}
|
|
Err(_) => Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body("404 Not Found".into())
|
|
.unwrap(),
|
|
}
|
|
}
|
|
|
|
fn get_runfile_path(relative_path: &str) -> String {
|
|
if let Ok(runfiles_dir) = std::env::var("RUNFILES_DIR") {
|
|
format!("{}/{}", runfiles_dir, relative_path)
|
|
} else if let Ok(_manifest_file) = std::env::var("RUNFILES_MANIFEST_FILE") {
|
|
// Parse manifest file to find the actual path
|
|
// For now, just use the relative path
|
|
relative_path.to_string()
|
|
} else {
|
|
// Development mode - files might be in the workspace
|
|
relative_path.to_string()
|
|
}
|
|
}
|
|
|
|
pub async fn cors_middleware(
|
|
request: axum::http::Request<axum::body::Body>,
|
|
next: axum::middleware::Next,
|
|
) -> axum::response::Response {
|
|
let response = next.run(request).await;
|
|
let (mut parts, body) = response.into_parts();
|
|
|
|
parts.headers.insert(
|
|
axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
axum::http::HeaderValue::from_static("*"),
|
|
);
|
|
parts.headers.insert(
|
|
axum::http::header::ACCESS_CONTROL_ALLOW_METHODS,
|
|
axum::http::HeaderValue::from_static("GET, POST, DELETE, OPTIONS"),
|
|
);
|
|
parts.headers.insert(
|
|
axum::http::header::ACCESS_CONTROL_ALLOW_HEADERS,
|
|
axum::http::HeaderValue::from_static("Content-Type, Authorization"),
|
|
);
|
|
|
|
axum::response::Response::from_parts(parts, body)
|
|
}
|
|
|
|
pub fn generate_build_request_id() -> String {
|
|
Uuid::new_v4().to_string()
|
|
}
|
|
|
|
pub fn status_to_string(status: BuildRequestStatus) -> String {
|
|
match status {
|
|
BuildRequestStatus::BuildRequestUnknown => "unknown".to_string(),
|
|
BuildRequestStatus::BuildRequestReceived => "received".to_string(),
|
|
BuildRequestStatus::BuildRequestPlanning => "planning".to_string(),
|
|
BuildRequestStatus::BuildRequestAnalysisCompleted => "analysis_completed".to_string(),
|
|
BuildRequestStatus::BuildRequestExecuting => "executing".to_string(),
|
|
BuildRequestStatus::BuildRequestCompleted => "completed".to_string(),
|
|
BuildRequestStatus::BuildRequestFailed => "failed".to_string(),
|
|
BuildRequestStatus::BuildRequestCancelled => "cancelled".to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn partition_status_to_string(status: PartitionStatus) -> String {
|
|
match status {
|
|
PartitionStatus::PartitionUnknown => "unknown".to_string(),
|
|
PartitionStatus::PartitionRequested => "requested".to_string(),
|
|
PartitionStatus::PartitionAnalyzed => "analyzed".to_string(),
|
|
PartitionStatus::PartitionBuilding => "building".to_string(),
|
|
PartitionStatus::PartitionAvailable => "available".to_string(),
|
|
PartitionStatus::PartitionFailed => "failed".to_string(),
|
|
PartitionStatus::PartitionDelegated => "delegated".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type ServiceState = Arc<BuildGraphService>; |