Skip to main content

saluki_app/
config.rs

1//! Configuration API handler.
2
3use async_trait::async_trait;
4use http::StatusCode;
5use saluki_api::{
6    extract::State,
7    response::IntoResponse,
8    routing::{get, Router},
9    APIHandler, DynamicRoute, EndpointType,
10};
11use saluki_config::GenericConfiguration;
12use saluki_core::runtime::{
13    state::DataspaceRegistry, InitializationError, ProcessShutdown, Supervisable, SupervisorFuture,
14};
15use saluki_error::generic_error;
16use serde_json::Value;
17
18/// State used for the config API handler.
19#[derive(Clone)]
20pub struct ConfigState {
21    config: GenericConfiguration,
22}
23
24/// An API handler for returning the current configuration.
25///
26/// This handler exposes a single route -- `/config` -- that returns the current configuration in its serialized JSON
27/// form. This allows determining exactly how the process' configuration looks based on the various providers being
28/// used, including any dynamic changes being applied.
29pub struct ConfigAPIHandler {
30    state: ConfigState,
31}
32
33impl ConfigAPIHandler {
34    fn new(config: GenericConfiguration) -> Self {
35        Self {
36            state: ConfigState { config },
37        }
38    }
39
40    async fn config_handler(State(state): State<ConfigState>) -> impl IntoResponse {
41        match state.config.as_typed::<Value>() {
42            Ok(config) => (StatusCode::OK, serde_json::to_string(&config).unwrap()).into_response(),
43            Err(e) => (
44                StatusCode::INTERNAL_SERVER_ERROR,
45                format!("Failed to get configuration: {}", e),
46            )
47                .into_response(),
48        }
49    }
50}
51
52impl APIHandler for ConfigAPIHandler {
53    type State = ConfigState;
54
55    fn generate_initial_state(&self) -> Self::State {
56        self.state.clone()
57    }
58
59    fn generate_routes(&self) -> Router<Self::State> {
60        Router::new().route("/config", get(Self::config_handler))
61    }
62}
63
64/// A worker for exposing an endpoint that returns the current configuration.
65///
66/// When running, the worker asserts a set of routes (based on [`ConfigAPIHandler`]) that allow querying the current
67/// configuration. As the configuration may contain sensitive data, these routes are only present on the privileged API
68/// endpoint.
69pub struct ConfigWorker {
70    handler: ConfigAPIHandler,
71}
72
73impl ConfigWorker {
74    /// Creates a new [`ConfigWorker`] with the given configuration.
75    pub fn new(config: GenericConfiguration) -> Self {
76        Self {
77            handler: ConfigAPIHandler::new(config),
78        }
79    }
80}
81
82#[async_trait]
83impl Supervisable for ConfigWorker {
84    fn name(&self) -> &str {
85        "config-api"
86    }
87
88    async fn initialize(&self, process_shutdown: ProcessShutdown) -> Result<SupervisorFuture, InitializationError> {
89        let config_route = DynamicRoute::http(EndpointType::Privileged, &self.handler);
90
91        Ok(Box::pin(async move {
92            DataspaceRegistry::try_current()
93                .ok_or_else(|| generic_error!("Dataspace not available."))?
94                .assert(config_route, "config-api");
95
96            process_shutdown.await;
97            Ok(())
98        }))
99    }
100}