WIP 2
This commit is contained in:
parent
232e0583f2
commit
9e8928b11a
4 changed files with 161 additions and 17 deletions
65
databuild/client/BUILD.bazel
Normal file
65
databuild/client/BUILD.bazel
Normal 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"],
|
||||||
|
)
|
||||||
|
|
@ -6,6 +6,8 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_jsonschema::Json;
|
use axum_jsonschema::Json;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
|
@ -73,14 +75,19 @@ pub async fn submit_build_request(
|
||||||
Ok(Json(BuildRequestResponse { build_request_id }))
|
Ok(Json(BuildRequestResponse { build_request_id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
pub struct BuildStatusRequest {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_build_status(
|
pub async fn get_build_status(
|
||||||
State(service): State<ServiceState>,
|
State(service): State<ServiceState>,
|
||||||
Path(build_request_id): Path<String>,
|
Path(request): Path<BuildStatusRequest>,
|
||||||
) -> Result<Json<BuildStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<BuildStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
// Get build request state
|
// Get build request state
|
||||||
let build_state = {
|
let build_state = {
|
||||||
let active_builds = service.active_builds.read().await;
|
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 {
|
let build_state = match build_state {
|
||||||
|
|
@ -96,7 +103,7 @@ pub async fn get_build_status(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get events for this build request
|
// 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 {
|
Ok(events) => events.into_iter().map(|e| BuildEventSummary {
|
||||||
event_id: e.event_id,
|
event_id: e.event_id,
|
||||||
timestamp: e.timestamp,
|
timestamp: e.timestamp,
|
||||||
|
|
@ -110,7 +117,7 @@ pub async fn get_build_status(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(BuildStatusResponse {
|
Ok(Json(BuildStatusResponse {
|
||||||
build_request_id,
|
build_request_id: request.id,
|
||||||
status: BuildGraphService::status_to_string(build_state.status),
|
status: BuildGraphService::status_to_string(build_state.status),
|
||||||
requested_partitions: build_state.requested_partitions,
|
requested_partitions: build_state.requested_partitions,
|
||||||
created_at: build_state.created_at,
|
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(
|
pub async fn cancel_build_request(
|
||||||
State(service): State<ServiceState>,
|
State(service): State<ServiceState>,
|
||||||
Path(build_request_id): Path<String>,
|
Path(request): Path<CancelBuildRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
// Update build request state
|
// Update build request state
|
||||||
{
|
{
|
||||||
let mut active_builds = service.active_builds.write().await;
|
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.status = BuildRequestStatus::BuildRequestCancelled;
|
||||||
build_state.updated_at = current_timestamp_nanos();
|
build_state.updated_at = current_timestamp_nanos();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -141,7 +153,7 @@ pub async fn cancel_build_request(
|
||||||
|
|
||||||
// Log cancellation event
|
// Log cancellation event
|
||||||
let event = create_build_event(
|
let event = create_build_event(
|
||||||
build_request_id.clone(),
|
request.id.clone(),
|
||||||
crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent {
|
crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent {
|
||||||
status: BuildRequestStatus::BuildRequestCancelled as i32,
|
status: BuildRequestStatus::BuildRequestCancelled as i32,
|
||||||
requested_partitions: vec![],
|
requested_partitions: vec![],
|
||||||
|
|
@ -153,20 +165,26 @@ pub async fn cancel_build_request(
|
||||||
error!("Failed to log build request cancelled event: {}", e);
|
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!({
|
Ok(Json(serde_json::json!({
|
||||||
"cancelled": true,
|
"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(
|
pub async fn get_partition_status(
|
||||||
State(service): State<ServiceState>,
|
State(service): State<ServiceState>,
|
||||||
Path(partition_ref): Path<String>,
|
Path(partition_ref): Path<PartitionStatusRequest>,
|
||||||
) -> Result<Json<PartitionStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<PartitionStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
// Get latest partition status
|
// 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(Some((status, timestamp))) => (status, Some(timestamp)),
|
||||||
Ok(None) => (PartitionStatus::PartitionUnknown, None),
|
Ok(None) => (PartitionStatus::PartitionUnknown, None),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -181,7 +199,7 @@ pub async fn get_partition_status(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get active builds for this partition
|
// 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,
|
Ok(builds) => builds,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get active builds for partition: {}", e);
|
error!("Failed to get active builds for partition: {}", e);
|
||||||
|
|
@ -190,18 +208,24 @@ pub async fn get_partition_status(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(PartitionStatusResponse {
|
Ok(Json(PartitionStatusResponse {
|
||||||
partition_ref,
|
partition_ref: partition_ref.ref_param,
|
||||||
status: BuildGraphService::partition_status_to_string(status),
|
status: BuildGraphService::partition_status_to_string(status),
|
||||||
last_updated,
|
last_updated,
|
||||||
build_requests,
|
build_requests,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
pub struct PartitionEventsRequest {
|
||||||
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_param: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_partition_events(
|
pub async fn get_partition_events(
|
||||||
State(service): State<ServiceState>,
|
State(service): State<ServiceState>,
|
||||||
Path(partition_ref): Path<String>,
|
Path(request): Path<PartitionEventsRequest>,
|
||||||
) -> Result<Json<PartitionEventsResponse>, (StatusCode, Json<ErrorResponse>)> {
|
) -> 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 {
|
Ok(events) => events.into_iter().map(|e| BuildEventSummary {
|
||||||
event_id: e.event_id,
|
event_id: e.event_id,
|
||||||
timestamp: e.timestamp,
|
timestamp: e.timestamp,
|
||||||
|
|
@ -220,7 +244,7 @@ pub async fn get_partition_events(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(PartitionEventsResponse {
|
Ok(Json(PartitionEventsResponse {
|
||||||
partition_ref,
|
partition_ref: request.ref_param,
|
||||||
events,
|
events,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ async fn main() {
|
||||||
.help("Job lookup binary path")
|
.help("Job lookup binary path")
|
||||||
.default_value("job_lookup")
|
.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();
|
.get_matches();
|
||||||
|
|
||||||
let port: u16 = matches.get_one::<String>("port").unwrap()
|
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()))
|
.map(|s| serde_json::from_str(&s).unwrap_or_else(|_| HashMap::new()))
|
||||||
.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!("Starting Build Graph Service on {}:{}", host, port);
|
||||||
info!("Event log URI: {}", event_log_uri);
|
info!("Event log URI: {}", event_log_uri);
|
||||||
info!("Graph label: {}", graph_label);
|
info!("Graph label: {}", graph_label);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::*;
|
||||||
use crate::event_log::{BuildEventLog, BuildEventLogError, create_build_event_log};
|
use crate::event_log::{BuildEventLog, BuildEventLogError, create_build_event_log};
|
||||||
use aide::{
|
use aide::{
|
||||||
axum::{
|
axum::{
|
||||||
routing::{get, post, delete},
|
routing::{get, get_with, post, delete},
|
||||||
ApiRouter,
|
ApiRouter,
|
||||||
},
|
},
|
||||||
openapi::OpenApi,
|
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 {
|
pub fn create_router(self) -> axum::Router {
|
||||||
let mut api = OpenApi::default();
|
let mut api = OpenApi::default();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue