feat: convert postgresclient module to independent sal-postgresclient package
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
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
This commit is contained in:
355
postgresclient/src/installer.rs
Normal file
355
postgresclient/src/installer.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
// 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),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user