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