lightshuttle_control/lib.rs
1#![deny(missing_docs)]
2//! Local HTTP control plane and dashboard for LightShuttle.
3//!
4//! This crate is the developer-facing control surface that runs alongside the
5//! LightShuttle orchestrator. It depends on [`lightshuttle_runtime`] for the
6//! [`lightshuttle_runtime::LifecycleHandle`] trait and the resource view types,
7//! and adds an HTTP layer on top: a REST API, WebSocket streams for logs and
8//! lifecycle events, Prometheus metrics, and an SSR dashboard built with Askama
9//! and HTMX.
10//!
11//! # Crate layout
12//!
13//! | Public item | Role |
14//! |---|---|
15//! | [`ControlState`] | Shared axum state (project name + lifecycle handle + metrics) |
16//! | [`ControlServer`] | HTTP server wrapping an axum router |
17//! | [`bind`] | Async helper to open a `TcpListener` before building the server |
18//! | [`Metrics`] | Prometheus recorder and scrape renderer |
19//! | [`observe_event_duration`] | Record a lifecycle-event duration histogram sample |
20//! | [`ApiError`] / [`ApiErrorBody`] | HTTP error type returned by every REST handler |
21//!
22//! # Security note
23//!
24//! The control plane is designed for **local development only**. It carries no
25//! authentication and the caller is expected to bind it to the loopback address
26//! (`127.0.0.1`). Never expose this server on a non-loopback interface or in a
27//! shared or production environment.
28//!
29//! # Quick start
30//!
31//! ```rust,no_run
32//! use std::net::SocketAddr;
33//! use lightshuttle_control::{ControlState, ControlServer, Metrics, bind};
34//! # use lightshuttle_runtime::{
35//! # LifecycleEvent, LifecycleHandle, LifecycleHandleError,
36//! # LogChunkStream, ResourceView,
37//! # };
38//! # use tokio::sync::broadcast;
39//! # #[derive(Clone)]
40//! # struct MyHandle;
41//! # impl LifecycleHandle for MyHandle {
42//! # async fn list(&self) -> Result<Vec<ResourceView>, LifecycleHandleError> { Ok(vec![]) }
43//! # async fn get(&self, _: &str) -> Result<ResourceView, LifecycleHandleError> {
44//! # Err(LifecycleHandleError::NotSupported("get"))
45//! # }
46//! # async fn restart(&self, _: &str) -> Result<(), LifecycleHandleError> {
47//! # Err(LifecycleHandleError::NotSupported("restart"))
48//! # }
49//! # async fn logs(&self, _: &str, _: bool) -> Result<LogChunkStream, LifecycleHandleError> {
50//! # Err(LifecycleHandleError::NotSupported("logs"))
51//! # }
52//! # fn subscribe_events(&self) -> broadcast::Receiver<LifecycleEvent> {
53//! # broadcast::channel(1).1
54//! # }
55//! # }
56//!
57//! #[tokio::main]
58//! async fn main() -> std::io::Result<()> {
59//! // Bind to loopback only. The control plane has no authentication.
60//! let addr: SocketAddr = "127.0.0.1:9090".parse().unwrap();
61//! let listener = bind(addr).await?;
62//!
63//! let metrics = std::sync::Arc::new(Metrics::install());
64//! let state = ControlState::with_metrics("my-project", MyHandle, metrics);
65//! let server = ControlServer::new(state);
66//!
67//! server.serve(listener, async { /* await shutdown signal */ }).await
68//! }
69//! ```
70
71pub use crate::error::{ApiError, ApiErrorBody};
72pub use crate::metrics::{Metrics, observe_event_duration};
73pub use crate::server::{ControlServer, bind};
74pub use crate::state::ControlState;
75
76mod error;
77mod metrics;
78mod routes;
79mod server;
80mod state;