1//! The sidecar's one config file (TOML). 2//! 3//! ```toml 4//! [target] 5//! server = "target" # the foreign server's local name 6//! host = "db.example.com" 7//! port = 5432 8//! dbname = "app" 9//! sslmode = "verify-full" # the default 10//! use_remote_estimate = true # the default 11//! schemas = ["public"] # imported into same-named local schemas 12//! 13//! [[mapping]] 14//! local_user = "app" 15//! remote_user = "app_ro" 16//! password_file = "/run/secrets/app_ro" # or $POSTJEVSQL_TARGET_PASSWORD 17//! 18//! [jev] 19//! api_key_file = "/run/secrets/typesafe" # or $TYPESAFE_API_KEY 20//! ``` 21 22use serde::Deserialize; 23use std::path::PathBuf; 24 25use crate::secret::Sources; 26 27#[derive(Debug, Deserialize)] 28#[serde(deny_unknown_fields)] 29pub struct Config { 30 pub target: Target, 31 #[serde(default, rename = "mapping")] 32 pub mappings: Vec<Mapping>, 33 #[serde(default)] 34 pub jev: Jev, 35} 36 37#[derive(Debug, Deserialize)] 38#[serde(deny_unknown_fields)] 39pub struct Target { 40 #[serde(default = "default_server")] 41 pub server: String, 42 pub host: String, 43 #[serde(default = "default_port")] 44 pub port: u16, 45 pub dbname: String, 46 #[serde(default)] 47 pub sslmode: SslMode, 48 pub sslrootcert: Option<String>, 49 #[serde(default = "yes")] 50 pub use_remote_estimate: bool, 51 #[serde(default = "default_fetch_size")] 52 pub fetch_size: u32, 53 pub schemas: Vec<String>, 54} 55 56/// libpq's `sslmode`. `verify-full` is the default and what the contract 57/// requires of a real target; the weaker modes exist for loopback tests. 58#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)] 59#[serde(rename_all = "kebab-case")] 60pub enum SslMode { 61 Disable, 62 Require, 63 VerifyCa, 64 #[default] 65 VerifyFull, 66} 67 68impl SslMode { 69 pub fn as_str(self) -> &'static str { 70 match self { 71 SslMode::Disable => "disable", 72 SslMode::Require => "require", 73 SslMode::VerifyCa => "verify-ca", 74 SslMode::VerifyFull => "verify-full", 75 } 76 } 77} 78 79#[derive(Debug, Deserialize)] 80#[serde(deny_unknown_fields)] 81pub struct Mapping { 82 pub local_user: String, 83 pub remote_user: String, 84 pub password_file: Option<PathBuf>, 85 /// The env var that may hold the password instead of the file. 86 #[serde(default = "default_password_env")] 87 pub password_env: String, 88} 89 90impl Mapping { 91 pub fn password(&self) -> Sources { 92 Sources { 93 what: format!("the target password for {}", self.remote_user), 94 file: self.password_file.clone(), 95 env: self.password_env.clone(), 96 } 97 } 98} 99 100#[derive(Debug, Default, Deserialize)] 101#[serde(deny_unknown_fields)] 102pub struct Jev { 103 pub api_key_file: Option<PathBuf>, 104} 105 106impl Jev { 107 /// The API key the sidecar's server reads: `jev.api_key_file` or 108 /// `TYPESAFE_API_KEY`, exactly one (CLAUDE.md *Cost and safety*). 109 pub fn api_key(&self) -> Sources { 110 Sources { 111 what: "the Jev API key".into(), 112 file: self.api_key_file.clone(), 113 env: "TYPESAFE_API_KEY".into(), 114 } 115 } 116} 117 118impl Config { 119 pub fn parse(text: &str) -> Result<Config, String> { 120 let config: Config = toml::from_str(text).map_err(|e| e.to_string())?; 121 if config.target.schemas.is_empty() { 122 return Err("target.schemas names no schema to import".into()); 123 } 124 Ok(config) 125 } 126} 127 128fn default_server() -> String { 129 "target".into() 130} 131fn default_port() -> u16 { 132 5432 133} 134fn yes() -> bool { 135 true 136} 137fn default_fetch_size() -> u32 { 138 100 139} 140fn default_password_env() -> String { 141 "POSTJEVSQL_TARGET_PASSWORD".into() 142} 143 144#[cfg(test)] 145mod tests { 146 use super::*; 147 148 #[test] 149 fn defaults() { 150 let c = Config::parse( 151 "[target]\nhost = \"h\"\ndbname = \"d\"\nschemas = [\"public\"]\n\ 152 [[mapping]]\nlocal_user = \"a\"\nremote_user = \"b\"\n", 153 ) 154 .unwrap(); 155 assert_eq!(c.target.sslmode, SslMode::VerifyFull); 156 assert!(c.target.use_remote_estimate); 157 assert_eq!(c.target.port, 5432); 158 assert_eq!(c.mappings[0].password_env, "POSTJEVSQL_TARGET_PASSWORD"); 159 assert_eq!(c.jev.api_key().env, "TYPESAFE_API_KEY"); 160 } 161 162 #[test] 163 fn refuses_unknown_keys_and_no_schemas() { 164 assert!(Config::parse("[target]\nhost=\"h\"\ndbname=\"d\"\nschemas=[]\n").is_err()); 165 assert!( 166 Config::parse("[target]\nhost=\"h\"\ndbname=\"d\"\nschemas=[\"p\"]\nextensions=\"x\"\n") 167 .is_err() 168 ); 169 } 170}