#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
mod auth;
mod error;
mod events;
mod executor;
mod handlers;
mod progress;
mod provenance;
mod run_store;
mod state;
mod sweep;
pub use auth::{WORKFLOW_READ_ALL_SCOPE, WORKFLOW_READ_SCOPE, WORKFLOW_RUN_SCOPE};
pub use error::{ErrorCode, WorkflowApiError};
pub use run_store::{ClaimOutcome, RunRecordView, RunStatus, RunStore, StoreError, RUNS_BUCKET};
pub use sweep::{sweep_orphaned_runs, SweepError, SweepSummary};
use crate::state::WorkflowRunState;
use axum::{
middleware,
routing::{get, post},
Router,
};
use klieo::App;
use klieo_auth_common::Authenticator;
use klieo_provenance::ProvenanceRepository;
use klieo_workflow::Registry;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
use tower_http::limit::RequestBodyLimitLayer;
pub const MAX_CONCURRENT_RUNS: usize = 32;
pub const MAX_RUN_DURATION: Duration = Duration::from_secs(300);
pub const MAX_DEF_BYTES: usize = 256 * 1024;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum WorkflowRunBuilderError {
#[error("authenticator not configured — call with_authenticator() before build()")]
MissingAuthenticator,
#[error("app not configured — call with_app() before build()")]
MissingApp,
#[error("registry not configured — call with_registry() before build()")]
MissingRegistry,
#[error("run store not configured — call with_run_store() before build()")]
MissingRunStore,
#[error("provenance repository not configured — call with_provenance() before build()")]
MissingProvenance,
}
#[derive(Default)]
pub struct WorkflowRunRouterBuilder {
authenticator: Option<Arc<dyn Authenticator>>,
app: Option<Arc<App>>,
registry: Option<Registry>,
run_store: Option<RunStore>,
provenance: Option<Arc<dyn ProvenanceRepository>>,
run_timeout: Option<Duration>,
max_concurrent_runs: Option<usize>,
}
impl WorkflowRunRouterBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_authenticator(mut self, authenticator: Arc<dyn Authenticator>) -> Self {
self.authenticator = Some(authenticator);
self
}
pub fn with_app(mut self, app: Arc<App>) -> Self {
self.app = Some(app);
self
}
pub fn with_registry(mut self, registry: Registry) -> Self {
self.registry = Some(registry);
self
}
pub fn with_run_store(mut self, run_store: RunStore) -> Self {
self.run_store = Some(run_store);
self
}
pub fn with_provenance(mut self, provenance: Arc<dyn ProvenanceRepository>) -> Self {
self.provenance = Some(provenance);
self
}
pub fn with_run_timeout(mut self, timeout: Duration) -> Self {
self.run_timeout = Some(timeout);
self
}
pub fn with_max_concurrent_runs(mut self, max: usize) -> Self {
self.max_concurrent_runs = Some(max);
self
}
pub fn build(self) -> Result<Router, WorkflowRunBuilderError> {
let max_concurrent = self.max_concurrent_runs.unwrap_or(MAX_CONCURRENT_RUNS);
let state = Arc::new(WorkflowRunState {
authenticator: self
.authenticator
.ok_or(WorkflowRunBuilderError::MissingAuthenticator)?,
app: self.app.ok_or(WorkflowRunBuilderError::MissingApp)?,
registry: self
.registry
.ok_or(WorkflowRunBuilderError::MissingRegistry)?,
run_store: self
.run_store
.ok_or(WorkflowRunBuilderError::MissingRunStore)?,
provenance: self
.provenance
.ok_or(WorkflowRunBuilderError::MissingProvenance)?,
progress: progress::ProgressHub::default(),
permits: Arc::new(Semaphore::new(max_concurrent)),
run_timeout: self.run_timeout.unwrap_or(MAX_RUN_DURATION),
});
let submit_route = post(handlers::submit)
.layer(RequestBodyLimitLayer::new(MAX_DEF_BYTES))
.route_layer(middleware::from_fn_with_state(
Arc::clone(&state),
auth::require_run_scope,
));
let read_layer =
middleware::from_fn_with_state(Arc::clone(&state), auth::require_read_scope);
let get_route = get(handlers::get_run).route_layer(read_layer.clone());
let events_route = get(events::get_run_events).route_layer(read_layer.clone());
let registry_route = get(handlers::get_registry).route_layer(read_layer);
Ok(Router::new()
.route("/health", get(handlers::health))
.route("/runs", submit_route)
.route("/runs/:run_id", get_route)
.route("/runs/:run_id/events", events_route)
.route("/registry", registry_route)
.with_state(state))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_without_authenticator_returns_missing_authenticator() {
let err = WorkflowRunRouterBuilder::new().build().unwrap_err();
assert!(matches!(err, WorkflowRunBuilderError::MissingAuthenticator));
}
}