Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
- Pull the PostgreSQL image before installation to ensure the latest version is used. This improves reliability and reduces the chance of using outdated images. Improves the robustness of the installation process. - Added comprehensive unit tests for `PostgresInstallerConfig`, `PostgresInstallerError`, `install_postgres`, `create_database`, `execute_sql`, and `is_postgres_running` functions to ensure correctness and handle potential errors effectively. Improves code quality and reduces the risk of regressions.
356 lines
10 KiB
Rust
356 lines
10 KiB
Rust
// PostgreSQL installer module
|
|
//
|
|
// This module provides functionality to install and configure PostgreSQL using nerdctl.
|
|
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use crate::virt::nerdctl::Container;
|
|
use std::error::Error;
|
|
use std::fmt;
|
|
|
|
// Custom error type for PostgreSQL installer
|
|
#[derive(Debug)]
|
|
pub enum PostgresInstallerError {
|
|
IoError(std::io::Error),
|
|
NerdctlError(String),
|
|
PostgresError(String),
|
|
}
|
|
|
|
impl fmt::Display for PostgresInstallerError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
PostgresInstallerError::IoError(e) => write!(f, "I/O error: {}", e),
|
|
PostgresInstallerError::NerdctlError(e) => write!(f, "Nerdctl error: {}", e),
|
|
PostgresInstallerError::PostgresError(e) => write!(f, "PostgreSQL error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for PostgresInstallerError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match self {
|
|
PostgresInstallerError::IoError(e) => Some(e),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for PostgresInstallerError {
|
|
fn from(error: std::io::Error) -> Self {
|
|
PostgresInstallerError::IoError(error)
|
|
}
|
|
}
|
|
|
|
/// PostgreSQL installer configuration
|
|
pub struct PostgresInstallerConfig {
|
|
/// Container name for PostgreSQL
|
|
pub container_name: String,
|
|
/// PostgreSQL version to install
|
|
pub version: String,
|
|
/// Port to expose PostgreSQL on
|
|
pub port: u16,
|
|
/// Username for PostgreSQL
|
|
pub username: String,
|
|
/// Password for PostgreSQL
|
|
pub password: String,
|
|
/// Data directory for PostgreSQL
|
|
pub data_dir: Option<String>,
|
|
/// Environment variables for PostgreSQL
|
|
pub env_vars: HashMap<String, String>,
|
|
/// Whether to use persistent storage
|
|
pub persistent: bool,
|
|
}
|
|
|
|
impl Default for PostgresInstallerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
container_name: "postgres".to_string(),
|
|
version: "latest".to_string(),
|
|
port: 5432,
|
|
username: "postgres".to_string(),
|
|
password: "postgres".to_string(),
|
|
data_dir: None,
|
|
env_vars: HashMap::new(),
|
|
persistent: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PostgresInstallerConfig {
|
|
/// Create a new PostgreSQL installer configuration with default values
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Set the container name
|
|
pub fn container_name(mut self, name: &str) -> Self {
|
|
self.container_name = name.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the PostgreSQL version
|
|
pub fn version(mut self, version: &str) -> Self {
|
|
self.version = version.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the port to expose PostgreSQL on
|
|
pub fn port(mut self, port: u16) -> Self {
|
|
self.port = port;
|
|
self
|
|
}
|
|
|
|
/// Set the username for PostgreSQL
|
|
pub fn username(mut self, username: &str) -> Self {
|
|
self.username = username.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the password for PostgreSQL
|
|
pub fn password(mut self, password: &str) -> Self {
|
|
self.password = password.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the data directory for PostgreSQL
|
|
pub fn data_dir(mut self, data_dir: &str) -> Self {
|
|
self.data_dir = Some(data_dir.to_string());
|
|
self
|
|
}
|
|
|
|
/// Add an environment variable
|
|
pub fn env_var(mut self, key: &str, value: &str) -> Self {
|
|
self.env_vars.insert(key.to_string(), value.to_string());
|
|
self
|
|
}
|
|
|
|
/// Set whether to use persistent storage
|
|
pub fn persistent(mut self, persistent: bool) -> Self {
|
|
self.persistent = persistent;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Install PostgreSQL using nerdctl
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `config` - PostgreSQL installer configuration
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Result<Container, PostgresInstallerError>` - Container instance or error
|
|
pub fn install_postgres(
|
|
config: PostgresInstallerConfig,
|
|
) -> Result<Container, PostgresInstallerError> {
|
|
// Create the data directory if it doesn't exist and persistent storage is enabled
|
|
let data_dir = if config.persistent {
|
|
let dir = config.data_dir.unwrap_or_else(|| {
|
|
let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
format!("{}/.postgres-data", home_dir)
|
|
});
|
|
|
|
if !Path::new(&dir).exists() {
|
|
fs::create_dir_all(&dir).map_err(|e| PostgresInstallerError::IoError(e))?;
|
|
}
|
|
|
|
Some(dir)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Build the image name
|
|
let image = format!("postgres:{}", config.version);
|
|
|
|
// Pull the PostgreSQL image to ensure we have the latest version
|
|
println!("Pulling PostgreSQL image: {}...", image);
|
|
let pull_result = Command::new("nerdctl")
|
|
.args(&["pull", &image])
|
|
.output()
|
|
.map_err(|e| PostgresInstallerError::IoError(e))?;
|
|
|
|
if !pull_result.status.success() {
|
|
return Err(PostgresInstallerError::NerdctlError(format!(
|
|
"Failed to pull PostgreSQL image: {}",
|
|
String::from_utf8_lossy(&pull_result.stderr)
|
|
)));
|
|
}
|
|
|
|
// Create the container
|
|
let mut container = Container::new(&config.container_name).map_err(|e| {
|
|
PostgresInstallerError::NerdctlError(format!("Failed to create container: {}", e))
|
|
})?;
|
|
|
|
// Set the image
|
|
container.image = Some(image);
|
|
|
|
// Set the port
|
|
container = container.with_port(&format!("{}:5432", config.port));
|
|
|
|
// Set environment variables
|
|
container = container.with_env("POSTGRES_USER", &config.username);
|
|
container = container.with_env("POSTGRES_PASSWORD", &config.password);
|
|
container = container.with_env("POSTGRES_DB", "postgres");
|
|
|
|
// Add custom environment variables
|
|
for (key, value) in &config.env_vars {
|
|
container = container.with_env(key, value);
|
|
}
|
|
|
|
// Add volume for persistent storage if enabled
|
|
if let Some(dir) = data_dir {
|
|
container = container.with_volume(&format!("{}:/var/lib/postgresql/data", dir));
|
|
}
|
|
|
|
// Set restart policy
|
|
container = container.with_restart_policy("unless-stopped");
|
|
|
|
// Set detach mode
|
|
container = container.with_detach(true);
|
|
|
|
// Build and start the container
|
|
let container = container.build().map_err(|e| {
|
|
PostgresInstallerError::NerdctlError(format!("Failed to build container: {}", e))
|
|
})?;
|
|
|
|
// Wait for PostgreSQL to start
|
|
println!("Waiting for PostgreSQL to start...");
|
|
thread::sleep(Duration::from_secs(5));
|
|
|
|
// Set environment variables for PostgreSQL client
|
|
env::set_var("POSTGRES_HOST", "localhost");
|
|
env::set_var("POSTGRES_PORT", config.port.to_string());
|
|
env::set_var("POSTGRES_USER", config.username);
|
|
env::set_var("POSTGRES_PASSWORD", config.password);
|
|
env::set_var("POSTGRES_DB", "postgres");
|
|
|
|
Ok(container)
|
|
}
|
|
|
|
/// Create a new database in PostgreSQL
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `container` - PostgreSQL container
|
|
/// * `db_name` - Database name
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Result<(), PostgresInstallerError>` - Ok if successful, Err otherwise
|
|
pub fn create_database(container: &Container, db_name: &str) -> Result<(), PostgresInstallerError> {
|
|
// Check if container is running
|
|
if container.container_id.is_none() {
|
|
return Err(PostgresInstallerError::PostgresError(
|
|
"Container is not running".to_string(),
|
|
));
|
|
}
|
|
|
|
// Execute the command to create the database
|
|
let command = format!(
|
|
"createdb -U {} {}",
|
|
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
|
|
db_name
|
|
);
|
|
|
|
container.exec(&command).map_err(|e| {
|
|
PostgresInstallerError::NerdctlError(format!("Failed to create database: {}", e))
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Execute a SQL script in PostgreSQL
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `container` - PostgreSQL container
|
|
/// * `db_name` - Database name
|
|
/// * `sql` - SQL script to execute
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Result<String, PostgresInstallerError>` - Output of the command or error
|
|
pub fn execute_sql(
|
|
container: &Container,
|
|
db_name: &str,
|
|
sql: &str,
|
|
) -> Result<String, PostgresInstallerError> {
|
|
// Check if container is running
|
|
if container.container_id.is_none() {
|
|
return Err(PostgresInstallerError::PostgresError(
|
|
"Container is not running".to_string(),
|
|
));
|
|
}
|
|
|
|
// Create a temporary file with the SQL script
|
|
let temp_file = "/tmp/postgres_script.sql";
|
|
fs::write(temp_file, sql).map_err(|e| PostgresInstallerError::IoError(e))?;
|
|
|
|
// Copy the file to the container
|
|
let container_id = container.container_id.as_ref().unwrap();
|
|
let copy_result = Command::new("nerdctl")
|
|
.args(&[
|
|
"cp",
|
|
temp_file,
|
|
&format!("{}:/tmp/script.sql", container_id),
|
|
])
|
|
.output()
|
|
.map_err(|e| PostgresInstallerError::IoError(e))?;
|
|
|
|
if !copy_result.status.success() {
|
|
return Err(PostgresInstallerError::PostgresError(format!(
|
|
"Failed to copy SQL script to container: {}",
|
|
String::from_utf8_lossy(©_result.stderr)
|
|
)));
|
|
}
|
|
|
|
// Execute the SQL script
|
|
let command = format!(
|
|
"psql -U {} -d {} -f /tmp/script.sql",
|
|
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
|
|
db_name
|
|
);
|
|
|
|
let result = container.exec(&command).map_err(|e| {
|
|
PostgresInstallerError::NerdctlError(format!("Failed to execute SQL script: {}", e))
|
|
})?;
|
|
|
|
// Clean up
|
|
fs::remove_file(temp_file).ok();
|
|
|
|
Ok(result.stdout)
|
|
}
|
|
|
|
/// Check if PostgreSQL is running
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `container` - PostgreSQL container
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Result<bool, PostgresInstallerError>` - true if running, false otherwise, or error
|
|
pub fn is_postgres_running(container: &Container) -> Result<bool, PostgresInstallerError> {
|
|
// Check if container is running
|
|
if container.container_id.is_none() {
|
|
return Ok(false);
|
|
}
|
|
|
|
// Execute a simple query to check if PostgreSQL is running
|
|
let command = format!(
|
|
"psql -U {} -c 'SELECT 1'",
|
|
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string())
|
|
);
|
|
|
|
match container.exec(&command) {
|
|
Ok(_) => Ok(true),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|