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, Deserialize, Serialize)]
337pub struct AuthZ {
338 /// Whether to enforce content-based authorization or not.
339 ///
340 /// Defaults to `true`. If this is set to `false`, checks are still performed but failures
341 /// will not result in `403 Unauthorized` responses.
342 #[serde(default = "default_enforce")]
343 pub enforce: bool,
344
345 /// Keys that may be used to verify a request's auth token.
346 ///
347 /// The auth token is read from the `X-Os-Auth` header (preferred)
348 /// or the standard `Authorization` header (fallback). This field is a
349 /// container keyed on a key's ID. When verifying a JWT, the `kid` field
350 /// should be read from the JWT header and used to index into this map to
351 /// select the appropriate key.
352 #[serde(default)]
353 pub keys: BTreeMap<String, AuthZVerificationKey>,
354}
355
356fn default_enforce() -> bool {
357 true
358}
359
360impl Default for AuthZ {
361 fn default() -> Self {
362 Self {
363 enforce: true,
364 keys: BTreeMap::new(),
365 }
366 }
367}
368
369/// Main configuration struct for the objectstore server.
370///
371/// This is the top-level configuration that combines all server settings including networking,
372/// storage backends, runtime, and observability options.
373///
374/// Configuration is loaded with the following precedence (highest to lowest):
375/// 1. Environment variables (prefixed with `OS__`)
376/// 2. YAML configuration file (if provided via `-c` flag)
377/// 3. Default values
378///
379/// See individual field documentation for details on each configuration option, including
380/// defaults and environment variables.
381#[derive(Debug, Deserialize, Serialize)]
382pub struct Config {
383 /// HTTP server bind address.
384 ///
385 /// The socket address (IP and port) where the HTTP server will listen for incoming
386 /// connections. Supports both IPv4 and IPv6 addresses. Note that binding to `0.0.0.0`
387 /// makes the server accessible from all network interfaces.
388 ///
389 /// # Default
390 ///
391 /// `0.0.0.0:8888` (listens on all network interfaces, port 8888)
392 ///
393 /// # Environment Variable
394 ///
395 /// `OS__HTTP_ADDR`
396 pub http_addr: SocketAddr,
397
398 /// Storage backend configuration.
399 ///
400 /// Configures the storage backend used by the server. Use `type: "filesystem"` for
401 /// development, `type: "tiered"` for production two-tier routing (small objects to a
402 /// high-volume backend, large objects to a long-term backend), or any other single backend
403 /// type for simple deployments.
404 ///
405 /// # Default
406 ///
407 /// Filesystem storage in the `./data` directory
408 ///
409 /// # Environment Variables
410 ///
411 /// - `OS__STORAGE__TYPE` — backend type (`filesystem`, `tiered`, `gcs`, `bigtable`,
412 /// `s3compatible`)
413 /// - Additional fields depending on the type (see [`StorageConfig`])
414 ///
415 /// For tiered storage, sub-backend fields are nested under `high_volume` and `long_term`:
416 /// - `OS__STORAGE__TYPE=tiered`
417 /// - `OS__STORAGE__HIGH_VOLUME__TYPE=bigtable`
418 /// - `OS__STORAGE__LONG_TERM__TYPE=gcs`
419 ///
420 /// # Example (tiered)
421 ///
422 /// ```yaml
423 /// storage:
424 /// type: tiered
425 /// high_volume:
426 /// type: bigtable
427 /// project_id: my-project
428 /// instance_name: objectstore
429 /// table_name: objectstore
430 /// long_term:
431 /// type: gcs
432 /// bucket: my-objectstore-bucket
433 /// ```
434 pub storage: StorageConfig,
435
436 /// Configuration of the internal task runtime.
437 ///
438 /// Controls the thread pool size and behavior of the async runtime powering the server.
439 /// See [`Runtime`] for configuration options.
440 pub runtime: Runtime,
441
442 /// Logging configuration.
443 ///
444 /// Controls log verbosity and output format. See [`LoggingConfig`] for configuration options.
445 pub logging: LoggingConfig,
446
447 /// Sentry error tracking configuration.
448 ///
449 /// Optional integration with Sentry for error tracking and performance monitoring.
450 /// See [`Sentry`] for configuration options.
451 pub sentry: Sentry,
452
453 /// Internal metrics configuration.
454 ///
455 /// Configures submission of internal metrics to a DogStatsD-compatible endpoint.
456 /// See [`objectstore_metrics::MetricsConfig`] for configuration options.
457 pub metrics: objectstore_metrics::MetricsConfig,
458
459 /// Content-based authorization configuration.
460 ///
461 /// Controls the verification and enforcement of content-based access control based on the
462 /// JWT in a request's `X-Os-Auth` or `Authorization` header.
463 pub auth: AuthZ,
464
465 /// A list of matchers for requests to discard without processing.
466 pub killswitches: Killswitches,
467
468 /// Definitions for rate limits to enforce on incoming requests.
469 pub rate_limits: RateLimits,
470
471 /// Per-use-case configuration.
472 ///
473 /// Controls properties of individual use cases such as which expiration
474 /// policies are permitted and their maximum durations. Use cases not
475 /// present in the map receive default configuration (all policies allowed,
476 /// no duration caps).
477 pub usecases: UseCases,
478
479 /// Configuration for the [`StorageService`](objectstore_service::StorageService).
480 pub service: Service,
481
482 /// Configuration for the HTTP layer.
483 ///
484 /// Controls HTTP-level settings that operate before requests reach the
485 /// storage service. See [`Http`] for configuration options.
486 pub http: Http,
487}
488
489/// Configuration for the [`StorageService`](objectstore_service::StorageService).
490///
491/// Controls operational parameters of the storage service layer that sits
492/// between the HTTP server and the storage backends.
493///
494/// Used in: [`Config::service`]
495///
496/// # Environment Variables
497///
498/// - `OS__SERVICE__MAX_CONCURRENCY`
499#[derive(Debug, Deserialize, Serialize)]
500#[serde(default)]
501pub struct Service {
502 /// Maximum number of concurrent backend operations.
503 ///
504 /// This caps the total number of in-flight storage operations (reads,
505 /// writes, deletes) across all requests. Operations that exceed the limit
506 /// are rejected with HTTP 429.
507 ///
508 /// # Default
509 ///
510 /// [`DEFAULT_CONCURRENCY_LIMIT`](objectstore_service::service::DEFAULT_CONCURRENCY_LIMIT)
511 pub max_concurrency: usize,
512}
513
514impl Default for Service {
515 fn default() -> Self {
516 Self {
517 max_concurrency: objectstore_service::service::DEFAULT_CONCURRENCY_LIMIT,
518 }
519 }
520}
521
522/// Default maximum number of concurrent in-flight HTTP requests.
523///
524/// Requests beyond this limit are rejected with HTTP 503.
525pub const DEFAULT_MAX_HTTP_REQUESTS: usize = 10_000;
526
527/// Configuration for the HTTP layer.
528///
529/// Controls behaviour at the HTTP request level, before requests reach the
530/// storage service. Grouping these settings separately from [`Service`] keeps
531/// HTTP-layer and service-layer concerns distinct and provides a natural home
532/// for future HTTP-level settings (e.g. timeouts, body size limits).
533///
534/// Used in: [`Config::http`]
535///
536/// # Environment Variables
537///
538/// - `OS__HTTP__MAX_REQUESTS`
539#[derive(Debug, Deserialize, Serialize)]
540#[serde(default)]
541pub struct Http {
542 /// Maximum number of concurrent in-flight HTTP requests.
543 ///
544 /// This is a flood protection limit. When the number of requests currently
545 /// being processed reaches this value, new requests are rejected immediately
546 /// with HTTP 503. Health and readiness endpoints (`/health`, `/ready`) are
547 /// excluded from this limit.
548 ///
549 /// Unlike readiness-based backpressure, direct rejection responds in
550 /// milliseconds and recovers the moment any in-flight request completes.
551 ///
552 /// # Default
553 ///
554 /// [`DEFAULT_MAX_HTTP_REQUESTS`]
555 ///
556 /// # Environment Variable
557 ///
558 /// `OS__HTTP__MAX_REQUESTS`
559 pub max_requests: usize,
560}
561
562impl Default for Http {
563 fn default() -> Self {
564 Self {
565 max_requests: DEFAULT_MAX_HTTP_REQUESTS,
566 }
567 }
568}
569
570impl Default for Config {
571 fn default() -> Self {
572 Self {
573 http_addr: "0.0.0.0:8888".parse().unwrap(),
574
575 storage: StorageConfig::FileSystem(FileSystemConfig {
576 path: PathBuf::from("data"),
577 }),
578
579 runtime: Runtime::default(),
580 logging: LoggingConfig::default(),
581 sentry: Sentry::default(),
582 metrics: objectstore_metrics::MetricsConfig::default(),
583 auth: AuthZ::default(),
584 killswitches: Killswitches::default(),
585 rate_limits: RateLimits::default(),
586 usecases: UseCases::default(),
587 service: Service::default(),
588 http: Http::default(),
589 }
590 }
591}
592
593impl Config {
594 /// Loads configuration from the provided arguments.
595 ///
596 /// Configuration is merged in the following order (later sources override earlier ones):
597 /// 1. Default values
598 /// 2. YAML configuration file (if provided in `args`)
599 /// 3. Environment variables (prefixed with `OS__`)
600 ///
601 /// # Errors
602 ///
603 /// Returns an error if:
604 /// - The YAML configuration file cannot be read or parsed
605 /// - Environment variables contain invalid values
606 /// - Required fields are missing or invalid
607 pub fn load(path: Option<&Path>) -> Result<Self> {
608 let mut figment = figment::Figment::from(Serialized::defaults(Config::default()));
609 if let Some(path) = path {
610 figment = figment.merge(Yaml::file(path));
611 }
612 let config = figment
613 .merge(Env::prefixed(ENV_PREFIX).split("__"))
614 .extract()?;
615
616 Ok(config)
617 }
618}
619
620#[cfg(test)]
621#[expect(
622 clippy::result_large_err,
623 reason = "figment::Error is inherently large"
624)]
625mod tests {
626 use std::io::Write;
627
628 use objectstore_service::backend::HighVolumeStorageConfig;
629 use secrecy::ExposeSecret;
630
631 use crate::killswitches::Killswitch;
632 use crate::rate_limits::{BandwidthLimits, RateLimits, ThroughputLimits, ThroughputRule};
633
634 use super::*;
635
636 #[test]
637 fn configurable_via_env() {
638 figment::Jail::expect_with(|jail| {
639 jail.set_env("OS__STORAGE__TYPE", "s3compatible");
640 jail.set_env("OS__STORAGE__ENDPOINT", "http://localhost:8888");
641 jail.set_env("OS__STORAGE__BUCKET", "whatever");
642 jail.set_env("OS__METRICS__TAGS__FOO", "bar");
643 jail.set_env("OS__METRICS__TAGS__BAZ", "qux");
644 jail.set_env("OS__SENTRY__DSN", "abcde");
645 jail.set_env("OS__SENTRY__SAMPLE_RATE", "0.5");
646 jail.set_env("OS__SENTRY__ENVIRONMENT", "production");
647 jail.set_env("OS__SENTRY__SERVER_NAME", "objectstore-deadbeef");
648 jail.set_env("OS__SENTRY__TRACES_SAMPLE_RATE", "0.5");
649
650 let config = Config::load(None).unwrap();
651
652 let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
653 panic!("expected s3 storage");
654 };
655 assert_eq!(c.endpoint, "http://localhost:8888");
656 assert_eq!(c.bucket, "whatever");
657 assert_eq!(
658 config.metrics.tags,
659 [("foo".into(), "bar".into()), ("baz".into(), "qux".into())].into()
660 );
661
662 assert_eq!(config.sentry.dsn.unwrap().expose_secret().as_str(), "abcde");
663 assert_eq!(config.sentry.environment.as_deref(), Some("production"));
664 assert_eq!(
665 config.sentry.server_name.as_deref(),
666 Some("objectstore-deadbeef")
667 );
668 assert_eq!(config.sentry.sample_rate, 0.5);
669 assert_eq!(config.sentry.traces_sample_rate, 0.5);
670
671 Ok(())
672 });
673 }
674
675 #[test]
676 fn configurable_via_yaml() {
677 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
678 tempfile
679 .write_all(
680 br#"
681 storage:
682 type: s3compatible
683 endpoint: http://localhost:8888
684 bucket: whatever
685 sentry:
686 dsn: abcde
687 environment: production
688 server_name: objectstore-deadbeef
689 sample_rate: 0.5
690 traces_sample_rate: 0.5
691 "#,
692 )
693 .unwrap();
694
695 figment::Jail::expect_with(|_jail| {
696 let config = Config::load(Some(tempfile.path())).unwrap();
697
698 let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
699 panic!("expected s3 storage");
700 };
701 assert_eq!(c.endpoint, "http://localhost:8888");
702 assert_eq!(c.bucket, "whatever");
703
704 assert_eq!(config.sentry.dsn.unwrap().expose_secret().as_str(), "abcde");
705 assert_eq!(config.sentry.environment.as_deref(), Some("production"));
706 assert_eq!(
707 config.sentry.server_name.as_deref(),
708 Some("objectstore-deadbeef")
709 );
710 assert_eq!(config.sentry.sample_rate, 0.5);
711 assert_eq!(config.sentry.traces_sample_rate, 0.5);
712
713 Ok(())
714 });
715 }
716
717 #[test]
718 fn configured_with_env_and_yaml() {
719 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
720 tempfile
721 .write_all(
722 br#"
723 storage:
724 type: s3compatible
725 endpoint: http://localhost:8888
726 bucket: whatever
727 "#,
728 )
729 .unwrap();
730
731 figment::Jail::expect_with(|jail| {
732 jail.set_env("OS__STORAGE__ENDPOINT", "http://localhost:9001");
733
734 let config = Config::load(Some(tempfile.path())).unwrap();
735
736 let StorageConfig::S3Compatible(c) = &dbg!(&config).storage else {
737 panic!("expected s3 storage");
738 };
739 // Env should overwrite the yaml config
740 assert_eq!(c.endpoint, "http://localhost:9001");
741
742 Ok(())
743 });
744 }
745
746 #[test]
747 fn tiered_storage_via_yaml() {
748 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
749 tempfile
750 .write_all(
751 br#"
752 storage:
753 type: tiered
754 high_volume:
755 type: bigtable
756 project_id: my-project
757 instance_name: objectstore
758 table_name: objectstore
759 long_term:
760 type: gcs
761 bucket: my-objectstore-bucket
762 "#,
763 )
764 .unwrap();
765
766 figment::Jail::expect_with(|_jail| {
767 let config = Config::load(Some(tempfile.path())).unwrap();
768
769 let StorageConfig::Tiered(c) = &dbg!(&config).storage else {
770 panic!("expected tiered storage");
771 };
772 let HighVolumeStorageConfig::BigTable(hv) = &c.high_volume;
773 assert_eq!(hv.project_id, "my-project");
774 let StorageConfig::Gcs(lt) = c.long_term.as_ref() else {
775 panic!("expected gcs long_term");
776 };
777 assert_eq!(lt.bucket, "my-objectstore-bucket");
778
779 Ok(())
780 });
781 }
782
783 #[test]
784 fn tiered_storage_via_env() {
785 figment::Jail::expect_with(|jail| {
786 jail.set_env("OS__STORAGE__TYPE", "tiered");
787 jail.set_env("OS__STORAGE__HIGH_VOLUME__TYPE", "bigtable");
788 jail.set_env("OS__STORAGE__HIGH_VOLUME__PROJECT_ID", "my-project");
789 jail.set_env("OS__STORAGE__HIGH_VOLUME__INSTANCE_NAME", "my-instance");
790 jail.set_env("OS__STORAGE__HIGH_VOLUME__TABLE_NAME", "my-table");
791 jail.set_env("OS__STORAGE__LONG_TERM__TYPE", "filesystem");
792 jail.set_env("OS__STORAGE__LONG_TERM__PATH", "/data/lt");
793
794 let config = Config::load(None).unwrap();
795
796 let StorageConfig::Tiered(c) = &dbg!(&config).storage else {
797 panic!("expected tiered storage");
798 };
799 let HighVolumeStorageConfig::BigTable(hv) = &c.high_volume;
800 assert_eq!(hv.project_id, "my-project");
801 assert_eq!(hv.instance_name, "my-instance");
802 assert_eq!(hv.table_name, "my-table");
803 let StorageConfig::FileSystem(lt) = c.long_term.as_ref() else {
804 panic!("expected filesystem long_term");
805 };
806 assert_eq!(lt.path, Path::new("/data/lt"));
807
808 Ok(())
809 });
810 }
811
812 #[test]
813 fn metrics_addr_via_env() {
814 figment::Jail::expect_with(|jail| {
815 jail.set_env("OS__METRICS__ADDR", "127.0.0.1:8125");
816
817 let config = Config::load(None).unwrap();
818 assert_eq!(config.metrics.addr.as_deref(), Some("127.0.0.1:8125"));
819
820 Ok(())
821 });
822 }
823
824 #[test]
825 fn configure_auth_with_env() {
826 figment::Jail::expect_with(|jail| {
827 jail.set_env("OS__AUTH__ENFORCE", "true");
828 jail.set_env(
829 "OS__AUTH__KEYS",
830 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"],}}"#,
831 );
832
833 let config = Config::load(None).unwrap();
834
835 assert!(config.auth.enforce);
836
837 let kid1 = config.auth.keys.get("kid1").unwrap();
838 assert_eq!(kid1.key_files[0], Path::new("abcde"));
839 assert_eq!(kid1.key_files[1], Path::new("fghij"));
840 assert_eq!(
841 kid1.key_files[2],
842 Path::new("this is a test\n multiline string\nend of string\n"),
843 );
844 assert_eq!(
845 kid1.max_permissions,
846 HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
847 );
848
849 let kid2 = config.auth.keys.get("kid2").unwrap();
850 assert_eq!(kid2.key_files[0], Path::new("12345"));
851 assert_eq!(kid2.max_permissions, HashSet::new());
852
853 Ok(())
854 });
855 }
856
857 #[test]
858 fn configure_auth_with_yaml() {
859 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
860 tempfile
861 .write_all(
862 br#"
863 auth:
864 enforce: true
865 keys:
866 kid1:
867 key_files:
868 - "abcde"
869 - "fghij"
870 - |
871 this is a test
872 multiline string
873 end of string
874 max_permissions:
875 - "object.read"
876 - "object.write"
877 kid2:
878 key_files:
879 - "12345"
880 "#,
881 )
882 .unwrap();
883
884 figment::Jail::expect_with(|_jail| {
885 let config = Config::load(Some(tempfile.path())).unwrap();
886
887 assert!(config.auth.enforce);
888
889 let kid1 = config.auth.keys.get("kid1").unwrap();
890 assert_eq!(kid1.key_files[0], Path::new("abcde"));
891 assert_eq!(kid1.key_files[1], Path::new("fghij"));
892 assert_eq!(
893 kid1.key_files[2],
894 Path::new("this is a test\n multiline string\nend of string\n")
895 );
896 assert_eq!(
897 kid1.max_permissions,
898 HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
899 );
900
901 let kid2 = config.auth.keys.get("kid2").unwrap();
902 assert_eq!(kid2.key_files[0], Path::new("12345"));
903 assert_eq!(kid2.max_permissions, HashSet::new());
904
905 Ok(())
906 });
907 }
908
909 #[test]
910 fn auth_enforce_defaults_to_true() {
911 figment::Jail::expect_with(|_jail| {
912 let config = Config::load(None).unwrap();
913 assert!(config.auth.enforce);
914 Ok(())
915 });
916 }
917
918 #[test]
919 fn auth_enforce_defaults_to_true_when_omitted_from_yaml() {
920 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
921 tempfile
922 .write_all(
923 br#"
924 auth:
925 keys: {}
926 "#,
927 )
928 .unwrap();
929
930 figment::Jail::expect_with(|_jail| {
931 let config = Config::load(Some(tempfile.path())).unwrap();
932 assert!(config.auth.enforce);
933 Ok(())
934 });
935 }
936
937 #[test]
938 fn auth_enforce_can_be_disabled() {
939 figment::Jail::expect_with(|jail| {
940 jail.set_env("OS__AUTH__ENFORCE", "false");
941 let config = Config::load(None).unwrap();
942 assert!(!config.auth.enforce);
943 Ok(())
944 });
945 }
946
947 #[test]
948 fn configure_killswitches_with_yaml() {
949 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
950 tempfile
951 .write_all(
952 br#"
953 killswitches:
954 - usecase: broken_usecase
955 - scopes:
956 org: "42"
957 - service: "test-*"
958 - scopes:
959 org: "42"
960 project: "4711"
961 - usecase: attachments
962 scopes:
963 org: "42"
964 service: "test-*"
965 "#,
966 )
967 .unwrap();
968
969 figment::Jail::expect_with(|_jail| {
970 let expected = [
971 Killswitch {
972 usecase: Some("broken_usecase".into()),
973 scopes: BTreeMap::new(),
974 service: None,
975 service_matcher: std::sync::OnceLock::new(),
976 },
977 Killswitch {
978 usecase: None,
979 scopes: BTreeMap::from([("org".into(), "42".into())]),
980 service: None,
981 service_matcher: std::sync::OnceLock::new(),
982 },
983 Killswitch {
984 usecase: None,
985 scopes: BTreeMap::new(),
986 service: Some("test-*".into()),
987 service_matcher: std::sync::OnceLock::new(),
988 },
989 Killswitch {
990 usecase: None,
991 scopes: BTreeMap::from([
992 ("org".into(), "42".into()),
993 ("project".into(), "4711".into()),
994 ]),
995 service: None,
996 service_matcher: std::sync::OnceLock::new(),
997 },
998 Killswitch {
999 usecase: Some("attachments".into()),
1000 scopes: BTreeMap::from([("org".into(), "42".into())]),
1001 service: Some("test-*".into()),
1002 service_matcher: std::sync::OnceLock::new(),
1003 },
1004 ];
1005
1006 let config = Config::load(Some(tempfile.path())).unwrap();
1007 assert_eq!(&config.killswitches.0, &expected,);
1008
1009 Ok(())
1010 });
1011 }
1012
1013 #[test]
1014 fn configure_rate_limits_with_yaml() {
1015 let mut tempfile = tempfile::NamedTempFile::new().unwrap();
1016 tempfile
1017 .write_all(
1018 br#"
1019 rate_limits:
1020 throughput:
1021 global_rps: 1000
1022 burst: 100
1023 usecase_pct: 50
1024 scope_pct: 25
1025 rules:
1026 - usecase: "high_priority"
1027 scopes:
1028 - ["org", "123"]
1029 rps: 500
1030 - scopes:
1031 - ["org", "456"]
1032 - ["project", "789"]
1033 pct: 10
1034 bandwidth:
1035 global_bps: 1048576
1036 usecase_pct: 50
1037 scope_pct: 25
1038 "#,
1039 )
1040 .unwrap();
1041
1042 figment::Jail::expect_with(|_jail| {
1043 let expected = RateLimits {
1044 throughput: ThroughputLimits {
1045 global_rps: Some(1000),
1046 burst: 100,
1047 usecase_pct: Some(50),
1048 scope_pct: Some(25),
1049 rules: vec![
1050 ThroughputRule {
1051 usecase: Some("high_priority".to_string()),
1052 scopes: vec![("org".to_string(), "123".to_string())],
1053 rps: Some(500),
1054 pct: None,
1055 },
1056 ThroughputRule {
1057 usecase: None,
1058 scopes: vec![
1059 ("org".to_string(), "456".to_string()),
1060 ("project".to_string(), "789".to_string()),
1061 ],
1062 rps: None,
1063 pct: Some(10),
1064 },
1065 ],
1066 },
1067 bandwidth: BandwidthLimits {
1068 global_bps: Some(1_048_576),
1069 usecase_pct: Some(50),
1070 scope_pct: Some(25),
1071 },
1072 };
1073
1074 let config = Config::load(Some(tempfile.path())).unwrap();
1075 assert_eq!(config.rate_limits, expected);
1076
1077 Ok(())
1078 });
1079 }
1080}