Skip to main content

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;