manta_shared/common/config/
mod.rs1use std::{
13 fs::{self, File},
14 io::{Read, Write},
15 path::PathBuf,
16};
17
18use crate::common::error::MantaError as Error;
19use config::Config;
20use directories::ProjectDirs;
21use toml_edit::DocumentMut;
22
23fn get_project_dirs() -> Result<ProjectDirs, Error> {
29 ProjectDirs::from(
30 "local", "cscs", "manta", )
34 .ok_or_else(|| {
35 Error::MissingField(
36 "Could not determine project directories \
37 (home directory may not be set)"
38 .to_string(),
39 )
40 })
41}
42
43pub fn get_default_config_path() -> Result<PathBuf, Error> {
46 Ok(PathBuf::from(get_project_dirs()?.config_dir()))
47}
48
49pub fn get_default_manta_config_file_path() -> Result<PathBuf, Error> {
55 let mut path = get_default_config_path()?;
56 path.push("config.toml");
57 Ok(path)
58}
59
60pub fn get_default_manta_cli_config_file_path() -> Result<PathBuf, Error> {
63 let mut path = get_default_config_path()?;
64 path.push("cli.toml");
65 Ok(path)
66}
67
68pub fn get_default_manta_server_config_file_path() -> Result<PathBuf, Error> {
71 let mut path = get_default_config_path()?;
72 path.push("server.toml");
73 Ok(path)
74}
75
76pub fn get_default_cache_path() -> Result<PathBuf, Error> {
79 Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
80}
81
82pub fn read_config_toml() -> Result<(PathBuf, DocumentMut), Error> {
88 let path = get_cli_config_file_path()?;
89
90 tracing::debug!(
91 "Reading manta CLI configuration from {}",
92 path.to_string_lossy()
93 );
94
95 let content = fs::read_to_string(&path)?;
96
97 let doc = content.parse::<DocumentMut>()?;
98
99 Ok((path, doc))
100}
101
102pub fn write_config_toml(
104 path: &std::path::Path,
105 doc: &DocumentMut,
106) -> Result<(), Error> {
107 let mut file = std::fs::OpenOptions::new()
108 .write(true)
109 .truncate(true)
110 .open(path)?;
111
112 file.write_all(doc.to_string().as_bytes())?;
113 file.flush()?;
114
115 Ok(())
116}
117
118pub fn get_csm_root_cert_content(file_path: &str) -> Result<Vec<u8>, Error> {
122 let mut buf = Vec::new();
123 let root_cert_file_rslt = File::open(file_path);
124
125 let file_rslt = if root_cert_file_rslt.is_err() {
126 let mut config_path = get_default_config_path()?;
127 config_path.push(file_path);
128 File::open(config_path)
129 } else {
130 root_cert_file_rslt
131 };
132
133 match file_rslt {
134 Ok(mut file) => {
135 file.read_to_end(&mut buf)?;
136 Ok(buf)
137 }
138 Err(_) => Err(Error::NotFound(
139 "CA public root file could not be found".to_string(),
140 )),
141 }
142}
143
144pub fn get_cli_config_file_path() -> Result<PathBuf, Error> {
146 if let Ok(env_path) = std::env::var("MANTA_CLI_CONFIG") {
147 Ok(PathBuf::from(env_path))
148 } else {
149 get_default_manta_cli_config_file_path()
150 }
151}
152
153pub fn get_server_config_file_path() -> Result<PathBuf, Error> {
155 if let Ok(env_path) = std::env::var("MANTA_SERVER_CONFIG") {
156 Ok(PathBuf::from(env_path))
157 } else {
158 get_default_manta_server_config_file_path()
159 }
160}
161
162const CLI_CONFIG_SAMPLE: &str = r#"log = "info"
164site = "<site_name>"
165manta_server_url = "https://manta-server.example.com:8443"
166
167# Timeout knobs. Values shown are the built-in defaults — delete a
168# line to fall back to the default, or change the value to override.
169#
170# Per-request HTTP timeout reaching `manta_server_url` (seconds).
171# Default 300 caps REST calls. Streams (SSE log tail, WS console)
172# are unlimited when this is absent; setting it caps streams too.
173request_timeout_secs = 300
174#
175# `manta power on/off/reset`: poll interval (seconds) and max
176# attempts before giving up. 300 × 3 s = 15 min total wait.
177power_poll_interval_secs = 3
178power_max_poll_attempts = 300
179#
180# `manta apply sat-file`: poll interval, overall hard cap, and
181# cap on consecutive "session not yet visible" responses (seconds).
182sat_file_poll_interval_secs = 10
183sat_file_poll_budget_secs = 14400 # 4 hours
184sat_file_not_visible_budget_secs = 300 # 5 minutes
185
186[sites.<site_name>]
187backend = "csm" # or "ochami"
188shasta_base_url = "https://api.example.com"
189root_ca_cert_file = "alps_root_cert.pem"
190"#;
191
192const CLI_CONFIG_MIGRATION: &str = "\
194Migration from ~/.config/manta/config.toml:
195 copy these fields verbatim: log, site, auditor, sites
196 add CLI-only (now required): manta_server_url = \"https://...\"
197 (CLI talks only to the manta server)
198 drop (no longer recognised): sites.<X>.manta_server_url, audit_file
199 do not copy (server-only fields): the [server] section belongs in
200 server.toml, not cli.toml";
201
202const SERVER_CONFIG_SAMPLE: &str = r#"log = "info"
204
205[server]
206listen_address = "0.0.0.0"
207port = 8443
208cert = "/path/to/server.crt"
209key = "/path/to/server.key"
210console_inactivity_timeout_secs = 1800
211auth_rate_limit_per_minute = 60 # per source IP for /auth/*; omit to disable
212# Values shown for the two timeout knobs are the built-in defaults —
213# delete a line to fall back to the default, or change to override.
214request_timeout_secs = 300 # global per-route timeout; returns 408 on expiry
215shutdown_grace_period_secs = 30 # drain window after SIGTERM / Ctrl+C; matches k8s terminationGracePeriodSeconds
216# allow_http = false # opt in to plain-HTTP listen when no cert/key is set
217 # (e.g. TLS terminated upstream). Default fail-closed.
218# Filesystem root for POST /migrate/{backup,restore}. Required for those
219# endpoints to work — the server will reject migrate requests with 400
220# while this is unset. Must be an absolute path to an existing directory.
221# migrate_backup_root = "/var/lib/manta/migrate"
222
223# [auditor.kafka] # optional: enable Kafka audit emission
224# brokers = ["kafka.example.com:9092"]
225# topic = "manta-audit"
226# message_timeout_ms = 5000 # librdkafka per-message delivery deadline; default 5000
227# delivery_wait_secs = 0 # how long produce_message blocks; 0 = fire-and-forget (default)
228
229[sites.<site_name>]
230backend = "csm"
231shasta_base_url = "https://api.example.com"
232root_ca_cert_file = "/path/to/alps_root_cert.pem"
233"#;
234
235const SERVER_CONFIG_MIGRATION: &str = "\
237Migration from ~/.config/manta/config.toml:
238 copy these fields verbatim: log, auditor, sites
239 add new [server] section: listen_address, port, cert, key,
240 console_inactivity_timeout_secs
241 drop (CLI-only): site, hsm_group, manta_server_url
242 drop (no longer recognised): sites.<X>.manta_server_url, audit_file";
243
244fn missing_config_message(
245 binary: &str,
246 expected_path: &std::path::Path,
247 sample: &str,
248 migration: &str,
249) -> String {
250 let legacy_exists = get_default_manta_config_file_path()
251 .map(|p| p.exists())
252 .unwrap_or(false);
253 let mut msg = format!(
254 "{binary} configuration file '{}' not found.\n\nMinimal example:\n\n{sample}",
255 expected_path.to_string_lossy()
256 );
257 if legacy_exists {
258 msg.push('\n');
259 msg.push_str(migration);
260 }
261 msg
262}
263
264pub fn get_cli_configuration() -> Result<Config, Error> {
268 let path = get_cli_config_file_path()?;
269 if !path.exists() {
270 return Err(Error::NotFound(missing_config_message(
271 "CLI",
272 &path,
273 CLI_CONFIG_SAMPLE,
274 CLI_CONFIG_MIGRATION,
275 )));
276 }
277 let path_str = path.to_str().ok_or_else(|| {
278 Error::MissingField(
279 "CLI configuration file path contains invalid UTF-8".to_string(),
280 )
281 })?;
282 ::config::Config::builder()
283 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
284 .add_source(
285 ::config::Environment::with_prefix("MANTA")
286 .try_parsing(true)
287 .prefix_separator("_"),
288 )
289 .build()
290 .map_err(Error::ConfigError)
291}
292
293pub fn get_server_configuration() -> Result<Config, Error> {
297 let path = get_server_config_file_path()?;
298 if !path.exists() {
299 return Err(Error::NotFound(missing_config_message(
300 "Server",
301 &path,
302 SERVER_CONFIG_SAMPLE,
303 SERVER_CONFIG_MIGRATION,
304 )));
305 }
306 let path_str = path.to_str().ok_or_else(|| {
307 Error::MissingField(
308 "Server configuration file path contains invalid UTF-8".to_string(),
309 )
310 })?;
311 ::config::Config::builder()
312 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
313 .add_source(
314 ::config::Environment::with_prefix("MANTA")
315 .try_parsing(true)
316 .prefix_separator("_"),
317 )
318 .build()
319 .map_err(Error::ConfigError)
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use std::io::Write;
326 use std::sync::Mutex;
327 use tempfile::NamedTempFile;
328
329 static ENV_LOCK: Mutex<()> = Mutex::new(());
333
334 struct EnvGuard(&'static str);
338 impl EnvGuard {
339 fn set(key: &'static str, value: &str) -> Self {
340 unsafe { std::env::set_var(key, value) };
342 Self(key)
343 }
344 }
345 impl Drop for EnvGuard {
346 fn drop(&mut self) {
347 unsafe { std::env::remove_var(self.0) };
348 }
349 }
350
351 fn write_tmp_toml(content: &str) -> NamedTempFile {
352 let mut f = NamedTempFile::new().expect("tempfile");
353 f.write_all(content.as_bytes()).expect("write tempfile");
354 f
355 }
356
357 #[test]
358 fn default_cli_config_path_ends_with_cli_toml() {
359 let path = get_default_manta_cli_config_file_path().unwrap();
360 assert_eq!(path.file_name().unwrap(), "cli.toml");
361 }
362
363 #[test]
364 fn default_server_config_path_ends_with_server_toml() {
365 let path = get_default_manta_server_config_file_path().unwrap();
366 assert_eq!(path.file_name().unwrap(), "server.toml");
367 }
368
369 #[test]
370 fn default_legacy_config_path_ends_with_config_toml() {
371 let path = get_default_manta_config_file_path().unwrap();
372 assert_eq!(path.file_name().unwrap(), "config.toml");
373 }
374
375 #[test]
376 fn cli_and_server_default_paths_share_parent() {
377 let cli = get_default_manta_cli_config_file_path().unwrap();
378 let server = get_default_manta_server_config_file_path().unwrap();
379 assert_eq!(cli.parent(), server.parent());
380 }
381
382 #[test]
383 fn cli_config_file_path_honors_env_var() {
384 let _g = ENV_LOCK.lock().unwrap();
385 let _e = EnvGuard::set("MANTA_CLI_CONFIG", "/tmp/custom-cli.toml");
386 let path = get_cli_config_file_path().unwrap();
387 assert_eq!(path, PathBuf::from("/tmp/custom-cli.toml"));
388 }
389
390 #[test]
391 fn server_config_file_path_honors_env_var() {
392 let _g = ENV_LOCK.lock().unwrap();
393 let _e = EnvGuard::set("MANTA_SERVER_CONFIG", "/tmp/custom-server.toml");
394 let path = get_server_config_file_path().unwrap();
395 assert_eq!(path, PathBuf::from("/tmp/custom-server.toml"));
396 }
397
398 #[test]
399 fn cli_configuration_with_missing_file_returns_notfound() {
400 let _g = ENV_LOCK.lock().unwrap();
401 let _e = EnvGuard::set(
402 "MANTA_CLI_CONFIG",
403 "/nonexistent-dir/definitely-not-here.toml",
404 );
405 let err = get_cli_configuration().unwrap_err();
406 match err {
407 Error::NotFound(msg) => {
408 assert!(
409 msg.contains("CLI configuration file"),
410 "expected helpful NotFound message, got: {msg}"
411 );
412 assert!(
413 msg.contains("Minimal example"),
414 "expected sample TOML in message"
415 );
416 }
417 other => panic!("expected NotFound, got {other:?}"),
418 }
419 }
420
421 #[test]
422 fn cli_configuration_with_malformed_toml_returns_config_error() {
423 let _g = ENV_LOCK.lock().unwrap();
424 let bad = write_tmp_toml("this is = not [valid toml");
425 let _e = EnvGuard::set("MANTA_CLI_CONFIG", bad.path().to_str().unwrap());
426 let err = get_cli_configuration().unwrap_err();
427 assert!(
428 matches!(err, Error::ConfigError(_)),
429 "expected ConfigError variant, got {err:?}"
430 );
431 }
432
433 #[test]
434 fn cli_configuration_loads_valid_toml_and_env_var_overrides_file() {
435 let _g = ENV_LOCK.lock().unwrap();
436 let good = write_tmp_toml(
437 r#"log = "info"
438site = "alps"
439manta_server_url = "https://example:8443"
440"#,
441 );
442 let _path =
443 EnvGuard::set("MANTA_CLI_CONFIG", good.path().to_str().unwrap());
444
445 let cfg = get_cli_configuration().unwrap();
446 assert_eq!(cfg.get_string("log").unwrap(), "info");
447 assert_eq!(cfg.get_string("site").unwrap(), "alps");
448 drop(cfg);
449
450 let _override = EnvGuard::set("MANTA_LOG", "trace");
453 let cfg = get_cli_configuration().unwrap();
454 assert_eq!(
455 cfg.get_string("log").unwrap(),
456 "trace",
457 "env var should override file value"
458 );
459 }
460
461 #[test]
462 fn server_configuration_with_missing_file_returns_notfound() {
463 let _g = ENV_LOCK.lock().unwrap();
464 let _e = EnvGuard::set(
465 "MANTA_SERVER_CONFIG",
466 "/nonexistent-dir/missing-server.toml",
467 );
468 let err = get_server_configuration().unwrap_err();
469 match err {
470 Error::NotFound(msg) => {
471 assert!(
472 msg.contains("Server configuration file"),
473 "expected helpful NotFound message, got: {msg}"
474 );
475 }
476 other => panic!("expected NotFound, got {other:?}"),
477 }
478 }
479
480 #[test]
481 fn server_configuration_loads_valid_toml() {
482 let _g = ENV_LOCK.lock().unwrap();
483 let good = write_tmp_toml(
484 r#"log = "info"
485
486[server]
487listen_address = "0.0.0.0"
488port = 8443
489cert = "/etc/manta/cert.pem"
490key = "/etc/manta/key.pem"
491"#,
492 );
493 let _e =
494 EnvGuard::set("MANTA_SERVER_CONFIG", good.path().to_str().unwrap());
495 let cfg = get_server_configuration().unwrap();
496 assert_eq!(cfg.get_string("server.listen_address").unwrap(), "0.0.0.0");
497 assert_eq!(cfg.get_int("server.port").unwrap(), 8443);
498 }
499}