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