Skip to main content

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