manta_server/config.rs
1//! Typed schema for `server.toml`.
2//!
3//! The untyped `config::Config` is loaded by
4//! [`manta_shared::common::config::get_server_configuration`]; this
5//! module owns the typed deserialisation target.
6
7use std::collections::HashMap;
8
9use crate::server::common::audit::Auditor;
10use manta_backend_dispatcher::types::K8sDetails;
11use serde::{Deserialize, Serialize};
12
13/// Which backend API this site speaks.
14#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
15#[serde(rename_all = "lowercase")]
16pub enum BackendTechnology {
17 /// HPE Cray System Management (CSM) backend.
18 Csm,
19 /// OpenCHAMI backend.
20 Ochami,
21}
22
23impl BackendTechnology {
24 /// Return the lowercase string expected by `StaticBackendDispatcher::new`.
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 Self::Csm => "csm",
28 Self::Ochami => "ochami",
29 }
30 }
31}
32
33#[derive(Serialize, Deserialize, Debug)]
34/// Connection details for a single ALPS site (CSM or OCHAMI instance).
35///
36/// The Vault URL used by handlers requiring vault (sat-file, session,
37/// console, logs) is derived at startup from
38/// `[sites.X.k8s.authentication.vault] base_url`. The vault secret path
39/// is derived from a hard-coded prefix and the site name. Neither is
40/// configured here.
41pub struct Site {
42 /// Which backend implementation this site uses (`csm` or `ochami`).
43 pub backend: BackendTechnology,
44 /// Optional per-site SOCKS5 proxy URL used by every outbound HTTP
45 /// request to this site's backend. `None` means direct connection.
46 pub socks5_proxy: Option<String>,
47 /// Base URL of the backend API (e.g. `https://api.alps.cscs.ch`).
48 pub shasta_base_url: String,
49 /// Optional Kubernetes connection details, required by handlers
50 /// that stream CFS session logs or attach to consoles.
51 pub k8s: Option<K8sDetails>,
52 /// Path (absolute or relative to the config dir) of the backend's
53 /// root CA certificate, used to verify TLS to `shasta_base_url`.
54 pub root_ca_cert_file: String,
55}
56
57/// Server-only settings — TLS, listen address, console behaviour. Lives
58/// under `[server]` in `server.toml`.
59#[derive(Serialize, Deserialize, Debug)]
60pub struct ServerSettings {
61 /// TCP listen address (e.g. "0.0.0.0"). When omitted from config
62 /// **and** no `--listen-address` flag is supplied, the server falls
63 /// back to `"0.0.0.0"`.
64 #[serde(default)]
65 pub listen_address: Option<String>,
66 /// TCP port. When omitted from config **and** no `--port` flag is
67 /// supplied, the effective default depends on whether TLS is
68 /// configured: `8443` if both `cert` and `key` are present (HTTPS),
69 /// otherwise `8080` (plain HTTP). See
70 /// [`ServerSettings::default_port`].
71 #[serde(default)]
72 pub port: Option<u16>,
73 /// Path to the TLS certificate (PEM).
74 pub cert: Option<String>,
75 /// Path to the TLS private key (PEM).
76 pub key: Option<String>,
77 /// How long a node-console WebSocket stays open without activity
78 /// before the server tears it down.
79 pub console_inactivity_timeout_secs: u64,
80 /// Per-source-IP rate limit for the `/api/v1/auth/*` endpoints,
81 /// in requests per minute. `None` disables in-process rate limiting
82 /// (operators are then expected to enforce it at the reverse proxy).
83 pub auth_rate_limit_per_minute: Option<u32>,
84 /// Global request timeout applied to every HTTP route, in seconds.
85 /// When this elapses the server returns `408 REQUEST_TIMEOUT`. All
86 /// long-running work (e.g. power transitions) now runs CLI-side,
87 /// so no endpoint needs more than the default.
88 #[serde(default = "default_request_timeout_secs")]
89 pub request_timeout_secs: u64,
90 /// Grace period (seconds) `axum_server` waits for in-flight
91 /// requests to finish after SIGTERM / Ctrl+C before force-aborting.
92 /// Matches the standard k8s `terminationGracePeriodSeconds` default
93 /// (30 s); pods that hit this without finishing get SIGKILL'd by
94 /// the kubelet.
95 #[serde(default = "default_shutdown_grace_period_secs")]
96 pub shutdown_grace_period_secs: u64,
97 /// Filesystem root that confines `POST /migrate/{backup,restore}`
98 /// file access. When set, every `destination` / `bos_file` /
99 /// `cfs_file` / `hsm_file` / `ims_file` / `image_dir` path in the
100 /// request is canonicalised and rejected unless it resolves under
101 /// this directory. When unset (default), the migrate endpoints
102 /// return `BadRequest` even for admin callers — the operator must
103 /// explicitly opt in to server-side filesystem writes.
104 #[serde(default)]
105 pub migrate_backup_root: Option<String>,
106 /// Opt in to plain-HTTP listen mode. Default `false`: when neither
107 /// `cert` nor `key` is configured the server refuses to start, so
108 /// bearer tokens can't accidentally land on the wire in cleartext.
109 /// Set to `true` only when TLS terminates upstream (reverse proxy
110 /// or sidecar); otherwise leave it off and configure both `cert`
111 /// and `key`.
112 #[serde(default)]
113 pub allow_http: bool,
114}
115
116impl ServerSettings {
117 /// Effective default listen address when neither config nor CLI flag
118 /// supplies one: bind on all interfaces.
119 pub const DEFAULT_LISTEN_ADDRESS: &'static str = "0.0.0.0";
120
121 /// Effective default port when neither config nor CLI flag supplies
122 /// one. `8443` for the HTTPS path (cert + key both present), `8080`
123 /// for plain HTTP — the latter is the typical dev / sidecar setup
124 /// where TLS is terminated upstream.
125 pub fn default_port(has_tls: bool) -> u16 {
126 if has_tls { 8443 } else { 8080 }
127 }
128}
129
130/// Default global request timeout — 600s (10 min). Bumped from 300s
131/// after `GET /cache/configuration` (and other cross-resource fan-out
132/// endpoints that hit CFS + BSS + IMS concurrently) started 408'ing
133/// against busy sites where any single upstream fetch can stall on
134/// the CSM Envoy reset window. 10 min still bounds truly hung
135/// requests while letting heavy-but-healthy ones through. Override
136/// via `request_timeout_secs` in `server.toml` if your deployment
137/// needs a different ceiling.
138fn default_shutdown_grace_period_secs() -> u64 {
139 30
140}
141
142fn default_request_timeout_secs() -> u64 {
143 600
144}
145
146/// Top-level configuration for the `manta-server` binary. Persisted as
147/// TOML under `~/.config/manta/server.toml`. Has no notion of an "active"
148/// site — the server hosts every configured site simultaneously and
149/// clients select per-request via the `X-Manta-Site` header.
150#[derive(Serialize, Deserialize, Debug)]
151pub struct ServerConfiguration {
152 /// `EnvFilter` directive for the tracing subscriber.
153 pub log: String,
154 /// Network / TLS / console / rate-limit knobs for the HTTPS server.
155 pub server: ServerSettings,
156 /// Per-site backend connection details, keyed by site name. The
157 /// `X-Manta-Site` header on each request picks which one to route to.
158 pub sites: HashMap<String, Site>,
159 /// Optional Kafka audit forwarder (typically used for `/auth/*`
160 /// attempts). When `None`, the server emits no audit messages.
161 pub auditor: Option<Auditor>,
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn site_deserialize_missing_backend_fails() {
170 let bad_toml = r#"
171 shasta_base_url = "https://api.example.com"
172 root_ca_cert_file = "cert.pem"
173 # missing backend
174 "#;
175 let result = toml::from_str::<Site>(bad_toml);
176 assert!(result.is_err());
177 }
178
179 #[test]
180 fn backend_technology_as_str() {
181 assert_eq!(BackendTechnology::Csm.as_str(), "csm");
182 assert_eq!(BackendTechnology::Ochami.as_str(), "ochami");
183 }
184
185 #[test]
186 fn backend_technology_roundtrip_toml() {
187 // Verify TOML serializes as lowercase "csm" / "ochami"
188 #[derive(Serialize, Deserialize)]
189 struct Wrapper {
190 backend: BackendTechnology,
191 }
192 let w = Wrapper {
193 backend: BackendTechnology::Csm,
194 };
195 let s = toml::to_string(&w).unwrap();
196 assert!(s.contains("\"csm\"") || s.contains("csm"));
197 let parsed: Wrapper = toml::from_str(&s).unwrap();
198 assert_eq!(parsed.backend, BackendTechnology::Csm);
199 }
200
201 fn make_minimal_site() -> Site {
202 Site {
203 backend: BackendTechnology::Csm,
204 socks5_proxy: None,
205 shasta_base_url: "https://api.example.com".to_string(),
206 k8s: None,
207 root_ca_cert_file: "cert.pem".to_string(),
208 }
209 }
210
211 #[test]
212 fn server_configuration_roundtrip_toml_minimal() {
213 let mut sites = HashMap::new();
214 sites.insert("alps".to_string(), make_minimal_site());
215 let cfg = ServerConfiguration {
216 log: "info".to_string(),
217 server: ServerSettings {
218 listen_address: Some("0.0.0.0".to_string()),
219 port: Some(8443),
220 cert: Some("/etc/manta/tls/server.crt".to_string()),
221 key: Some("/etc/manta/tls/server.key".to_string()),
222 console_inactivity_timeout_secs: 1800,
223 auth_rate_limit_per_minute: Some(60),
224 request_timeout_secs: 300,
225 shutdown_grace_period_secs: 30,
226 migrate_backup_root: None,
227 allow_http: false,
228 },
229 sites,
230 auditor: None,
231 };
232 let toml_str = toml::to_string(&cfg).unwrap();
233 let parsed: ServerConfiguration = toml::from_str(&toml_str).unwrap();
234 assert_eq!(parsed.server.port, Some(8443));
235 assert_eq!(parsed.server.listen_address.as_deref(), Some("0.0.0.0"));
236 assert_eq!(parsed.server.console_inactivity_timeout_secs, 1800);
237 assert_eq!(parsed.server.request_timeout_secs, 300);
238 assert_eq!(
239 parsed.server.cert.as_deref(),
240 Some("/etc/manta/tls/server.crt")
241 );
242 }
243
244 /// Default port helper: 8443 when TLS is configured, 8080
245 /// otherwise. Used by `manta-server::main` when no `port` is set
246 /// in config or on the CLI.
247 #[test]
248 fn server_settings_default_port_depends_on_tls() {
249 assert_eq!(ServerSettings::default_port(true), 8443);
250 assert_eq!(ServerSettings::default_port(false), 8080);
251 }
252
253 /// power_timeout_secs is gone — confirm the surrounding
254 /// timeout-related fields still default correctly when the only
255 /// remaining knob is absent.
256 #[test]
257 fn server_settings_request_timeout_secs_defaults_to_600() {
258 let toml_str = r#"
259 listen_address = "0.0.0.0"
260 port = 8443
261 console_inactivity_timeout_secs = 1800
262 "#;
263 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
264 assert_eq!(parsed.request_timeout_secs, 600);
265 }
266
267 /// `[server]` block with neither `listen_address` nor `port`
268 /// supplied — both fields deserialise as `None`, leaving the
269 /// effective values to be filled in at startup time. Confirms the
270 /// schema-level back-compat for the new defaults.
271 #[test]
272 fn server_settings_listen_address_and_port_default_to_none() {
273 let toml_str = r#"
274 console_inactivity_timeout_secs = 1800
275 "#;
276 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
277 assert!(parsed.listen_address.is_none());
278 assert!(parsed.port.is_none());
279 }
280
281 /// Existing server.toml files that pre-date the request_timeout
282 /// field must keep working — the field falls back to its default.
283 #[test]
284 fn server_settings_request_timeout_field_defaults_when_omitted() {
285 let toml_str = r#"
286 listen_address = "0.0.0.0"
287 port = 8443
288 console_inactivity_timeout_secs = 1800
289 "#;
290 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
291 assert_eq!(parsed.request_timeout_secs, 600);
292 }
293
294 #[test]
295 fn server_configuration_deserialize_missing_server_section_fails() {
296 let bad_toml = r#"
297 log = "info"
298 [sites]
299 "#;
300 let result = toml::from_str::<ServerConfiguration>(bad_toml);
301 assert!(result.is_err());
302 }
303
304 #[test]
305 fn server_settings_optional_tls_paths() {
306 // TLS cert/key are optional in the schema — flags can supply them
307 // at runtime when the config omits them.
308 let toml_str = r#"
309 listen_address = "0.0.0.0"
310 port = 8443
311 console_inactivity_timeout_secs = 1800
312 "#;
313 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
314 assert!(parsed.cert.is_none());
315 assert!(parsed.key.is_none());
316 }
317}