This commit is contained in:
Stuart Axelbrooke 2025-07-08 22:12:09 -07:00
parent 232e0583f2
commit 9e8928b11a
4 changed files with 161 additions and 17 deletions

View file

@ -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"],
)

View file

@ -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<ServiceState>,
Path(build_request_id): Path<String>,
Path(request): Path<BuildStatusRequest>,
) -> Result<Json<BuildStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
// 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<ServiceState>,
Path(build_request_id): Path<String>,
Path(request): Path<CancelBuildRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
// 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<ServiceState>,
Path(partition_ref): Path<String>,
Path(partition_ref): Path<PartitionStatusRequest>,
) -> Result<Json<PartitionStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
// 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<ServiceState>,
Path(partition_ref): Path<String>,
Path(request): Path<PartitionEventsRequest>,
) -> Result<Json<PartitionEventsResponse>, (StatusCode, Json<ErrorResponse>)> {
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,
}))
}

View file

@ -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::<String>("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);

View file

@ -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();