objectstore_server/
config.rs

1//! Configuration for the objectstore server.
2//!
3//! This module provides the configuration system for the objectstore HTTP server. Configuration can
4//! be loaded from multiple sources with the following precedence (highest to lowest):
5//!
6//! 1. Environment variables (prefixed with `OS__`)
7//! 2. YAML configuration file (specified via `-c` or `--config` flag)
8//! 3. Defaults
9//!
10//! See [`Config`] for a description of all configuration fields and their defaults.
11//!
12//! # Environment Variables
13//!
14//! Environment variables use `OS__` as a prefix and double underscores (`__`) to denote nested
15//! configuration structures. For example:
16//!
17//! - `OS__HTTP_ADDR=0.0.0.0:8888` sets the HTTP server address
18//! - `OS__STORAGE__TYPE=filesystem` sets the storage type
19//! - `OS__STORAGE__PATH=/data` sets the directory path
20//!
21//! # YAML Configuration File
22//!
23//! Configuration can also be provided via a YAML file. The above configuration in YAML format would
24//! look like this:
25//!
26//! ```yaml
27//! http_addr: 0.0.0.0:8888
28//!
29//! storage:
30//!   type: filesystem
31//!   path: /data
32//! ```
33
34use std::borrow::Cow;
35use std::collections::{BTreeMap, HashSet};
36use std::fmt;
37use std::net::SocketAddr;
38use std::path::{Path, PathBuf};
39use std::time::Duration;
40
41use anyhow::Result;
42use figment::providers::{Env, Format, Serialized, Yaml};
43use objectstore_service::backend::local_fs::FileSystemConfig;
44use objectstore_types::auth::Permission;
45use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
46use serde::{Deserialize, Serialize};
47
48pub use objectstore_log::{LevelFilter, LogFormat, LoggingConfig};
49pub use objectstore_service::backend::StorageConfig;
50
51use crate::killswitches::Killswitches;
52use crate::rate_limits::RateLimits;
53
54/// Environment variable prefix for all configuration options.
55const ENV_PREFIX: &str = "OS__";
56
57/// Newtype around `String` that may protect against accidental
58/// logging of secrets in our configuration struct. Use with
59/// [`secrecy::SecretBox`].
60#[derive(Clone, Default, Serialize, Deserialize, PartialEq)]
61pub struct ConfigSecret(String);
62
63impl ConfigSecret {
64    /// Returns the secret value as a string slice.
65    pub fn as_str(&self) -> &str {
66        self.0.as_str()
67    }
68}
69
70impl From<&str> for ConfigSecret {
71    fn from(str: &str) -> Self {
72        ConfigSecret(str.to_string())
73    }
74}
75
76impl std::ops::Deref for ConfigSecret {
77    type Target = str;
78    fn deref(&self) -> &Self::Target {
79        &self.0
80    }
81}
82
83impl fmt::Debug for ConfigSecret {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
85        write!(f, "[redacted]")
86    }
87}
88
89impl CloneableSecret for ConfigSecret {}
90impl SerializableSecret for ConfigSecret {}
91impl Zeroize for ConfigSecret {
92    fn zeroize(&mut self) {
93        self.0.zeroize();
94    }
95}
96
97/// Runtime configuration for the Tokio async runtime.
98///
99/// Controls the threading behavior of the server's async runtime.
100///
101/// Used in: [`Config::runtime`]
102#[derive(Debug, Deserialize, Serialize)]
103#[serde(default)]
104pub struct Runtime {
105    /// Number of worker threads for the server runtime.
106    ///
107    /// This controls the size of the Tokio thread pool used to execute async tasks. More threads
108    /// can improve concurrency for CPU-bound workloads, but too many threads can increase context
109    /// switching overhead.
110    ///
111    /// Set this in accordance with the resources available to the server, especially in Kubernetes
112    /// environments.
113    ///
114    /// # Default
115    ///
116    /// Defaults to the number of CPU cores on the host machine.
117    ///
118    /// # Environment Variable
119    ///
120    /// `OS__RUNTIME__WORKER_THREADS`
121    ///
122    /// # Considerations
123    ///
124    /// - For I/O-bound workloads, the default (number of CPU cores) is usually sufficient
125    /// - For CPU-intensive workloads, consider matching or exceeding the number of cores
126    /// - Setting this too high can lead to increased memory usage and context switching
127    pub worker_threads: usize,
128
129    /// Interval in seconds for reporting internal runtime metrics.
130    ///
131    /// Defaults to `10` seconds.
132    #[serde(with = "humantime_serde")]
133    pub metrics_interval: Duration,
134}
135
136impl Default for Runtime {
137    fn default() -> Self {
138        Self {
139            worker_threads: num_cpus::get(),
140            metrics_interval: Duration::from_secs(10),
141        }
142    }
143}
144
145/// [Sentry](https://sentry.io/) error tracking and performance monitoring configuration.
146///
147/// Configures integration with Sentry for error tracking, performance monitoring, and distributed
148/// tracing. Sentry is disabled by default and only enabled when a DSN is provided.
149///
150/// Used in: [`Config::sentry`]
151#[derive(Debug, Deserialize, Serialize)]
152pub struct Sentry {
153    /// Sentry DSN (Data Source Name).
154    ///
155    /// When set, enables Sentry error tracking and performance monitoring. When `None`, Sentry
156    /// integration is completely disabled.
157    ///
158    /// # Default
159    ///
160    /// `None` (Sentry disabled)
161    ///
162    /// # Environment Variable
163    ///
164    /// `OS__SENTRY__DSN`
165    pub dsn: Option<SecretBox<ConfigSecret>>,
166
167    /// Environment name for this deployment.
168    ///
169    /// Used to distinguish events from different environments (e.g., "production", "staging",
170    /// "development"). This appears in the Sentry UI and can be used for filtering.
171    ///
172    /// # Default
173    ///
174    /// `None`
175    ///
176    /// # Environment Variable
177    ///
178    /// `OS__SENTRY__ENVIRONMENT`
179    pub environment: Option<Cow<'static, str>>,
180
181    /// Server name or identifier.
182    ///
183    /// Used to identify which server instance sent an event. Useful in multi-server deployments for
184    /// tracking which instance encountered an error. Set to the hostname or pod name of the server.
185    ///
186    /// # Default
187    ///
188    /// `None`
189    ///
190    /// # Environment Variable
191    ///
192    /// `OS__SENTRY__SERVER_NAME`
193    pub server_name: Option<Cow<'static, str>>,
194
195    /// Error event sampling rate.
196    ///
197    /// Controls what percentage of error events are sent to Sentry. A value of `1.0` sends all
198    /// errors, while `0.5` sends 50% of errors, and `0.0` sends no errors.
199    ///
200    /// # Default
201    ///
202    /// `1.0` (send all errors)
203    ///
204    /// # Environment Variable
205    ///
206    /// `OS__SENTRY__SAMPLE_RATE`
207    pub sample_rate: f32,
208
209    /// Performance trace sampling rate.
210    ///
211    /// Controls what percentage of transactions (traces) are sent to Sentry for performance
212    /// monitoring. A value of `1.0` sends all traces, while `0.01` sends 1% of traces.
213    ///
214    /// **Important**: Performance traces can generate significant data volume in high-traffic
215    /// systems. Start with a low rate (0.01-0.1) and adjust based on traffic and Sentry quota.
216    ///
217    /// # Default
218    ///
219    /// `0.01` (send 1% of traces)
220    ///
221    /// # Environment Variable
222    ///
223    /// `OS__SENTRY__TRACES_SAMPLE_RATE`
224    pub traces_sample_rate: f32,
225
226    /// Whether to inherit sampling decisions from incoming traces.
227    ///
228    /// When `true` (default), if an incoming request contains a distributed tracing header with a
229    /// sampling decision (e.g., from an upstream service), that decision is honored. When `false`,
230    /// the local `traces_sample_rate` is always used instead.
231    ///
232    /// When this is enabled, the calling service effectively controls the sampling decision for the
233    /// entire trace. Set this to `false` if you want to have independent sampling control at the
234    /// objectstore level.
235    ///
236    /// # Default
237    ///
238    /// `true`
239    ///
240    /// # Environment Variable
241    ///
242    /// `OS__SENTRY__INHERIT_SAMPLING_DECISION`
243    pub inherit_sampling_decision: bool,
244
245    /// Enable Sentry SDK debug mode.
246    ///
247    /// When enabled, the Sentry SDK will output debug information to stderr, which can be useful
248    /// for troubleshooting Sentry integration issues. It is discouraged to enable this in
249    /// production as it generates verbose logging.
250    ///
251    /// # Default
252    ///
253    /// `false`
254    ///
255    /// # Environment Variable
256    ///
257    /// `OS__SENTRY__DEBUG`
258    pub debug: bool,
259
260    /// Additional tags to attach to all Sentry events.
261    ///
262    /// Key-value pairs that are sent as tags with every event reported to Sentry. Useful for adding
263    /// context such as deployment identifiers or environment details.
264    ///
265    /// # Default
266    ///
267    /// Empty (no tags)
268    ///
269    /// # Environment Variables
270    ///
271    /// Each tag is set individually:
272    /// - `OS__SENTRY__TAGS__FOO=foo`
273    /// - `OS__SENTRY__TAGS__BAR=bar`
274    ///
275    /// # YAML Example
276    ///
277    /// ```yaml
278    /// sentry:
279    ///   tags:
280    ///     foo: foo
281    ///     bar: bar
282    /// ```
283    pub tags: BTreeMap<String, String>,
284}
285
286impl Sentry {
287    /// Returns whether Sentry integration is enabled.
288    ///
289    /// Sentry is considered enabled if a DSN is configured.
290    pub fn is_enabled(&self) -> bool {
291        self.dsn.is_some()
292    }
293}
294
295impl Default for Sentry {
296    fn default() -> Self {
297        Self {
298            dsn: None,
299            environment: None,
300            server_name: None,
301            sample_rate: 1.0,
302            traces_sample_rate: 0.01,
303            inherit_sampling_decision: true,
304            debug: false,
305            tags: BTreeMap::new(),
306        }
307    }
308}
309
310// Logging configuration is defined in `objectstore_log::LoggingConfig`.
311// Metrics configuration is defined in `objectstore_metrics::MetricsConfig`.
312
313/// A key that may be used to verify a request's `Authorization` header and its
314/// associated permissions. May contain multiple key versions to facilitate rotation.
315#[derive(Debug, Deserialize, Serialize)]
316pub struct AuthZVerificationKey {
317    /// Files that contain versions of this key's key material which may be used to verify
318    /// signatures.
319    ///
320    /// If a key is being rotated, the old and new versions of that key should both be
321    /// configured so objectstore can verify signatures while the updated key is still
322    /// rolling out. Otherwise, this should only contain the most recent version of a key.
323    pub key_files: Vec<PathBuf>,
324
325    /// The maximum set of permissions that this key's signer is authorized to grant.
326    ///
327    /// If a request's `Authorization` header grants full permission but it was signed by
328    /// a key that is only allowed to grant read permission, then the request only has
329    /// read permission.
330    #[serde(default)]
331    pub max_permissions: HashSet<Permission>,
332}
333
334/// Configuration for content-based authorization.
335#[derive(Debug, Default, Deserialize, Serialize)]
336pub struct AuthZ {
337    /// Whether to enforce content-based authorization or not.
338    ///
339    /// If this is set to `false`, checks are still performed but failures will not result
340    /// in `403 Unauthorized` responses.
341    pub enforce: bool,
342
343    /// Keys that may be used to verify a request's `Authorization` header.
344    ///
345    /// This field is a container that is keyed on a key's ID. When verifying a JWT
346    /// from the `Authorization` header, the `kid` field should be read from the JWT
347    /// header and used to index into this map to select the appropriate key.
348    #[serde(default)]
349    pub keys: BTreeMap<String, AuthZVerificationKey>,
350}
351
352/// Main configuration struct for the objectstore server.
353///
354/// This is the top-level configuration that combines all server settings including networking,
355/// storage backends, runtime, and observability options.
356///
357/// Configuration is loaded with the following precedence (highest to lowest):
358/// 1. Environment variables (prefixed with `OS__`)
359/// 2. YAML configuration file (if provided via `-c` flag)
360/// 3. Default values
361///
362/// See individual field documentation for details on each configuration option, including
363/// defaults and environment variables.
364#[derive(Debug, Deserialize, Serialize)]
365pub struct Config {
366    /// HTTP server bind address.
367    ///
368    /// The socket address (IP and port) where the HTTP server will listen for incoming
369    /// connections. Supports both IPv4 and IPv6 addresses. Note that binding to `0.0.0.0`
370    /// makes the server accessible from all network interfaces.
371    ///
372    /// # Default
373    ///
374    /// `0.0.0.0:8888` (listens on all network interfaces, port 8888)
375    ///
376    /// # Environment Variable
377    ///
378    /// `OS__HTTP_ADDR`
379    pub http_addr: SocketAddr,
380
381    /// Storage backend configuration.
382    ///
383    /// Configures the storage backend used by the server. Use `type: "filesystem"` for
384    /// development, `type: "tiered"` for production two-tier routing (small objects to a
385    /// high-volume backend, large objects to a long-term backend), or any other single backend
386    /// type for simple deployments.
387    ///
388    /// # Default
389    ///
390    /// Filesystem storage in the `./data` directory
391    ///
392    /// # Environment Variables
393    ///
394    /// - `OS__STORAGE__TYPE` — backend type (`filesystem`, `tiered`, `gcs`, `bigtable`,
395    ///   `s3compatible`)
396    /// - Additional fields depending on the type (see [`StorageConfig`])
397    ///
398    /// For tiered storage, sub-backend fields are nested under `high_volume` and `long_term`:
399    /// - `OS__STORAGE__TYPE=tiered`
400    /// - `OS__STORAGE__HIGH_VOLUME__TYPE=bigtable`
401    /// - `OS__STORAGE__LONG_TERM__TYPE=gcs`
402    ///
403    /// # Example (tiered)
404    ///
405    /// ```yaml
406    /// storage:
407    ///   type: tiered
408    ///   high_volume:
409    ///     type: bigtable
410    ///     project_id: my-project
411    ///     instance_name: objectstore
412    ///     table_name: objectstore
413    ///   long_term:
414    ///     type: gcs
415    ///     bucket: my-objectstore-bucket
416    /// ```
417    pub storage: StorageConfig,
418
419    /// Configuration of the internal task runtime.
420    ///
421    /// Controls the thread pool size and behavior of the async runtime powering the server.
422    /// See [`Runtime`] for configuration options.
423    pub runtime: Runtime,
424
425    /// Logging configuration.
426    ///
427    /// Controls log verbosity and output format. See [`LoggingConfig`] for configuration options.
428    pub logging: LoggingConfig,
429
430    /// Sentry error tracking configuration.
431    ///
432    /// Optional integration with Sentry for error tracking and performance monitoring.
433    /// See [`Sentry`] for configuration options.
434    pub sentry: Sentry,
435
436    /// Internal metrics configuration.
437    ///
438    /// Configures submission of internal metrics to a DogStatsD-compatible endpoint.
439    /// See [`objectstore_metrics::MetricsConfig`] for configuration options.
440    pub metrics: objectstore_metrics::MetricsConfig,
441
442    /// Content-based authorization configuration.
443    ///
444    /// Controls the verification and enforcement of content-based access control based on the
445    /// JWT in a request's `Authorization` header.
446    pub auth: AuthZ,
447
448    /// A list of matchers for requests to discard without processing.
449    pub killswitches: Killswitches,
450
451    /// Definitions for rate limits to enforce on incoming requests.
452    pub rate_limits: RateLimits,
453
454    /// Configuration for the [`StorageService`](objectstore_service::StorageService).
455    pub service: Service,
456
457    /// Configuration for the HTTP layer.
458    ///
459    /// Controls HTTP-level settings that operate before requests reach the
460    /// storage service. See [`Http`] for configuration options.
461    pub http: Http,
462}
463
464/// Configuration for the [`StorageService`](objectstore_service::StorageService).
465///
466/// Controls operational parameters of the storage service layer that sits
467/// between the HTTP server and the storage backends.
468///
469/// Used in: [`Config::service`]
470///
471/// # Environment Variables
472///
473/// - `OS__SERVICE__MAX_CONCURRENCY`
474#[derive(Debug, Deserialize, Serialize)]
475#[serde(default)]
476pub struct Service {
477    /// Maximum number of concurrent backend operations.
478    ///
479    /// This caps the total number of in-flight storage operations (reads,
480    /// writes, deletes) across all requests. Operations that exceed the limit
481    /// are rejected with HTTP 429.
482    ///
483    /// # Default
484    ///
485    /// [`DEFAULT_CONCURRENCY_LIMIT`](objectstore_service::service::DEFAULT_CONCURRENCY_LIMIT)
486    pub max_concurrency: usize,
487}
488
489impl Default for Service {
490    fn default() -> Self {
491        Self {
492            max_concurrency: objectstore_service::service::DEFAULT_CONCURRENCY_LIMIT,
493        }
494    }
495}
496
497/// Default maximum number of concurrent in-flight HTTP requests.
498///
499/// Requests beyond this limit are rejected with HTTP 503.
500pub const DEFAULT_MAX_HTTP_REQUESTS: usize = 10_000;
501
502/// Configuration for the HTTP layer.
503///
504/// Controls behaviour at the HTTP request level, before requests reach the
505/// storage service. Grouping these settings separately from [`Service`] keeps
506/// HTTP-layer and service-layer concerns distinct and provides a natural home
507/// for future HTTP-level settings (e.g. timeouts, body size limits).
508///
509/// Used in: [`Config::http`]
510///
511/// # Environment Variables
512///
513/// - `OS__HTTP__MAX_REQUESTS`
514#[derive(Debug, Deserialize, Serialize)]
515#[serde(default)]
516pub struct Http {
517    /// Maximum number of concurrent in-flight HTTP requests.
518    ///
519    /// This is a flood protection limit. When the number of requests currently
520    /// being processed reaches this value, new requests are rejected immediately
521    /// with HTTP 503. Health and readiness endpoints (`/health`, `/ready`) are
522    /// excluded from this limit.
523    ///
524    /// Unlike readiness-based backpressure, direct rejection responds in
525    /// milliseconds and recovers the moment any in-flight request completes.
526    ///
527    /// # Default
528    ///
529    /// [`DEFAULT_MAX_HTTP_REQUESTS`]
530    ///
531    /// # Environment Variable
532    ///
533    /// `OS__HTTP__MAX_REQUESTS`
534    pub max_requests: usize,
535}
536
537impl Default for Http {
538    fn default() -> Self {
539        Self {
540            max_requests: DEFAULT_MAX_HTTP_REQUESTS,
541        }
542    }
543}
544
545impl Default for Config {
546    fn default() -> Self {
547        Self {
548            http_addr: "0.0.0.0:8888".parse().unwrap(),
549
550            storage: StorageConfig::FileSystem(FileSystemConfig {
551                path: PathBuf::from("data"),
552            }),
553
554            runtime: Runtime::default(),
555            logging: LoggingConfig::default(),
556            sentry: Sentry::default(),
557            metrics: objectstore_metrics::MetricsConfig::default(),
558            auth: AuthZ::default(),
559            killswitches: Killswitches::default(),
560            rate_limits: RateLimits::default(),
561            service: Service::default(),
562            http: Http::default(),
563        }
564    }
565}
566
567impl Config {
568    /// Loads configuration from the provided arguments.
569    ///
570    /// Configuration is merged in the following order (later sources override earlier ones):
571    /// 1. Default values
572    /// 2. YAML configuration file (if provided in `args`)
573    /// 3. Environment variables (prefixed with `OS__`)
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if:
578    /// - The YAML configuration file cannot be read or parsed
579    /// - Environment variables contain invalid values
580    /// - Required fields are missing or invalid
581    pub fn load(path: Option<&Path>) -> Result<Self> {
582        let mut figment = figment::Figment::from(Serialized::defaults(Config::default()));
583        if let Some(path) = path {
584            figment = figment.merge(Yaml::file(path));
585        }
586        let config = figment
587            .merge(Env::prefixed(ENV_PREFIX).split("__"))
588            .extract()?;
589
590        Ok(config)
591    }
592}
593
594#[cfg(test)]
595#[expect(
596    clippy::result_large_err,
597    reason = "figment::Error is inherently large"
598)]
599mod tests {
600    use std::io::Write;
601
602    use objectstore_service::backend::HighVolumeStorageConfig;
603    use secrecy::ExposeSecret;
604
605    use crate::killswitches::Killswitch;
606    use crate::rate_limits::{BandwidthLimits, RateLimits, ThroughputLimits, ThroughputRule};
607
608    use super::*;
609
610    #[test]
611    fn configurable_via_env() {
612        figment::Jail::expect_with(|jail| {
613            jail.set_env("OS__STORAGE__TYPE", "s3compatible");
614            jail.set_env("OS__STORAGE__ENDPOINT", "http://localhost:8888");
615            jail.set_env("OS__STORAGE__BUCKET", "whatever");
616            jail.set_env("OS__METRICS__TAGS__FOO", "bar");
617            jail.set_env("OS__METRICS__TAGS__BAZ", "qux");
618            jail.set_env("OS__SENTRY__DSN", "abcde");
619            jail.set_env("OS__SENTRY__SAMPLE_RATE", "0.5");
620            jail.set_env("OS__SENTRY__ENVIRONMENT", "production");
621            jail.set_env("OS__SENTRY__SERVER_NAME", "objectstore-deadbeef");
622            jail.set_env("OS__SENTRY__TRACES_SAMPLE_RATE", "0.5");
623
624            let config = Config::load(None).unwrap();
625
626            let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
627                panic!("expected s3 storage");
628            };
629            assert_eq!(c.endpoint, "http://localhost:8888");
630            assert_eq!(c.bucket, "whatever");
631            assert_eq!(
632                config.metrics.tags,
633                [("foo".into(), "bar".into()), ("baz".into(), "qux".into())].into()
634            );
635
636            assert_eq!(config.sentry.dsn.unwrap().expose_secret().as_str(), "abcde");
637            assert_eq!(config.sentry.environment.as_deref(), Some("production"));
638            assert_eq!(
639                config.sentry.server_name.as_deref(),
640                Some("objectstore-deadbeef")
641            );
642            assert_eq!(config.sentry.sample_rate, 0.5);
643            assert_eq!(config.sentry.traces_sample_rate, 0.5);
644
645            Ok(())
646        });
647    }
648
649    #[test]
650    fn configurable_via_yaml() {
651        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
652        tempfile
653            .write_all(
654                br#"
655            storage:
656                type: s3compatible
657                endpoint: http://localhost:8888
658                bucket: whatever
659            sentry:
660                dsn: abcde
661                environment: production
662                server_name: objectstore-deadbeef
663                sample_rate: 0.5
664                traces_sample_rate: 0.5
665            "#,
666            )
667            .unwrap();
668
669        figment::Jail::expect_with(|_jail| {
670            let config = Config::load(Some(tempfile.path())).unwrap();
671
672            let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
673                panic!("expected s3 storage");
674            };
675            assert_eq!(c.endpoint, "http://localhost:8888");
676            assert_eq!(c.bucket, "whatever");
677
678            assert_eq!(config.sentry.dsn.unwrap().expose_secret().as_str(), "abcde");
679            assert_eq!(config.sentry.environment.as_deref(), Some("production"));
680            assert_eq!(
681                config.sentry.server_name.as_deref(),
682                Some("objectstore-deadbeef")
683            );
684            assert_eq!(config.sentry.sample_rate, 0.5);
685            assert_eq!(config.sentry.traces_sample_rate, 0.5);
686
687            Ok(())
688        });
689    }
690
691    #[test]
692    fn configured_with_env_and_yaml() {
693        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
694        tempfile
695            .write_all(
696                br#"
697            storage:
698                type: s3compatible
699                endpoint: http://localhost:8888
700                bucket: whatever
701            "#,
702            )
703            .unwrap();
704
705        figment::Jail::expect_with(|jail| {
706            jail.set_env("OS__STORAGE__ENDPOINT", "http://localhost:9001");
707
708            let config = Config::load(Some(tempfile.path())).unwrap();
709
710            let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
711                panic!("expected s3 storage");
712            };
713            // Env should overwrite the yaml config
714            assert_eq!(c.endpoint, "http://localhost:9001");
715
716            Ok(())
717        });
718    }
719
720    #[test]
721    fn tiered_storage_via_yaml() {
722        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
723        tempfile
724            .write_all(
725                br#"
726            storage:
727                type: tiered
728                high_volume:
729                    type: bigtable
730                    project_id: my-project
731                    instance_name: objectstore
732                    table_name: objectstore
733                long_term:
734                    type: gcs
735                    bucket: my-objectstore-bucket
736            "#,
737            )
738            .unwrap();
739
740        figment::Jail::expect_with(|_jail| {
741            let config = Config::load(Some(tempfile.path())).unwrap();
742
743            let StorageConfig::Tiered(c) = &dbg!(&config).storage else {
744                panic!("expected tiered storage");
745            };
746            let HighVolumeStorageConfig::BigTable(hv) = &c.high_volume;
747            assert_eq!(hv.project_id, "my-project");
748            let StorageConfig::Gcs(lt) = c.long_term.as_ref() else {
749                panic!("expected gcs long_term");
750            };
751            assert_eq!(lt.bucket, "my-objectstore-bucket");
752
753            Ok(())
754        });
755    }
756
757    #[test]
758    fn tiered_storage_via_env() {
759        figment::Jail::expect_with(|jail| {
760            jail.set_env("OS__STORAGE__TYPE", "tiered");
761            jail.set_env("OS__STORAGE__HIGH_VOLUME__TYPE", "bigtable");
762            jail.set_env("OS__STORAGE__HIGH_VOLUME__PROJECT_ID", "my-project");
763            jail.set_env("OS__STORAGE__HIGH_VOLUME__INSTANCE_NAME", "my-instance");
764            jail.set_env("OS__STORAGE__HIGH_VOLUME__TABLE_NAME", "my-table");
765            jail.set_env("OS__STORAGE__LONG_TERM__TYPE", "filesystem");
766            jail.set_env("OS__STORAGE__LONG_TERM__PATH", "/data/lt");
767
768            let config = Config::load(None).unwrap();
769
770            let StorageConfig::Tiered(c) = &dbg!(&config).storage else {
771                panic!("expected tiered storage");
772            };
773            let HighVolumeStorageConfig::BigTable(hv) = &c.high_volume;
774            assert_eq!(hv.project_id, "my-project");
775            assert_eq!(hv.instance_name, "my-instance");
776            assert_eq!(hv.table_name, "my-table");
777            let StorageConfig::FileSystem(lt) = c.long_term.as_ref() else {
778                panic!("expected filesystem long_term");
779            };
780            assert_eq!(lt.path, Path::new("/data/lt"));
781
782            Ok(())
783        });
784    }
785
786    #[test]
787    fn metrics_addr_via_env() {
788        figment::Jail::expect_with(|jail| {
789            jail.set_env("OS__METRICS__ADDR", "127.0.0.1:8125");
790
791            let config = Config::load(None).unwrap();
792            assert_eq!(config.metrics.addr.as_deref(), Some("127.0.0.1:8125"));
793
794            Ok(())
795        });
796    }
797
798    #[test]
799    fn configure_auth_with_env() {
800        figment::Jail::expect_with(|jail| {
801            jail.set_env("OS__AUTH__ENFORCE", "true");
802            jail.set_env(
803                "OS__AUTH__KEYS",
804                r#"{kid1={key_files=["abcde","fghij","this is a test\n  multiline string\nend of string\n"],max_permissions=["object.read", "object.write"],}, kid2={key_files=["12345"],}}"#,
805            );
806
807            let config = Config::load(None).unwrap();
808
809            assert!(config.auth.enforce);
810
811            let kid1 = config.auth.keys.get("kid1").unwrap();
812            assert_eq!(kid1.key_files[0], Path::new("abcde"));
813            assert_eq!(kid1.key_files[1], Path::new("fghij"));
814            assert_eq!(
815                kid1.key_files[2],
816                Path::new("this is a test\n  multiline string\nend of string\n"),
817            );
818            assert_eq!(
819                kid1.max_permissions,
820                HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
821            );
822
823            let kid2 = config.auth.keys.get("kid2").unwrap();
824            assert_eq!(kid2.key_files[0], Path::new("12345"));
825            assert_eq!(kid2.max_permissions, HashSet::new());
826
827            Ok(())
828        });
829    }
830
831    #[test]
832    fn configure_auth_with_yaml() {
833        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
834        tempfile
835            .write_all(
836                br#"
837                auth:
838                    enforce: true
839                    keys:
840                        kid1:
841                            key_files:
842                                - "abcde"
843                                - "fghij"
844                                - |
845                                  this is a test
846                                    multiline string
847                                  end of string
848                            max_permissions:
849                                - "object.read"
850                                - "object.write"
851                        kid2:
852                            key_files:
853                                - "12345"
854            "#,
855            )
856            .unwrap();
857
858        figment::Jail::expect_with(|_jail| {
859            let config = Config::load(Some(tempfile.path())).unwrap();
860
861            assert!(config.auth.enforce);
862
863            let kid1 = config.auth.keys.get("kid1").unwrap();
864            assert_eq!(kid1.key_files[0], Path::new("abcde"));
865            assert_eq!(kid1.key_files[1], Path::new("fghij"));
866            assert_eq!(
867                kid1.key_files[2],
868                Path::new("this is a test\n  multiline string\nend of string\n")
869            );
870            assert_eq!(
871                kid1.max_permissions,
872                HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
873            );
874
875            let kid2 = config.auth.keys.get("kid2").unwrap();
876            assert_eq!(kid2.key_files[0], Path::new("12345"));
877            assert_eq!(kid2.max_permissions, HashSet::new());
878
879            Ok(())
880        });
881    }
882
883    #[test]
884    fn configure_killswitches_with_yaml() {
885        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
886        tempfile
887            .write_all(
888                br#"
889                killswitches:
890                  - usecase: broken_usecase
891                  - scopes:
892                      org: "42"
893                  - service: "test-*"
894                  - scopes:
895                      org: "42"
896                      project: "4711"
897                  - usecase: attachments
898                    scopes:
899                      org: "42"
900                    service: "test-*"
901                "#,
902            )
903            .unwrap();
904
905        figment::Jail::expect_with(|_jail| {
906            let expected = [
907                Killswitch {
908                    usecase: Some("broken_usecase".into()),
909                    scopes: BTreeMap::new(),
910                    service: None,
911                    service_matcher: std::sync::OnceLock::new(),
912                },
913                Killswitch {
914                    usecase: None,
915                    scopes: BTreeMap::from([("org".into(), "42".into())]),
916                    service: None,
917                    service_matcher: std::sync::OnceLock::new(),
918                },
919                Killswitch {
920                    usecase: None,
921                    scopes: BTreeMap::new(),
922                    service: Some("test-*".into()),
923                    service_matcher: std::sync::OnceLock::new(),
924                },
925                Killswitch {
926                    usecase: None,
927                    scopes: BTreeMap::from([
928                        ("org".into(), "42".into()),
929                        ("project".into(), "4711".into()),
930                    ]),
931                    service: None,
932                    service_matcher: std::sync::OnceLock::new(),
933                },
934                Killswitch {
935                    usecase: Some("attachments".into()),
936                    scopes: BTreeMap::from([("org".into(), "42".into())]),
937                    service: Some("test-*".into()),
938                    service_matcher: std::sync::OnceLock::new(),
939                },
940            ];
941
942            let config = Config::load(Some(tempfile.path())).unwrap();
943            assert_eq!(&config.killswitches.0, &expected,);
944
945            Ok(())
946        });
947    }
948
949    #[test]
950    fn configure_rate_limits_with_yaml() {
951        let mut tempfile = tempfile::NamedTempFile::new().unwrap();
952        tempfile
953            .write_all(
954                br#"
955                rate_limits:
956                  throughput:
957                    global_rps: 1000
958                    burst: 100
959                    usecase_pct: 50
960                    scope_pct: 25
961                    rules:
962                      - usecase: "high_priority"
963                        scopes:
964                          - ["org", "123"]
965                        rps: 500
966                      - scopes:
967                          - ["org", "456"]
968                          - ["project", "789"]
969                        pct: 10
970                  bandwidth:
971                    global_bps: 1048576
972                    usecase_pct: 50
973                    scope_pct: 25
974                "#,
975            )
976            .unwrap();
977
978        figment::Jail::expect_with(|_jail| {
979            let expected = RateLimits {
980                throughput: ThroughputLimits {
981                    global_rps: Some(1000),
982                    burst: 100,
983                    usecase_pct: Some(50),
984                    scope_pct: Some(25),
985                    rules: vec![
986                        ThroughputRule {
987                            usecase: Some("high_priority".to_string()),
988                            scopes: vec![("org".to_string(), "123".to_string())],
989                            rps: Some(500),
990                            pct: None,
991                        },
992                        ThroughputRule {
993                            usecase: None,
994                            scopes: vec![
995                                ("org".to_string(), "456".to_string()),
996                                ("project".to_string(), "789".to_string()),
997                            ],
998                            rps: None,
999                            pct: Some(10),
1000                        },
1001                    ],
1002                },
1003                bandwidth: BandwidthLimits {
1004                    global_bps: Some(1_048_576),
1005                    usecase_pct: Some(50),
1006                    scope_pct: Some(25),
1007                },
1008            };
1009
1010            let config = Config::load(Some(tempfile.path())).unwrap();
1011            assert_eq!(config.rate_limits, expected);
1012
1013            Ok(())
1014        });
1015    }
1016}