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