#[derive(Debug, Clone)]
pub struct PgConnection {
pub host: String,
pub port: u16,
pub user: String,
pub password: String,
pub database: String,
pub connect_timeout_secs: u32,
}
impl PgConnection {
pub fn env(&self) -> Vec<(String, String)> {
vec![
("PGHOST".to_string(), self.host.clone()),
("PGPORT".to_string(), self.port.to_string()),
("PGUSER".to_string(), self.user.clone()),
("PGPASSWORD".to_string(), self.password.clone()),
("PGDATABASE".to_string(), self.database.clone()),
(
"PGCONNECT_TIMEOUT".to_string(),
self.connect_timeout_secs.to_string(),
),
]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SqlInvocation {
pub argv: Vec<String>,
pub env: Vec<(String, String)>,
}
pub fn pg_dump_invocation(conn: &PgConnection, data_only: bool) -> SqlInvocation {
let mut argv = vec![
"pg_dump".to_string(),
"--no-owner".to_string(),
"--no-privileges".to_string(),
"-Fc".to_string(),
];
if data_only {
argv.push("--data-only".to_string());
argv.push("--exclude-table-data=boatramp_migrations.*".to_string());
}
SqlInvocation {
argv,
env: conn.env(),
}
}
pub fn pg_restore_invocation(conn: &PgConnection, data_only: bool) -> SqlInvocation {
let mut argv = vec![
"pg_restore".to_string(),
"--no-owner".to_string(),
"--no-privileges".to_string(),
"--exit-on-error".to_string(),
"-d".to_string(),
conn.database.clone(),
];
if data_only {
argv.push("--data-only".to_string());
}
SqlInvocation {
argv,
env: conn.env(),
}
}
#[derive(Debug, Clone)]
pub struct MysqlConnection {
pub host: String,
pub port: u16,
pub user: String,
pub password: String,
pub database: String,
pub connect_timeout_secs: u32,
}
impl MysqlConnection {
pub fn env(&self) -> Vec<(String, String)> {
vec![("MYSQL_PWD".to_string(), self.password.clone())]
}
pub fn conn_args(&self) -> Vec<String> {
vec![
format!("--host={}", self.host),
format!("--port={}", self.port),
format!("--user={}", self.user),
]
}
fn connect_timeout_arg(&self) -> String {
format!("--connect-timeout={}", self.connect_timeout_secs)
}
}
pub fn mysqldump_invocation(conn: &MysqlConnection, data_only: bool) -> SqlInvocation {
let mut argv = vec![
"mysqldump".to_string(),
"--single-transaction".to_string(),
"--no-tablespaces".to_string(),
];
argv.extend(conn.conn_args());
if data_only {
argv.push("--no-create-info".to_string());
argv.push(conn.database.clone());
} else {
argv.push("--databases".to_string());
argv.push(conn.database.clone());
}
SqlInvocation {
argv,
env: conn.env(),
}
}
pub fn mysql_restore_invocation(conn: &MysqlConnection, data_only: bool) -> SqlInvocation {
let mut argv = vec!["mysql".to_string()];
argv.extend(conn.conn_args());
argv.push(conn.connect_timeout_arg());
if data_only {
argv.push(conn.database.clone());
}
SqlInvocation {
argv,
env: conn.env(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn conn() -> PgConnection {
PgConnection {
host: "10.0.0.5".to_string(),
port: 5432,
user: "svc".to_string(),
password: "sup3r-s3cret-pw".to_string(),
database: "app".to_string(),
connect_timeout_secs: 10,
}
}
#[test]
fn data_only_toggles_the_flag_on_both_sides() {
let dump_do = pg_dump_invocation(&conn(), true);
let dump_full = pg_dump_invocation(&conn(), false);
assert!(dump_do.argv.iter().any(|a| a == "--data-only"));
assert!(!dump_full.argv.iter().any(|a| a == "--data-only"));
assert!(
dump_do
.argv
.iter()
.any(|a| a == "--exclude-table-data=boatramp_migrations.*")
);
assert!(
!dump_full
.argv
.iter()
.any(|a| a == "--exclude-table-data=boatramp_migrations.*")
);
let restore_do = pg_restore_invocation(&conn(), true);
let restore_full = pg_restore_invocation(&conn(), false);
assert!(restore_do.argv.iter().any(|a| a == "--data-only"));
assert!(!restore_full.argv.iter().any(|a| a == "--data-only"));
for inv in [&restore_do, &restore_full] {
let pos = inv
.argv
.iter()
.position(|a| a == "-d")
.expect("pg_restore argv must carry -d");
assert_eq!(
inv.argv.get(pos + 1).map(String::as_str),
Some(conn().database.as_str()),
"the target database must follow -d"
);
}
}
#[test]
fn credential_is_in_the_env_never_in_argv() {
let c = conn();
for inv in [
pg_dump_invocation(&c, true),
pg_restore_invocation(&c, false),
] {
assert!(
inv.env
.iter()
.any(|(k, v)| k == "PGPASSWORD" && v == &c.password),
"PGPASSWORD must be set in the environment"
);
for a in &inv.argv {
assert!(
!a.contains(&c.password),
"credential leaked into argv: {a:?}"
);
}
for a in &inv.argv {
assert!(
!a.contains("://"),
"a DSN must never be built into argv: {a:?}"
);
}
}
}
#[test]
fn env_carries_the_discrete_connection_and_a_bounded_timeout() {
let e = conn().env();
let get = |k: &str| e.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(get("PGHOST").as_deref(), Some("10.0.0.5"));
assert_eq!(get("PGPORT").as_deref(), Some("5432"));
assert_eq!(get("PGUSER").as_deref(), Some("svc"));
assert_eq!(get("PGDATABASE").as_deref(), Some("app"));
assert_eq!(get("PGCONNECT_TIMEOUT").as_deref(), Some("10"));
}
fn myconn() -> MysqlConnection {
MysqlConnection {
host: "10.0.0.6".to_string(),
port: 3306,
user: "svc".to_string(),
password: "sup3r-s3cret-pw".to_string(),
database: "app".to_string(),
connect_timeout_secs: 10,
}
}
#[test]
fn mysql_data_only_toggles_and_credential_stays_in_env() {
let dump_do = mysqldump_invocation(&myconn(), true);
let dump_full = mysqldump_invocation(&myconn(), false);
assert!(dump_do.argv.iter().any(|a| a == "--no-create-info"));
assert!(!dump_full.argv.iter().any(|a| a == "--no-create-info"));
assert!(dump_full.argv.iter().any(|a| a == "--databases"));
assert!(!dump_do.argv.iter().any(|a| a == "--databases"));
for d in [&dump_do, &dump_full] {
assert!(
!d.argv.iter().any(|a| a.starts_with("--connect-timeout")),
"mysqldump must not carry --connect-timeout"
);
}
let restore_do = mysql_restore_invocation(&myconn(), true);
let restore_full = mysql_restore_invocation(&myconn(), false);
assert!(
restore_full
.argv
.iter()
.any(|a| a.starts_with("--connect-timeout")),
"the mysql restore client should carry --connect-timeout"
);
assert!(restore_do.argv.iter().any(|a| a == "app"));
assert!(!restore_full.argv.iter().any(|a| a == "app"));
let c = myconn();
for inv in [
mysqldump_invocation(&c, true),
mysql_restore_invocation(&c, false),
] {
assert!(
inv.env
.iter()
.any(|(k, v)| k == "MYSQL_PWD" && v == &c.password),
"MYSQL_PWD must be set in the environment"
);
assert!(
inv.argv.iter().any(|a| a == &format!("--host={}", c.host)),
"the connection host must be explicit in argv: {:?}",
inv.argv
);
for a in &inv.argv {
assert!(
!a.contains(&c.password),
"credential leaked into argv: {a:?}"
);
assert!(
!a.contains("://"),
"a DSN must never be built into argv: {a:?}"
);
}
}
}
#[test]
fn mysql_env_carries_the_discrete_connection() {
let c = myconn();
let e = c.env();
assert_eq!(e.len(), 1, "env carries only MYSQL_PWD");
assert_eq!(e[0].0, "MYSQL_PWD");
assert_eq!(e[0].1, "sup3r-s3cret-pw");
let a = c.conn_args();
assert!(a.iter().any(|x| x == "--host=10.0.0.6"));
assert!(a.iter().any(|x| x == "--port=3306"));
assert!(a.iter().any(|x| x == "--user=svc"));
assert!(
!a.iter().any(|x| x.contains("sup3r-s3cret-pw")),
"password never in conn_args"
);
}
}