Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions databases.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
"host": "db-postgres",
"generated_id": "16678159-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 1 - PostgreSQL - BIS",
"database": "devdb2",
"type": "postgresql",
"username": "devuser2",
"password": "changeme2",
"port": 5432,
"host": "db-postgres-2",
"generated_id": "16678159-ff7e-5697-8c83-0adeff214681",
"options": {
"keep_ownership": true
}
},
{
"name": "Test database 2 - MariaDB",
"database": "mariadb",
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.databases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ services:
networks:
- portabase

db-postgres-2:
container_name: db-postgres-2
image: postgres:17-alpine
ports:
- "5438:5432"
volumes:
- postgres-data-2:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb2
- POSTGRES_USER=devuser2
- POSTGRES_PASSWORD=changeme2
networks:
- portabase

db-mariadb:
container_name: db-mariadb
image: mariadb:latest
Expand Down Expand Up @@ -179,6 +193,7 @@ services:

volumes:
postgres-data:
postgres-data-2:
mariadb-data:
mysql-data:
mongodb-data:
Expand Down
8 changes: 7 additions & 1 deletion src/domain/postgres/cluster/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::process::Command;
use std::sync::Arc;
use std::time::Instant;

use super::super::connection::{is_superuser, psql_binary_name, select_pg_path, server_version};
use super::super::connection::{is_superuser, psql_binary_name, select_pg_path, server_version, terminate_all_connections};
use crate::services::backup::logger::JobLogger;
use crate::services::config::DatabaseConfig;

Expand Down Expand Up @@ -40,6 +40,12 @@ pub async fn run(

let psql = select_pg_path(&version).join(psql_binary_name());

if let Err(e) = futures::executor::block_on(terminate_all_connections(&cfg)) {
logger.log("error", format!("Failed to terminate connections for cluster {}: {:?}", cfg.name, e));
return Err(e.into());
}
logger.log("info", format!("All user database connections terminated for cluster {}", cfg.name));

logger.log("info", format!("Replaying cluster dump for {} via {:?}", cfg.name, psql));

let start = Instant::now();
Expand Down
21 changes: 21 additions & 0 deletions src/domain/postgres/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ pub async fn terminate_connections(cfg: &DatabaseConfig) -> Result<()> {
Ok(())
}

pub async fn terminate_all_connections(cfg: &DatabaseConfig) -> Result<()> {
let mut admin = cfg.clone();
admin.database = "postgres".to_string().into();

let client = connect(&admin).await?;

client
.execute(
r#"
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname NOT IN ('postgres', 'template0', 'template1')
AND pid <> pg_backend_pid();
"#,
&[],
)
.await?;

Ok(())
}

pub fn detect_format_from_file(restore_file: &Path) -> PostgresDumpFormat {
match restore_file.extension().and_then(|e| e.to_str()) {
Some("dump") => PostgresDumpFormat::Fc,
Expand Down
27 changes: 21 additions & 6 deletions src/domain/postgres/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,26 @@ pub async fn run(
}
logger.log("info", format!("Connections terminated for database {}", cfg.name));

let keep_ownership = cfg.options
.get("keep_ownership")
.and_then(|v| v.as_bool())
.unwrap_or(false);

if keep_ownership {
logger.log("info", format!("Restoring ownership and privileges for {}", cfg.name));
} else {
logger.log("info", format!("Stripping ownership and privileges for {} (--no-owner --no-privileges)", cfg.name));
}

match format {
PostgresDumpFormat::Fc => {
logger.log("info", format!("Running FC restore for {}", cfg.name));
let start = Instant::now();
let output = Command::new(&pg_restore)
.arg("--no-owner")
.arg("--no-privileges")
let mut cmd = Command::new(&pg_restore);
if !keep_ownership {
cmd.arg("--no-owner").arg("--no-privileges");
}
let output = cmd
.arg("--clean")
.arg("--if-exists")
// .arg("--create")
Expand Down Expand Up @@ -147,9 +160,11 @@ pub async fn run(
};

let start = Instant::now();
let output = Command::new(&pg_restore)
.arg("--no-owner")
.arg("--no-privileges")
let mut cmd = Command::new(&pg_restore);
if !keep_ownership {
cmd.arg("--no-owner").arg("--no-privileges");
}
let output = cmd
.arg("--clean")
.arg("--if-exists")
// .arg("--create")
Expand Down
4 changes: 4 additions & 0 deletions src/services/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::core::context::Context;
use serde::Deserialize;
use serde_json;
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::Path;
Expand Down Expand Up @@ -58,6 +59,7 @@ pub struct DatabaseConfig {
pub generated_id: String,
pub path: String,
pub max_packet_size: String,
pub options: HashMap<String, serde_json::Value>,
}

#[allow(dead_code)]
Expand All @@ -80,6 +82,7 @@ pub struct InputDatabaseConfig {
pub generated_id: String,
pub path: Option<String>,
pub max_packet_size: Option<String>,
pub options: Option<HashMap<String, serde_json::Value>>,
}

#[allow(dead_code)]
Expand Down Expand Up @@ -247,6 +250,7 @@ impl ConfigService {
generated_id: db.generated_id,
path: path_val,
max_packet_size,
options: db.options.unwrap_or_default(),
});
}

Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/cluster/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ fn cluster_config() -> DatabaseConfig {
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: String::new(),
max_packet_size: String::new(),
options: std::collections::HashMap::new(),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/cluster/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async fn start_cluster(user: &str) -> (ContainerAsync<Postgres>, DatabaseConfig)
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};
(container, config)
}
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/firebird.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async fn create_config() -> (ContainerAsync<GenericImage>, DatabaseConfig) {
generated_id: "3c445eb4-c2c6-4bde-a423-ee1385dcf6d2".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/mariadb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async fn create_config() -> (ContainerAsync<Mariadb>, DatabaseConfig) {
generated_id: "3c4b4eb4-c2c6-4bde-a423-ee1385dcf6d2".to_string(),
path: "".to_string(),
max_packet_size: "512M".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/mongodb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async fn create_config() -> (ContainerAsync<Mongo>, DatabaseConfig) {
generated_id: "96d30a9f-ff4b-47c9-aaab-f3147bb34f16".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn make_config(host: String, port: u16, database: &str, generated_id: &str) -> D
generated_id: generated_id.to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async fn create_config() -> (ContainerAsync<Mysql>, DatabaseConfig) {
generated_id: "0f1bb8f2-35a0-4c91-8098-e36873d3ce31".to_string(),
path: "".to_string(),
max_packet_size: "512M".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
2 changes: 2 additions & 0 deletions src/tests/domain/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async fn create_config() -> (ContainerAsync<Postgres>, DatabaseConfig) {
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down Expand Up @@ -156,6 +157,7 @@ async fn postgres_password_with_slash_test() {
generated_id: "5a1f0e3c-9b8a-4a8e-9b1b-0a1c2d3e4f5a".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

let db = DatabaseFactory::create_for_backup(config.clone()).await;
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/redis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async fn create_config() -> (ContainerAsync<Redis>, DatabaseConfig) {
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
1 change: 1 addition & 0 deletions src/tests/domain/valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async fn create_config() -> (ContainerAsync<Valkey>, DatabaseConfig) {
generated_id: "40875485-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
max_packet_size: "".to_string(),
options: std::collections::HashMap::new(),
};

(container, config)
Expand Down
122 changes: 122 additions & 0 deletions src/tests/services/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,125 @@ fn postgresql_cluster_respects_explicit_database() {

assert_eq!(cfg.databases[0].database, "maintenance");
}

#[test]
fn postgresql_options_keep_ownership_parses() {
let file = write_json(
r#"{
"databases": [
{
"name": "db1",
"type": "postgresql",
"username": "u",
"password": "p",
"port": 5432,
"host": "localhost",
"database": "mydb",
"generated_id": "16678159-ff7e-4c97-8c83-0adeff214681",
"options": {
"keep_ownership": true
}
}
]
}"#,
);

let service = ConfigService::new(test_context());
let cfg = service.load(Some(file.path().to_str().unwrap())).unwrap();

let keep = cfg.databases[0]
.options
.get("keep_ownership")
.and_then(|v| v.as_bool())
.unwrap_or(false);

assert!(keep);
}

#[test]
fn postgresql_options_absent_defaults_to_empty() {
let file = write_json(
r#"{
"databases": [
{
"name": "db1",
"type": "postgresql",
"username": "u",
"password": "p",
"port": 5432,
"host": "localhost",
"database": "mydb",
"generated_id": "16678159-ff7e-4c97-8c83-0adeff214681"
}
]
}"#,
);

let service = ConfigService::new(test_context());
let cfg = service.load(Some(file.path().to_str().unwrap())).unwrap();

assert!(cfg.databases[0].options.is_empty());
}

#[test]
fn postgresql_options_non_bool_keep_ownership_falls_back_to_false() {
let file = write_json(
r#"{
"databases": [
{
"name": "db1",
"type": "postgresql",
"username": "u",
"password": "p",
"port": 5432,
"host": "localhost",
"database": "mydb",
"generated_id": "16678159-ff7e-4c97-8c83-0adeff214681",
"options": {
"keep_ownership": "yes"
}
}
]
}"#,
);

let service = ConfigService::new(test_context());
let cfg = service.load(Some(file.path().to_str().unwrap())).unwrap();

let keep = cfg.databases[0]
.options
.get("keep_ownership")
.and_then(|v| v.as_bool())
.unwrap_or(false);

assert!(!keep);
}

#[test]
fn keep_ownership_extraction_logic() {
use serde_json::Value;
use std::collections::HashMap;

// true → keep ownership
let mut opts: HashMap<String, Value> = HashMap::new();
opts.insert("keep_ownership".to_string(), Value::Bool(true));
let keep = opts.get("keep_ownership").and_then(|v| v.as_bool()).unwrap_or(false);
assert!(keep, "should keep ownership when flag is true");

// false → strip
let mut opts2: HashMap<String, Value> = HashMap::new();
opts2.insert("keep_ownership".to_string(), Value::Bool(false));
let keep2 = opts2.get("keep_ownership").and_then(|v| v.as_bool()).unwrap_or(false);
assert!(!keep2, "should strip when flag is false");

// missing → strip
let opts3: HashMap<String, Value> = HashMap::new();
let keep3 = opts3.get("keep_ownership").and_then(|v| v.as_bool()).unwrap_or(false);
assert!(!keep3, "should strip when key absent");

// wrong type → strip
let mut opts4: HashMap<String, Value> = HashMap::new();
opts4.insert("keep_ownership".to_string(), Value::String("yes".to_string()));
let keep4 = opts4.get("keep_ownership").and_then(|v| v.as_bool()).unwrap_or(false);
assert!(!keep4, "should strip when value is not bool");
}
Loading