diff --git a/databuild/client/BUILD.bazel b/databuild/client/BUILD.bazel new file mode 100644 index 0000000..9123573 --- /dev/null +++ b/databuild/client/BUILD.bazel @@ -0,0 +1,65 @@ +# Extract OpenAPI spec from the service binary +genrule( + name = "extract_openapi_spec", + srcs = [], + outs = ["openapi.json"], + cmd = """ + $(location //databuild:build_graph_service) --print-openapi-spec > $@ + """, + tools = [ + "//databuild:build_graph_service", + ], + visibility = ["//visibility:public"], +) + +# TypeScript generator configuration +filegroup( + name = "typescript_generator_config", + srcs = ["typescript_generator_config.json"], + visibility = ["//visibility:public"], +) + +# Generate TypeScript client using OpenAPI Generator JAR +genrule( + name = "typescript_client", + srcs = [ + ":extract_openapi_spec", + ":typescript_generator_config", + ], + outs = [ + "typescript_generated/apis/DefaultApi.ts", + "typescript_generated/models/index.ts", + "typescript_generated/runtime.ts", + "typescript_generated/index.ts", + ], + cmd = """ + # Download OpenAPI Generator JAR + OPENAPI_JAR=/tmp/openapi-generator-cli.jar + if [ ! -f $$OPENAPI_JAR ]; then + curl -L -o $$OPENAPI_JAR https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.2.0/openapi-generator-cli-7.2.0.jar + fi + + # Generate TypeScript client + java -jar $$OPENAPI_JAR generate \ + -i $(location :extract_openapi_spec) \ + -g typescript-fetch \ + -c $(location :typescript_generator_config) \ + -o $$(dirname $(location typescript_generated/index.ts)) + + # Ensure all expected output files exist + touch $(location typescript_generated/apis/DefaultApi.ts) + touch $(location typescript_generated/models/index.ts) + touch $(location typescript_generated/runtime.ts) + touch $(location typescript_generated/index.ts) + """, + visibility = ["//visibility:public"], +) + +# Main TypeScript client target +filegroup( + name = "typescript", + srcs = [ + ":typescript_client", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/databuild/service/handlers.rs b/databuild/service/handlers.rs index bc66ee4..d003653 100644 --- a/databuild/service/handlers.rs +++ b/databuild/service/handlers.rs @@ -6,6 +6,8 @@ use axum::{ }; use axum_jsonschema::Json; use log::{error, info}; +use serde::Deserialize; +use schemars::JsonSchema; use std::process::Command; use std::env; @@ -73,14 +75,19 @@ pub async fn submit_build_request( Ok(Json(BuildRequestResponse { build_request_id })) } +#[derive(Deserialize, JsonSchema)] +pub struct BuildStatusRequest { + pub id: String, +} + pub async fn get_build_status( State(service): State, - Path(build_request_id): Path, + Path(request): Path, ) -> Result, (StatusCode, Json)> { // Get build request state let build_state = { let active_builds = service.active_builds.read().await; - active_builds.get(&build_request_id).cloned() + active_builds.get(&request.id).cloned() }; let build_state = match build_state { @@ -96,7 +103,7 @@ pub async fn get_build_status( }; // Get events for this build request - let events = match service.event_log.get_build_request_events(&build_request_id, None).await { + let events = match service.event_log.get_build_request_events(&request.id, None).await { Ok(events) => events.into_iter().map(|e| BuildEventSummary { event_id: e.event_id, timestamp: e.timestamp, @@ -110,7 +117,7 @@ pub async fn get_build_status( }; Ok(Json(BuildStatusResponse { - build_request_id, + build_request_id: request.id, status: BuildGraphService::status_to_string(build_state.status), requested_partitions: build_state.requested_partitions, created_at: build_state.created_at, @@ -119,14 +126,19 @@ pub async fn get_build_status( })) } +#[derive(Deserialize, JsonSchema)] +pub struct CancelBuildRequest { + pub id: String, +} + pub async fn cancel_build_request( State(service): State, - Path(build_request_id): Path, + Path(request): Path, ) -> Result, (StatusCode, Json)> { // Update build request state { let mut active_builds = service.active_builds.write().await; - if let Some(build_state) = active_builds.get_mut(&build_request_id) { + if let Some(build_state) = active_builds.get_mut(&request.id) { build_state.status = BuildRequestStatus::BuildRequestCancelled; build_state.updated_at = current_timestamp_nanos(); } else { @@ -141,7 +153,7 @@ pub async fn cancel_build_request( // Log cancellation event let event = create_build_event( - build_request_id.clone(), + request.id.clone(), crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent { status: BuildRequestStatus::BuildRequestCancelled as i32, requested_partitions: vec![], @@ -153,20 +165,26 @@ pub async fn cancel_build_request( error!("Failed to log build request cancelled event: {}", e); } - info!("Build request {} cancelled", build_request_id); + info!("Build request {} cancelled", request.id); Ok(Json(serde_json::json!({ "cancelled": true, - "build_request_id": build_request_id + "build_request_id": request.id }))) } +#[derive(Deserialize, JsonSchema)] +pub struct PartitionStatusRequest { + #[serde(rename = "ref")] + pub ref_param: String, +} + pub async fn get_partition_status( State(service): State, - Path(partition_ref): Path, + Path(partition_ref): Path, ) -> Result, (StatusCode, Json)> { // Get latest partition status - let (status, last_updated) = match service.event_log.get_latest_partition_status(&partition_ref).await { + let (status, last_updated) = match service.event_log.get_latest_partition_status(&partition_ref.ref_param).await { Ok(Some((status, timestamp))) => (status, Some(timestamp)), Ok(None) => (PartitionStatus::PartitionUnknown, None), Err(e) => { @@ -181,7 +199,7 @@ pub async fn get_partition_status( }; // Get active builds for this partition - let build_requests = match service.event_log.get_active_builds_for_partition(&partition_ref).await { + let build_requests = match service.event_log.get_active_builds_for_partition(&partition_ref.ref_param).await { Ok(builds) => builds, Err(e) => { error!("Failed to get active builds for partition: {}", e); @@ -190,18 +208,24 @@ pub async fn get_partition_status( }; Ok(Json(PartitionStatusResponse { - partition_ref, + partition_ref: partition_ref.ref_param, status: BuildGraphService::partition_status_to_string(status), last_updated, build_requests, })) } +#[derive(Deserialize, JsonSchema)] +pub struct PartitionEventsRequest { + #[serde(rename = "ref")] + pub ref_param: String, +} + pub async fn get_partition_events( State(service): State, - Path(partition_ref): Path, + Path(request): Path, ) -> Result, (StatusCode, Json)> { - let events = match service.event_log.get_partition_events(&partition_ref, None).await { + let events = match service.event_log.get_partition_events(&request.ref_param, None).await { Ok(events) => events.into_iter().map(|e| BuildEventSummary { event_id: e.event_id, timestamp: e.timestamp, @@ -220,7 +244,7 @@ pub async fn get_partition_events( }; Ok(Json(PartitionEventsResponse { - partition_ref, + partition_ref: request.ref_param, events, })) } diff --git a/databuild/service/main.rs b/databuild/service/main.rs index 783ff00..7b53dce 100644 --- a/databuild/service/main.rs +++ b/databuild/service/main.rs @@ -49,6 +49,12 @@ async fn main() { .help("Job lookup binary path") .default_value("job_lookup") ) + .arg( + Arg::new("print-openapi-spec") + .long("print-openapi-spec") + .help("Print OpenAPI spec to stdout and exit") + .action(clap::ArgAction::SetTrue) + ) .get_matches(); let port: u16 = matches.get_one::("port").unwrap() @@ -63,6 +69,39 @@ async fn main() { .map(|s| serde_json::from_str(&s).unwrap_or_else(|_| HashMap::new())) .unwrap_or_else(|_| HashMap::new()); + // Handle OpenAPI spec generation + if matches.get_flag("print-openapi-spec") { + // Disable logging for OpenAPI generation to keep output clean + log::set_max_level(log::LevelFilter::Off); + + // Create a minimal service instance for OpenAPI generation + let service = match BuildGraphService::new( + "sqlite://:memory:", // Use in-memory database for spec generation + graph_label, + job_lookup_path, + candidate_jobs, + ).await { + Ok(service) => service, + Err(e) => { + eprintln!("Failed to create service for OpenAPI generation: {}", e); + std::process::exit(1); + } + }; + + // Generate and print OpenAPI spec + let spec = service.generate_openapi_spec(); + match serde_json::to_string_pretty(&spec) { + Ok(json) => { + println!("{}", json); + std::process::exit(0); + } + Err(e) => { + eprintln!("Failed to serialize OpenAPI spec: {}", e); + std::process::exit(1); + } + } + } + info!("Starting Build Graph Service on {}:{}", host, port); info!("Event log URI: {}", event_log_uri); info!("Graph label: {}", graph_label); diff --git a/databuild/service/mod.rs b/databuild/service/mod.rs index 8b8bdf4..3515674 100644 --- a/databuild/service/mod.rs +++ b/databuild/service/mod.rs @@ -2,7 +2,7 @@ use crate::*; use crate::event_log::{BuildEventLog, BuildEventLogError, create_build_event_log}; use aide::{ axum::{ - routing::{get, post, delete}, + routing::{get, get_with, post, delete}, ApiRouter, }, openapi::OpenApi, @@ -113,6 +113,22 @@ impl BuildGraphService { }) } + 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/{id}", get(handlers::get_build_status)) + .api_route("/api/v1/builds/{id}", delete(handlers::cancel_build_request)) + .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/analyze", post(handlers::analyze_build_graph)) + .finish_api(&mut api); + + api + } + pub fn create_router(self) -> axum::Router { let mut api = OpenApi::default();