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