The sidecar's one config file (TOML).
[target]
server = "target" # the foreign server's local name
host = "db.example.com"
port = 5432
dbname = "app"
sslmode = "verify-full" # the default
use_remote_estimate = true # the default
schemas = ["public"] # imported into same-named local schemas
[[mapping]]
local_user = "app"
remote_user = "app_ro"
password_file = "/run/secrets/app_ro" # or $POSTJEVSQL_TARGET_PASSWORD
[jev]
api_key_file = "/run/secrets/typesafe" # or $TYPESAFE_API_KEY
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}
libpq's sslmode. verify-full is the default and what the contract
requires of a real target; the weaker modes exist for loopback tests.
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>,
The env var that may hold the password instead of the file.
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 {
The API key the sidecar's server reads: jev.api_key_file or
TYPESAFE_API_KEY, exactly one (CLAUDE.md Cost and safety).
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}