Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Move src/postgresclient/ to postgresclient/ package structure - Add comprehensive test suite (28 tests) with real PostgreSQL operations - Maintain Rhai integration with all 10 wrapper functions - Update workspace configuration and dependencies - Add complete documentation with usage examples - Remove old module and update all references - Ensure zero regressions in existing functionality Closes: postgresclient monorepo conversion
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 sal_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),
|
|
}
|
|
}
|