feat: Enhance documentation and add .gitignore entries
- Add new documentation sections for PostgreSQL installer functions and usage examples. Improves clarity and completeness of the documentation. - Add new files and patterns to .gitignore to prevent unnecessary files from being committed to the repository. Improves repository cleanliness and reduces clutter.
This commit is contained in:
355
src/postgresclient/installer.rs
Normal file
355
src/postgresclient/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 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),
|
||||
}
|
||||
}
|
@@ -2,9 +2,11 @@
|
||||
//
|
||||
// This module provides a PostgreSQL client for interacting with PostgreSQL databases.
|
||||
|
||||
mod installer;
|
||||
mod postgresclient;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// Re-export the public API
|
||||
pub use installer::*;
|
||||
pub use postgresclient::*;
|
||||
|
@@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params(
|
||||
/// This function sends a notification on the specified channel with the specified payload.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::postgresclient::notify;
|
||||
///
|
||||
/// notify("my_channel", "Hello, world!").expect("Failed to send notification");
|
||||
@@ -810,7 +810,7 @@ pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> {
|
||||
/// This function sends a notification on the specified channel with the specified payload using the connection pool.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::postgresclient::notify_with_pool;
|
||||
///
|
||||
/// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification");
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -134,6 +135,234 @@ mod postgres_client_tests {
|
||||
|
||||
// Integration tests that require a real PostgreSQL server
|
||||
// These tests will be skipped if PostgreSQL is not available
|
||||
#[cfg(test)]
|
||||
mod postgres_installer_tests {
|
||||
use super::*;
|
||||
use crate::virt::nerdctl::Container;
|
||||
|
||||
#[test]
|
||||
fn test_postgres_installer_config() {
|
||||
// Test default configuration
|
||||
let config = PostgresInstallerConfig::default();
|
||||
assert_eq!(config.container_name, "postgres");
|
||||
assert_eq!(config.version, "latest");
|
||||
assert_eq!(config.port, 5432);
|
||||
assert_eq!(config.username, "postgres");
|
||||
assert_eq!(config.password, "postgres");
|
||||
assert_eq!(config.data_dir, None);
|
||||
assert_eq!(config.env_vars.len(), 0);
|
||||
assert_eq!(config.persistent, true);
|
||||
|
||||
// Test builder pattern
|
||||
let config = PostgresInstallerConfig::new()
|
||||
.container_name("my-postgres")
|
||||
.version("15")
|
||||
.port(5433)
|
||||
.username("testuser")
|
||||
.password("testpass")
|
||||
.data_dir("/tmp/pgdata")
|
||||
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
|
||||
.persistent(false);
|
||||
|
||||
assert_eq!(config.container_name, "my-postgres");
|
||||
assert_eq!(config.version, "15");
|
||||
assert_eq!(config.port, 5433);
|
||||
assert_eq!(config.username, "testuser");
|
||||
assert_eq!(config.password, "testpass");
|
||||
assert_eq!(config.data_dir, Some("/tmp/pgdata".to_string()));
|
||||
assert_eq!(config.env_vars.len(), 1);
|
||||
assert_eq!(
|
||||
config.env_vars.get("POSTGRES_INITDB_ARGS").unwrap(),
|
||||
"--encoding=UTF8"
|
||||
);
|
||||
assert_eq!(config.persistent, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_installer_error() {
|
||||
// Test IoError
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
|
||||
let installer_error = PostgresInstallerError::IoError(io_error);
|
||||
assert!(format!("{}", installer_error).contains("I/O error"));
|
||||
|
||||
// Test NerdctlError
|
||||
let nerdctl_error = PostgresInstallerError::NerdctlError("Container not found".to_string());
|
||||
assert!(format!("{}", nerdctl_error).contains("Nerdctl error"));
|
||||
|
||||
// Test PostgresError
|
||||
let postgres_error =
|
||||
PostgresInstallerError::PostgresError("Database not found".to_string());
|
||||
assert!(format!("{}", postgres_error).contains("PostgreSQL error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_postgres_with_defaults() {
|
||||
// This is a unit test that doesn't actually install PostgreSQL
|
||||
// It just tests the configuration and error handling
|
||||
|
||||
// Test with default configuration
|
||||
let config = PostgresInstallerConfig::default();
|
||||
|
||||
// We expect this to fail because nerdctl is not available
|
||||
let result = install_postgres(config);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the error is a NerdctlError or IoError
|
||||
match result {
|
||||
Err(PostgresInstallerError::NerdctlError(_)) => {
|
||||
// This is fine, we expected a NerdctlError
|
||||
}
|
||||
Err(PostgresInstallerError::IoError(_)) => {
|
||||
// This is also fine, we expected an error
|
||||
}
|
||||
_ => panic!("Expected NerdctlError or IoError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_postgres_with_custom_config() {
|
||||
// Test with custom configuration
|
||||
let config = PostgresInstallerConfig::new()
|
||||
.container_name("test-postgres")
|
||||
.version("15")
|
||||
.port(5433)
|
||||
.username("testuser")
|
||||
.password("testpass")
|
||||
.data_dir("/tmp/pgdata")
|
||||
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
|
||||
.persistent(true);
|
||||
|
||||
// We expect this to fail because nerdctl is not available
|
||||
let result = install_postgres(config);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the error is a NerdctlError or IoError
|
||||
match result {
|
||||
Err(PostgresInstallerError::NerdctlError(_)) => {
|
||||
// This is fine, we expected a NerdctlError
|
||||
}
|
||||
Err(PostgresInstallerError::IoError(_)) => {
|
||||
// This is also fine, we expected an error
|
||||
}
|
||||
_ => panic!("Expected NerdctlError or IoError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_database() {
|
||||
// Create a mock container
|
||||
// In a real test, we would use mockall to create a mock container
|
||||
// But for this test, we'll just test the error handling
|
||||
|
||||
// We expect this to fail because the container is not running
|
||||
let result = create_database(
|
||||
&Container {
|
||||
name: "test-postgres".to_string(),
|
||||
container_id: None,
|
||||
image: Some("postgres:15".to_string()),
|
||||
config: HashMap::new(),
|
||||
ports: Vec::new(),
|
||||
volumes: Vec::new(),
|
||||
env_vars: HashMap::new(),
|
||||
network: None,
|
||||
network_aliases: Vec::new(),
|
||||
cpu_limit: None,
|
||||
memory_limit: None,
|
||||
memory_swap_limit: None,
|
||||
cpu_shares: None,
|
||||
restart_policy: None,
|
||||
health_check: None,
|
||||
detach: false,
|
||||
snapshotter: None,
|
||||
},
|
||||
"testdb",
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the error is a PostgresError
|
||||
match result {
|
||||
Err(PostgresInstallerError::PostgresError(msg)) => {
|
||||
assert!(msg.contains("Container is not running"));
|
||||
}
|
||||
_ => panic!("Expected PostgresError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_execute_sql() {
|
||||
// Create a mock container
|
||||
// In a real test, we would use mockall to create a mock container
|
||||
// But for this test, we'll just test the error handling
|
||||
|
||||
// We expect this to fail because the container is not running
|
||||
let result = execute_sql(
|
||||
&Container {
|
||||
name: "test-postgres".to_string(),
|
||||
container_id: None,
|
||||
image: Some("postgres:15".to_string()),
|
||||
config: HashMap::new(),
|
||||
ports: Vec::new(),
|
||||
volumes: Vec::new(),
|
||||
env_vars: HashMap::new(),
|
||||
network: None,
|
||||
network_aliases: Vec::new(),
|
||||
cpu_limit: None,
|
||||
memory_limit: None,
|
||||
memory_swap_limit: None,
|
||||
cpu_shares: None,
|
||||
restart_policy: None,
|
||||
health_check: None,
|
||||
detach: false,
|
||||
snapshotter: None,
|
||||
},
|
||||
"testdb",
|
||||
"SELECT 1",
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the error is a PostgresError
|
||||
match result {
|
||||
Err(PostgresInstallerError::PostgresError(msg)) => {
|
||||
assert!(msg.contains("Container is not running"));
|
||||
}
|
||||
_ => panic!("Expected PostgresError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_postgres_running() {
|
||||
// Create a mock container
|
||||
// In a real test, we would use mockall to create a mock container
|
||||
// But for this test, we'll just test the error handling
|
||||
|
||||
// We expect this to return false because the container is not running
|
||||
let result = is_postgres_running(&Container {
|
||||
name: "test-postgres".to_string(),
|
||||
container_id: None,
|
||||
image: Some("postgres:15".to_string()),
|
||||
config: HashMap::new(),
|
||||
ports: Vec::new(),
|
||||
volumes: Vec::new(),
|
||||
env_vars: HashMap::new(),
|
||||
network: None,
|
||||
network_aliases: Vec::new(),
|
||||
cpu_limit: None,
|
||||
memory_limit: None,
|
||||
memory_swap_limit: None,
|
||||
cpu_shares: None,
|
||||
restart_policy: None,
|
||||
health_check: None,
|
||||
detach: false,
|
||||
snapshotter: None,
|
||||
});
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod postgres_integration_tests {
|
||||
use super::*;
|
||||
|
Reference in New Issue
Block a user