#![deny(missing_docs)]
mod raw;
mod resolve;
use std::net::SocketAddr;
use raw::Raw;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Config {
pub bind: SocketAddr,
pub grpc_bind: Option<SocketAddr>,
pub upstream: String,
pub index: String,
pub tokens: Vec<(String, String)>,
pub require_tls_for_mutation: bool,
pub tls: Option<TlsConfig>,
pub observability: ObservabilityConfig,
pub admin_passthrough: Option<AdminPassthroughConfig>,
pub cursor_affinity_key: Option<String>,
pub passthrough: Option<PassthroughConfig>,
pub header_forwarding: HeaderForwardingConfig,
pub capture: Option<CaptureConfig>,
pub capture_default: bool,
pub fanout: Option<FanoutConfig>,
pub etcd: Option<EtcdConfig>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EtcdConfig {
pub endpoints: Vec<String>,
pub directives_key: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FanoutConfig {
pub brokers: Vec<String>,
pub topic: String,
pub tls: Option<CaptureTlsConfig>,
pub body_encoding: FanoutBodyEncoding,
pub async_default: bool,
pub expand_delete_by_query: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FanoutBodyEncoding {
#[default]
Cbor,
Json,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureConfig {
pub brokers: Vec<String>,
pub topic: String,
pub redact: bool,
pub tls: Option<CaptureTlsConfig>,
pub max_inflight: usize,
pub max_attempts: u32,
pub backoff_ms: u64,
pub wal_dir: Option<String>,
pub wal_max_bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureTlsConfig {
pub ca_path: String,
pub client_cert_path: Option<String>,
pub client_key_path: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TlsConfig {
pub cert_path: String,
pub key_path: String,
pub client_ca_path: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObservabilityConfig {
pub log_requests: bool,
pub otlp_endpoint: Option<String>,
pub service_name: String,
pub diag_baseline: DiagBaseline,
pub debug_directive_key: Option<String>,
pub directive_admin_token: Option<String>,
pub debug_endpoints: bool,
pub log_diagnostic_captures: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminPassthroughConfig {
pub cluster: String,
pub prefixes: Vec<String>,
pub endpoint: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HeaderForwardingConfig {
pub enabled: bool,
pub deny: Vec<String>,
}
impl Default for HeaderForwardingConfig {
fn default() -> Self {
Self {
enabled: true,
deny: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PassthroughConfig {
pub cluster: String,
pub endpoint: String,
pub index_prefixes: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum DiagBaseline {
Off,
#[default]
Shape,
ShapeTiming,
ShapeRewriteDiff,
}
impl DiagBaseline {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Shape => "shape",
Self::ShapeTiming => "shape-timing",
Self::ShapeRewriteDiff => "shape-rewrite-diff",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigError {
field: String,
reason: String,
}
impl ConfigError {
#[must_use]
pub fn invalid(field: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
field: field.into(),
reason: reason.into(),
}
}
#[must_use]
pub fn unknown(field: impl Into<String>) -> Self {
Self {
field: field.into(),
reason: "unknown setting".to_owned(),
}
}
#[must_use]
pub fn field(&self) -> &str {
&self.field
}
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "config: `{}`: {}", self.field, self.reason)
}
}
impl std::error::Error for ConfigError {}
impl Config {
pub fn load<I: IntoIterator<Item = String>>(args: I) -> Result<Self, ConfigError> {
let (file_flag, flags) = extract_config_flag(args)?;
let file_path = file_flag.or_else(|| {
std::env::var("OSPROXY_CONFIG")
.ok()
.filter(|v| !v.is_empty())
});
let file = match &file_path {
Some(path) => {
let text = std::fs::read_to_string(path)
.map_err(|e| ConfigError::invalid("config", format!("reading {path}: {e}")))?;
Raw::from_file(&text)?
}
None => Raw::default(),
};
let raw = Raw::layered(file, Raw::from_env(), Raw::from_flags(flags)?);
resolve::resolve(&raw)
}
pub fn resolve_for_test(pairs: &[(&str, &str)]) -> Result<Self, ConfigError> {
resolve::resolve(&Raw::from_pairs(pairs)?)
}
}
fn extract_config_flag<I: IntoIterator<Item = String>>(
args: I,
) -> Result<(Option<String>, Vec<String>), ConfigError> {
let mut file = None;
let mut rest = Vec::new();
let mut args = args.into_iter();
while let Some(arg) = args.next() {
if arg == "--config" {
file = Some(
args.next()
.ok_or_else(|| ConfigError::invalid("config", "--config needs a path"))?,
);
} else if let Some(path) = arg.strip_prefix("--config=") {
file = Some(path.to_owned());
} else {
rest.push(arg);
}
}
Ok((file, rest))
}