databuild/databuild/service/mod.rs
2025-07-20 23:24:50 -07:00

467 lines
No EOL
17 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_code: i32,
pub status_name: 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 {
#[schemars(schema_with = "job_graph_schema")]
pub job_graph: serde_json::Value,
}
fn job_graph_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new(schemars::schema::InstanceType::Object))),
..Default::default()
})
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildCancelResponse {
pub cancelled: bool,
pub build_request_id: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildCancelRepositoryResponse {
pub cancelled: bool,
pub build_request_id: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PartitionInvalidateResponse {
pub invalidated: bool,
pub partition_ref: String,
pub reason: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TaskCancelResponse {
pub cancelled: bool,
pub job_run_id: String,
pub reason: String,
}
// List endpoints request/response types
// Removed: duplicate of crate::BuildsListResponse from proto
// Wrapper structs for API responses that contain protobuf data + service metadata
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildsListApiResponse {
pub data: crate::BuildsListResponse,
pub request_id: Option<String>,
pub pagination: Option<PaginationInfo>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PartitionsListApiResponse {
pub data: crate::PartitionsListResponse,
pub request_id: Option<String>,
pub pagination: Option<PaginationInfo>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct JobsListApiResponse {
pub data: crate::JobsListResponse,
pub request_id: Option<String>,
pub pagination: Option<PaginationInfo>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TasksListApiResponse {
pub data: crate::TasksListResponse,
pub request_id: Option<String>,
pub pagination: Option<PaginationInfo>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActivityApiResponse {
pub data: crate::ActivityResponse,
pub request_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PaginationInfo {
pub total_count: u32,
pub has_more: bool,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
// Removed: Legacy types that duplicate proto definitions
// - BuildSummary (use crate::BuildSummary from proto)
// - PartitionsListResponse (use crate::PartitionsListResponse from proto)
// - PartitionSummary (use crate::PartitionSummary from proto)
// Job-related request/response types
// Removed: JobsListResponse and JobSummary (use crate:: proto versions)
#[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_code: i32,
pub status_name: 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_builds_repository))
.api_route("/api/v1/builds/:build_request_id", get(handlers::get_build_detail))
.api_route("/api/v1/builds/:build_request_id", delete(handlers::cancel_build_repository))
.api_route("/api/v1/partitions", get(handlers::list_partitions_repository))
.api_route("/api/v1/partitions/:partition_ref", get(handlers::get_partition_detail))
.api_route("/api/v1/partitions/:partition_ref/status", get(handlers::get_partition_status))
.api_route("/api/v1/partitions/:partition_ref/events", get(handlers::get_partition_events))
.api_route("/api/v1/partitions/:partition_ref/invalidate", post(handlers::invalidate_partition))
.api_route("/api/v1/jobs", get(handlers::list_jobs_repository))
.api_route("/api/v1/jobs/:label", get(handlers::get_job_detail))
.api_route("/api/v1/jobs/:label/metrics", get(handlers::get_job_metrics))
.api_route("/api/v1/tasks", get(handlers::list_tasks_repository))
.api_route("/api/v1/tasks/:job_run_id", get(handlers::get_task_detail))
.api_route("/api/v1/tasks/:job_run_id/cancel", post(handlers::cancel_task))
.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_builds_repository))
.api_route("/api/v1/builds/:build_request_id", get(handlers::get_build_detail))
.api_route("/api/v1/builds/:build_request_id", delete(handlers::cancel_build_repository))
.api_route("/api/v1/partitions", get(handlers::list_partitions_repository))
.api_route("/api/v1/partitions/:partition_ref", get(handlers::get_partition_detail))
.api_route("/api/v1/partitions/:partition_ref/status", get(handlers::get_partition_status))
.api_route("/api/v1/partitions/:partition_ref/events", get(handlers::get_partition_events))
.api_route("/api/v1/partitions/:partition_ref/invalidate", post(handlers::invalidate_partition))
.api_route("/api/v1/jobs", get(handlers::list_jobs_repository))
.api_route("/api/v1/jobs/:label", get(handlers::get_job_detail))
.api_route("/api/v1/jobs/:label/metrics", get(handlers::get_job_metrics))
.api_route("/api/v1/tasks", get(handlers::list_tasks_repository))
.api_route("/api/v1/tasks/:job_run_id", get(handlers::get_task_detail))
.api_route("/api/v1/tasks/:job_run_id/cancel", post(handlers::cancel_task))
.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>;
// Repository-based response types
// Removed: PartitionDetailResponse and PartitionTimelineEvent (use crate:: proto versions)
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct JobsRepositoryListResponse {
pub jobs: Vec<JobRepositorySummary>,
pub total_count: u32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct JobRepositorySummary {
pub job_label: String,
pub total_runs: usize,
pub successful_runs: usize,
pub failed_runs: usize,
pub cancelled_runs: usize,
pub average_partitions_per_run: f64,
pub last_run_timestamp: i64,
pub last_run_status: String,
pub recent_builds: Vec<String>,
}
// Removed: JobDetailResponse, JobRunDetail, TasksListResponse, TaskSummary (use crate:: proto versions)
// Removed: TaskDetailResponse and TaskTimelineEvent (use crate:: proto versions)
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildsRepositoryListResponse {
pub builds: Vec<BuildRepositorySummary>,
pub total_count: u32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildRepositorySummary {
pub build_request_id: String,
pub status: String,
pub requested_partitions: Vec<String>,
pub total_jobs: usize,
pub completed_jobs: usize,
pub failed_jobs: usize,
pub cancelled_jobs: usize,
pub requested_at: i64,
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
pub duration_ms: Option<i64>,
pub cancelled: bool,
}
// Removed: BuildDetailResponse and BuildTimelineEvent (use crate:: proto versions)