klieo_workflow_api/
lib.rs1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3mod auth;
14mod error;
15mod events;
16mod executor;
17mod handlers;
18mod progress;
19mod provenance;
20mod run_store;
21mod state;
22mod sweep;
23
24pub use auth::{WORKFLOW_READ_ALL_SCOPE, WORKFLOW_READ_SCOPE, WORKFLOW_RUN_SCOPE};
25pub use error::{ErrorCode, WorkflowApiError};
26pub use run_store::{ClaimOutcome, RunRecordView, RunStatus, RunStore, StoreError, RUNS_BUCKET};
27pub use sweep::{sweep_orphaned_runs, SweepError, SweepSummary};
28
29use crate::state::WorkflowRunState;
30use axum::{
31 middleware,
32 routing::{get, post},
33 Router,
34};
35use klieo::App;
36use klieo_auth_common::Authenticator;
37use klieo_provenance::ProvenanceRepository;
38use klieo_workflow::Registry;
39use std::sync::Arc;
40use std::time::Duration;
41use tokio::sync::Semaphore;
42use tower_http::limit::RequestBodyLimitLayer;
43
44pub const MAX_CONCURRENT_RUNS: usize = 32;
46
47pub const MAX_RUN_DURATION: Duration = Duration::from_secs(300);
49
50pub const MAX_DEF_BYTES: usize = 256 * 1024;
52
53#[non_exhaustive]
55#[derive(Debug, thiserror::Error)]
56pub enum WorkflowRunBuilderError {
57 #[error("authenticator not configured — call with_authenticator() before build()")]
59 MissingAuthenticator,
60 #[error("app not configured — call with_app() before build()")]
62 MissingApp,
63 #[error("registry not configured — call with_registry() before build()")]
65 MissingRegistry,
66 #[error("run store not configured — call with_run_store() before build()")]
68 MissingRunStore,
69 #[error("provenance repository not configured — call with_provenance() before build()")]
72 MissingProvenance,
73}
74
75#[derive(Default)]
77pub struct WorkflowRunRouterBuilder {
78 authenticator: Option<Arc<dyn Authenticator>>,
79 app: Option<Arc<App>>,
80 registry: Option<Registry>,
81 run_store: Option<RunStore>,
82 provenance: Option<Arc<dyn ProvenanceRepository>>,
83 run_timeout: Option<Duration>,
84 max_concurrent_runs: Option<usize>,
85}
86
87impl WorkflowRunRouterBuilder {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn with_authenticator(mut self, authenticator: Arc<dyn Authenticator>) -> Self {
95 self.authenticator = Some(authenticator);
96 self
97 }
98
99 pub fn with_app(mut self, app: Arc<App>) -> Self {
103 self.app = Some(app);
104 self
105 }
106
107 pub fn with_registry(mut self, registry: Registry) -> Self {
109 self.registry = Some(registry);
110 self
111 }
112
113 pub fn with_run_store(mut self, run_store: RunStore) -> Self {
115 self.run_store = Some(run_store);
116 self
117 }
118
119 pub fn with_provenance(mut self, provenance: Arc<dyn ProvenanceRepository>) -> Self {
123 self.provenance = Some(provenance);
124 self
125 }
126
127 pub fn with_run_timeout(mut self, timeout: Duration) -> Self {
129 self.run_timeout = Some(timeout);
130 self
131 }
132
133 pub fn with_max_concurrent_runs(mut self, max: usize) -> Self {
135 self.max_concurrent_runs = Some(max);
136 self
137 }
138
139 pub fn build(self) -> Result<Router, WorkflowRunBuilderError> {
145 let max_concurrent = self.max_concurrent_runs.unwrap_or(MAX_CONCURRENT_RUNS);
146 let state = Arc::new(WorkflowRunState {
147 authenticator: self
148 .authenticator
149 .ok_or(WorkflowRunBuilderError::MissingAuthenticator)?,
150 app: self.app.ok_or(WorkflowRunBuilderError::MissingApp)?,
151 registry: self
152 .registry
153 .ok_or(WorkflowRunBuilderError::MissingRegistry)?,
154 run_store: self
155 .run_store
156 .ok_or(WorkflowRunBuilderError::MissingRunStore)?,
157 provenance: self
158 .provenance
159 .ok_or(WorkflowRunBuilderError::MissingProvenance)?,
160 progress: progress::ProgressHub::default(),
161 permits: Arc::new(Semaphore::new(max_concurrent)),
162 run_timeout: self.run_timeout.unwrap_or(MAX_RUN_DURATION),
163 });
164
165 let submit_route = post(handlers::submit)
166 .layer(RequestBodyLimitLayer::new(MAX_DEF_BYTES))
167 .route_layer(middleware::from_fn_with_state(
168 Arc::clone(&state),
169 auth::require_run_scope,
170 ));
171 let read_layer =
172 middleware::from_fn_with_state(Arc::clone(&state), auth::require_read_scope);
173 let get_route = get(handlers::get_run).route_layer(read_layer.clone());
174 let events_route = get(events::get_run_events).route_layer(read_layer.clone());
175 let registry_route = get(handlers::get_registry).route_layer(read_layer);
176
177 Ok(Router::new()
178 .route("/runs", submit_route)
179 .route("/runs/:run_id", get_route)
180 .route("/runs/:run_id/events", events_route)
181 .route("/registry", registry_route)
182 .with_state(state))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn build_without_authenticator_returns_missing_authenticator() {
192 let err = WorkflowRunRouterBuilder::new().build().unwrap_err();
193 assert!(matches!(err, WorkflowRunBuilderError::MissingAuthenticator));
194 }
195}