klieo_workflow_api/
lib.rs1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3mod auth;
29mod error;
30mod events;
31mod executor;
32mod handlers;
33mod progress;
34mod provenance;
35mod run_store;
36mod state;
37mod sweep;
38
39pub use auth::{WORKFLOW_READ_ALL_SCOPE, WORKFLOW_READ_SCOPE, WORKFLOW_RUN_SCOPE};
40pub use error::{ErrorCode, WorkflowApiError};
41pub use run_store::{ClaimOutcome, RunRecordView, RunStatus, RunStore, StoreError, RUNS_BUCKET};
42pub use sweep::{sweep_orphaned_runs, SweepError, SweepSummary};
43
44use crate::state::WorkflowRunState;
45use axum::{
46 middleware,
47 routing::{get, post},
48 Router,
49};
50use klieo::App;
51use klieo_auth_common::Authenticator;
52use klieo_provenance::ProvenanceRepository;
53use klieo_workflow::Registry;
54use std::sync::Arc;
55use std::time::Duration;
56use tokio::sync::Semaphore;
57use tower_http::limit::RequestBodyLimitLayer;
58
59pub const MAX_CONCURRENT_RUNS: usize = 32;
61
62pub const MAX_RUN_DURATION: Duration = Duration::from_secs(300);
64
65pub const MAX_DEF_BYTES: usize = 256 * 1024;
67
68#[non_exhaustive]
70#[derive(Debug, thiserror::Error)]
71pub enum WorkflowRunBuilderError {
72 #[error("authenticator not configured — call with_authenticator() before build()")]
74 MissingAuthenticator,
75 #[error("app not configured — call with_app() before build()")]
77 MissingApp,
78 #[error("registry not configured — call with_registry() before build()")]
80 MissingRegistry,
81 #[error("run store not configured — call with_run_store() before build()")]
83 MissingRunStore,
84 #[error("provenance repository not configured — call with_provenance() before build()")]
87 MissingProvenance,
88}
89
90#[derive(Default)]
92pub struct WorkflowRunRouterBuilder {
93 authenticator: Option<Arc<dyn Authenticator>>,
94 app: Option<Arc<App>>,
95 registry: Option<Registry>,
96 run_store: Option<RunStore>,
97 provenance: Option<Arc<dyn ProvenanceRepository>>,
98 run_timeout: Option<Duration>,
99 max_concurrent_runs: Option<usize>,
100}
101
102impl WorkflowRunRouterBuilder {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn with_authenticator(mut self, authenticator: Arc<dyn Authenticator>) -> Self {
110 self.authenticator = Some(authenticator);
111 self
112 }
113
114 pub fn with_app(mut self, app: Arc<App>) -> Self {
118 self.app = Some(app);
119 self
120 }
121
122 pub fn with_registry(mut self, registry: Registry) -> Self {
124 self.registry = Some(registry);
125 self
126 }
127
128 pub fn with_run_store(mut self, run_store: RunStore) -> Self {
130 self.run_store = Some(run_store);
131 self
132 }
133
134 pub fn with_provenance(mut self, provenance: Arc<dyn ProvenanceRepository>) -> Self {
138 self.provenance = Some(provenance);
139 self
140 }
141
142 pub fn with_run_timeout(mut self, timeout: Duration) -> Self {
144 self.run_timeout = Some(timeout);
145 self
146 }
147
148 pub fn with_max_concurrent_runs(mut self, max: usize) -> Self {
150 self.max_concurrent_runs = Some(max);
151 self
152 }
153
154 pub fn build(self) -> Result<Router, WorkflowRunBuilderError> {
160 let max_concurrent = self.max_concurrent_runs.unwrap_or(MAX_CONCURRENT_RUNS);
161 let state = Arc::new(WorkflowRunState {
162 authenticator: self
163 .authenticator
164 .ok_or(WorkflowRunBuilderError::MissingAuthenticator)?,
165 app: self.app.ok_or(WorkflowRunBuilderError::MissingApp)?,
166 registry: self
167 .registry
168 .ok_or(WorkflowRunBuilderError::MissingRegistry)?,
169 run_store: self
170 .run_store
171 .ok_or(WorkflowRunBuilderError::MissingRunStore)?,
172 provenance: self
173 .provenance
174 .ok_or(WorkflowRunBuilderError::MissingProvenance)?,
175 progress: progress::ProgressHub::default(),
176 permits: Arc::new(Semaphore::new(max_concurrent)),
177 run_timeout: self.run_timeout.unwrap_or(MAX_RUN_DURATION),
178 });
179
180 let submit_route = post(handlers::submit)
181 .layer(RequestBodyLimitLayer::new(MAX_DEF_BYTES))
182 .route_layer(middleware::from_fn_with_state(
183 Arc::clone(&state),
184 auth::require_run_scope,
185 ));
186 let read_layer =
187 middleware::from_fn_with_state(Arc::clone(&state), auth::require_read_scope);
188 let get_route = get(handlers::get_run).route_layer(read_layer.clone());
189 let events_route = get(events::get_run_events).route_layer(read_layer.clone());
190 let registry_route = get(handlers::get_registry).route_layer(read_layer);
191
192 Ok(Router::new()
193 .route("/health", get(handlers::health))
194 .route("/runs", submit_route)
195 .route("/runs/:run_id", get_route)
196 .route("/runs/:run_id/events", events_route)
197 .route("/registry", registry_route)
198 .with_state(state))
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn build_without_authenticator_returns_missing_authenticator() {
208 let err = WorkflowRunRouterBuilder::new().build().unwrap_err();
209 assert!(matches!(err, WorkflowRunBuilderError::MissingAuthenticator));
210 }
211}