solti_api/lib.rs
1//! # solti-api
2//!
3//! Public task transports for a Solti agent.
4//!
5//! HTTP uses the model-owned CRD JSON representation.
6//! gRPC uses versioned protobuf messages.
7//! Both transports delegate domain operations to one [`ApiHandler`].
8//!
9//! This crate does not store or execute tasks.
10//!
11//! ## Start Here
12//!
13//! Use [`ApiHandler`] to define the transport-independent backend.
14//! Use `SupervisorApiAdapter` to connect that boundary to `solti-core`.
15//! Use `HttpApi` to build a standalone axum router or mount documented routes
16//! into an application router.
17//! Use `GrpcApi` to build a tonic service.
18//!
19//! ## Flow
20//!
21//! ```text
22//! HTTP CRD JSON ── parse and validate ──┐
23//! ▼
24//! ApiHandler
25//! ▲
26//! gRPC v1 DTO ── convert and validate ──┘
27//! └──► custom backend or solti-core
28//! ```
29//!
30//! The transports own wire validation, authentication, metrics, and error mapping.
31//! The handler owns task operations.
32//!
33//! ## Desired State
34//!
35//! The bundled adapter commits desired state before reconciliation finishes.
36//! A successful create or apply does not mean that execution has started.
37//! Clients observe reconciliation through `status.conditions[type=Reconciled]`.
38//!
39//! Apply is an upsert without write preconditions.
40//! Apply and delete can check `uid` and `resourceVersion`.
41//!
42//! ## Collections and Streams
43//!
44//! Lists use opaque continuation tokens.
45//! The bundled adapter provides snapshot-consistent pagination.
46//! Watches can resume from a retained resource version.
47//!
48//! Task output is live-only and lossy.
49//! It is not persisted or replayed.
50//! A slow subscriber receives a `Lagged` event.
51//!
52//! ## Workload Boundary
53//!
54//! The built-in `Embedded` workload is available only through the in-process SDK.
55//! HTTP and gRPC reject it.
56//! Extension workloads remain visible.
57//!
58//! ## Feature Flags
59//!
60//! | Feature | Capability |
61//! |----------------|-------------------------------------------------|
62//! | `core-adapter` | `SupervisorApiAdapter` for `solti-core` |
63//! | `grpc` | tonic gRPC service and generated current client |
64//! | `grpc-tls` | `solti-tls` adapter for tonic; implies `grpc` |
65//! | `http` | axum HTTP/JSON router |
66//!
67//! No feature is enabled by default.
68//!
69//! ## Main Types
70//!
71//! | Area | Types |
72//! |---------------|-------------------------------------------------------------|
73//! | Handler | [`ApiHandler`], [`ApiError`] |
74//! | Streams | [`TaskWatchEventStream`], [`OutputEventStream`] |
75//! | Metrics | [`ApiMetricsBackend`], [`ApiMetricsHandle`], [`Transport`] |
76//! | HTTP | `HttpApi`, `HttpApiParts` |
77//! | gRPC | `GrpcApi`, `grpc::wire` |
78//! | Core adapter | `SupervisorApiAdapter` |
79//!
80//! ## Quick Start
81//!
82//! Build both transports from one handler:
83//!
84#![cfg_attr(
85 all(feature = "core-adapter", feature = "grpc", feature = "http"),
86 doc = "```rust,no_run"
87)]
88#![cfg_attr(
89 not(all(feature = "core-adapter", feature = "grpc", feature = "http")),
90 doc = "```rust,no_run,ignore"
91)]
92//! # use std::sync::Arc;
93//! # use solti_api::{GrpcApi, HttpApi, SupervisorApiAdapter};
94//! # fn wire(supervisor: Arc<solti_core::SupervisorApi>) {
95//! let handler = Arc::new(SupervisorApiAdapter::new(supervisor));
96//! let grpc = GrpcApi::new(handler.clone()).server();
97//! let http = HttpApi::new(handler).build();
98//! # let _ = (grpc, http.router, http.openapi);
99//! # }
100//! ```
101
102#![forbid(unsafe_code)]
103#![warn(missing_docs)]
104
105/// Compiles the runnable Rust code blocks in `README.md` as doctests.
106///
107/// Gated on every feature: the README examples cover both transports and TLS.
108#[cfg(all(
109 doctest,
110 feature = "core-adapter",
111 feature = "grpc",
112 feature = "grpc-tls",
113 feature = "http"
114))]
115#[doc = include_str!("../README.md")]
116struct ReadmeDoctests;
117
118/// Compose a compile-time Kubernetes named-group URL rooted at `/apis/solti.io/v<API_MAJOR>`.
119macro_rules! api_url {
120 ($path:literal) => {
121 concat!("/apis/solti.io/v", env!("SOLTI_API_MAJOR"), $path)
122 };
123}
124
125/// Current public API major version.
126pub const API_VERSION: u32 = solti_model::TASK_API_VERSION_MAJOR;
127
128/// Current public API version name.
129pub const API_VERSION_NAME: &str = concat!("v", env!("SOLTI_API_MAJOR"));
130
131/// Current gRPC package exposed by the agent.
132pub const GRPC_API_PACKAGE: &str = concat!("solti.task.v", env!("SOLTI_API_MAJOR"));
133
134/// Current gRPC service exposed by the agent.
135pub const GRPC_API_SERVICE: &str = concat!("solti.task.v", env!("SOLTI_API_MAJOR"), ".TaskService");
136
137/// Root path of the HTTP Kubernetes API group.
138pub const HTTP_API_ROOT: &str = api_url!("");
139
140/// Maximum HTTP request body and gRPC message size.
141///
142/// The limit is 4 MiB.
143pub const MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
144
145mod error;
146pub use error::{ApiConflict, ApiError, ApiErrorCause};
147
148mod handler;
149pub use handler::{ApiHandler, OutputEventStream, TaskWatchEventStream};
150
151#[cfg(any(feature = "grpc", feature = "http"))]
152mod continuation;
153
154#[cfg(feature = "core-adapter")]
155mod adapter;
156#[cfg(feature = "core-adapter")]
157pub use adapter::SupervisorApiAdapter;
158
159mod metrics;
160pub use metrics::{
161 ApiMetricsBackend, ApiMetricsHandle, NoOpApiMetrics, Transport, noop_api_metrics,
162};
163
164// Generated prost output carries no doc comments; suppress the doc
165// lints on this module only. Never suppress them crate-wide.
166#[cfg(feature = "grpc")]
167#[allow(missing_docs)]
168#[allow(rustdoc::all)]
169pub(crate) mod proto_api {
170 include!(concat!(
171 env!("OUT_DIR"),
172 "/solti.task.v",
173 env!("SOLTI_API_MAJOR"),
174 ".rs"
175 ));
176}
177
178#[cfg(any(feature = "grpc", feature = "http"))]
179mod auth;
180
181#[cfg(any(feature = "grpc", feature = "http"))]
182mod validate;
183
184#[cfg(any(feature = "grpc", feature = "http", feature = "core-adapter"))]
185mod visibility;
186
187#[cfg(feature = "grpc")]
188pub mod grpc;
189
190#[cfg(feature = "grpc")]
191pub use grpc::GrpcApi;
192
193#[cfg(feature = "grpc")]
194pub use tonic;
195
196#[cfg(feature = "http")]
197mod http;
198
199#[cfg(feature = "http")]
200pub use http::{HttpApi, HttpApiParts};
201
202#[cfg(feature = "http")]
203pub use axum;
204
205#[cfg(feature = "http")]
206pub use aide;
207
208#[cfg(feature = "grpc-tls")]
209mod tls;
210
211#[cfg(feature = "grpc-tls")]
212pub use tls::to_tonic_server_tls;
213
214#[cfg(test)]
215mod contract_identity_guard {
216 #[test]
217 fn task_contract_identity_is_consistent() {
218 assert_eq!(super::API_VERSION.to_string(), env!("SOLTI_API_MAJOR"));
219 assert_eq!(super::API_VERSION_NAME, format!("v{}", super::API_VERSION));
220 assert_eq!(
221 super::GRPC_API_PACKAGE,
222 format!("solti.task.v{}", super::API_VERSION),
223 );
224 assert_eq!(
225 super::GRPC_API_SERVICE,
226 format!("{}.TaskService", super::GRPC_API_PACKAGE),
227 );
228 assert_eq!(
229 super::HTTP_API_ROOT.strip_prefix("/apis/"),
230 Some(solti_model::TASK_API_VERSION),
231 "HTTP named group must match the Task resource apiVersion",
232 );
233 }
234}