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}