| use crate::error::{DorisError, DorisResult}; |
| use crate::product::ProductProfile; |
| use serde::{Deserialize, Serialize}; |
| |
| /// A resolved environment with all connection details ready to use. |
| #[derive(Debug, Clone)] |
| pub struct Environment { |
| pub product: &'static ProductProfile, |
| /// The resolved environment's name (kept as metadata; not all paths read it). |
| #[allow(dead_code)] |
| pub name: String, |
| pub host: String, |
| pub mysql_port: u16, |
| pub http_port: u16, |
| pub user: String, |
| pub password: String, |
| pub socks5: Option<Socks5Config>, |
| /// Runtime-only (never persisted): a SQL statement to issue immediately |
| /// after `MysqlConnection::connect` succeeds — e.g. `USE @<compute-group>` |
| /// sourced from `--init-sql` / `DORIS_INIT_SQL` (the `cloudcli cloud |
| /// endpoint` handoff). |
| pub cluster_routing_directive: Option<String>, |
| } |
| |
| /// Stored environment config (without secrets). |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct EnvironmentConfig { |
| pub host: String, |
| #[serde(default = "default_mysql_port")] |
| pub mysql_port: u16, |
| #[serde(default = "default_http_port")] |
| pub http_port: u16, |
| pub user: String, |
| } |
| |
| /// Stored credentials (secrets). |
| #[derive(Debug, Clone, Serialize, Deserialize, Default)] |
| pub struct EnvironmentCredentials { |
| #[serde(default)] |
| pub password: String, |
| } |
| |
| /// Runtime-only SOCKS5 proxy configuration. |
| /// |
| /// Never persisted to disk. Sourced from the `--socks5` flag or `<PREFIX>_SOCKS5_*` |
| /// env vars because BYOC bastions are multi-tenant — one tenant's credentials |
| /// must not leak to another through a shared config file. |
| #[derive(Debug, Clone)] |
| pub struct Socks5Config { |
| pub host: String, |
| pub port: u16, |
| pub user: String, |
| pub pass: String, |
| } |
| |
| impl Socks5Config { |
| /// Build from env vars. Returns None unless both host and port are set. |
| /// user/pass default to "admin"/"admin" by convention. |
| pub fn from_env(product: &ProductProfile) -> Option<Self> { |
| let host = std::env::var(product.env_key("SOCKS5_HOST")).ok()?; |
| let port: u16 = std::env::var(product.env_key("SOCKS5_PORT")) |
| .ok()? |
| .parse() |
| .ok()?; |
| let user = |
| std::env::var(product.env_key("SOCKS5_USER")).unwrap_or_else(|_| "admin".to_string()); |
| let pass = |
| std::env::var(product.env_key("SOCKS5_PASS")).unwrap_or_else(|_| "admin".to_string()); |
| Some(Socks5Config { |
| host, |
| port, |
| user, |
| pass, |
| }) |
| } |
| |
| /// Parse `user:pass@host:port` (matches the Go mysql-client reference). |
| pub fn parse_flag(s: &str) -> DorisResult<Self> { |
| let (auth, hostport) = s.split_once('@').ok_or_else(|| { |
| DorisError::config(format!( |
| "--socks5 must be in 'user:pass@host:port' form, got '{s}'" |
| )) |
| })?; |
| let (user, pass) = auth.split_once(':').ok_or_else(|| { |
| DorisError::config(format!("--socks5 auth must be 'user:pass', got '{auth}'")) |
| })?; |
| let (host, port_s) = hostport.rsplit_once(':').ok_or_else(|| { |
| DorisError::config(format!( |
| "--socks5 host must be 'host:port', got '{hostport}'" |
| )) |
| })?; |
| let port: u16 = port_s.parse().map_err(|_| { |
| DorisError::config(format!("--socks5 port '{port_s}' is not a valid u16")) |
| })?; |
| Ok(Socks5Config { |
| host: host.to_string(), |
| port, |
| user: user.to_string(), |
| pass: pass.to_string(), |
| }) |
| } |
| } |
| |
| fn default_mysql_port() -> u16 { |
| 9030 |
| } |
| |
| fn default_http_port() -> u16 { |
| 8030 |
| } |
| |
| impl Environment { |
| /// Apply environment variable overrides (highest precedence before the --socks5 flag). |
| pub fn with_env_overrides(mut self) -> Self { |
| if let Ok(host) = std::env::var(self.product.env_key("HOST")) { |
| self.host = host; |
| } |
| if let Ok(user) = std::env::var(self.product.env_key("USER")) { |
| self.user = user; |
| } |
| if let Ok(password) = std::env::var(self.product.env_key("PASSWORD")) { |
| self.password = password; |
| } |
| if let Ok(port) = std::env::var(self.product.env_key("PORT")) { |
| if let Ok(p) = port.parse() { |
| self.mysql_port = p; |
| } |
| } |
| if let Ok(port) = std::env::var(self.product.env_key("HTTP_PORT")) { |
| if let Ok(p) = port.parse() { |
| self.http_port = p; |
| } |
| } |
| if self.socks5.is_none() { |
| self.socks5 = Socks5Config::from_env(self.product); |
| } |
| self |
| } |
| |
| /// Build from config + credentials. |
| pub fn from_config( |
| product: &'static ProductProfile, |
| name: String, |
| config: &EnvironmentConfig, |
| creds: &EnvironmentCredentials, |
| ) -> Self { |
| Environment { |
| product, |
| name, |
| host: config.host.clone(), |
| mysql_port: config.mysql_port, |
| http_port: config.http_port, |
| user: config.user.clone(), |
| password: creds.password.clone(), |
| socks5: None, |
| cluster_routing_directive: None, |
| } |
| } |
| } |