sal_postgresclient/
installer.rs

1// PostgreSQL installer module
2//
3// This module provides functionality to install and configure PostgreSQL using nerdctl.
4
5use std::collections::HashMap;
6use std::env;
7use std::fs;
8use std::path::Path;
9use std::process::Command;
10use std::thread;
11use std::time::Duration;
12
13use sal_virt::nerdctl::Container;
14use std::error::Error;
15use std::fmt;
16
17// Custom error type for PostgreSQL installer
18#[derive(Debug)]
19pub enum PostgresInstallerError {
20    IoError(std::io::Error),
21    NerdctlError(String),
22    PostgresError(String),
23}
24
25impl fmt::Display for PostgresInstallerError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            PostgresInstallerError::IoError(e) => write!(f, "I/O error: {}", e),
29            PostgresInstallerError::NerdctlError(e) => write!(f, "Nerdctl error: {}", e),
30            PostgresInstallerError::PostgresError(e) => write!(f, "PostgreSQL error: {}", e),
31        }
32    }
33}
34
35impl Error for PostgresInstallerError {
36    fn source(&self) -> Option<&(dyn Error + 'static)> {
37        match self {
38            PostgresInstallerError::IoError(e) => Some(e),
39            _ => None,
40        }
41    }
42}
43
44impl From<std::io::Error> for PostgresInstallerError {
45    fn from(error: std::io::Error) -> Self {
46        PostgresInstallerError::IoError(error)
47    }
48}
49
50/// PostgreSQL installer configuration
51pub struct PostgresInstallerConfig {
52    /// Container name for PostgreSQL
53    pub container_name: String,
54    /// PostgreSQL version to install
55    pub version: String,
56    /// Port to expose PostgreSQL on
57    pub port: u16,
58    /// Username for PostgreSQL
59    pub username: String,
60    /// Password for PostgreSQL
61    pub password: String,
62    /// Data directory for PostgreSQL
63    pub data_dir: Option<String>,
64    /// Environment variables for PostgreSQL
65    pub env_vars: HashMap<String, String>,
66    /// Whether to use persistent storage
67    pub persistent: bool,
68}
69
70impl Default for PostgresInstallerConfig {
71    fn default() -> Self {
72        Self {
73            container_name: "postgres".to_string(),
74            version: "latest".to_string(),
75            port: 5432,
76            username: "postgres".to_string(),
77            password: "postgres".to_string(),
78            data_dir: None,
79            env_vars: HashMap::new(),
80            persistent: true,
81        }
82    }
83}
84
85impl PostgresInstallerConfig {
86    /// Create a new PostgreSQL installer configuration with default values
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Set the container name
92    pub fn container_name(mut self, name: &str) -> Self {
93        self.container_name = name.to_string();
94        self
95    }
96
97    /// Set the PostgreSQL version
98    pub fn version(mut self, version: &str) -> Self {
99        self.version = version.to_string();
100        self
101    }
102
103    /// Set the port to expose PostgreSQL on
104    pub fn port(mut self, port: u16) -> Self {
105        self.port = port;
106        self
107    }
108
109    /// Set the username for PostgreSQL
110    pub fn username(mut self, username: &str) -> Self {
111        self.username = username.to_string();
112        self
113    }
114
115    /// Set the password for PostgreSQL
116    pub fn password(mut self, password: &str) -> Self {
117        self.password = password.to_string();
118        self
119    }
120
121    /// Set the data directory for PostgreSQL
122    pub fn data_dir(mut self, data_dir: &str) -> Self {
123        self.data_dir = Some(data_dir.to_string());
124        self
125    }
126
127    /// Add an environment variable
128    pub fn env_var(mut self, key: &str, value: &str) -> Self {
129        self.env_vars.insert(key.to_string(), value.to_string());
130        self
131    }
132
133    /// Set whether to use persistent storage
134    pub fn persistent(mut self, persistent: bool) -> Self {
135        self.persistent = persistent;
136        self
137    }
138}
139
140/// Install PostgreSQL using nerdctl
141///
142/// # Arguments
143///
144/// * `config` - PostgreSQL installer configuration
145///
146/// # Returns
147///
148/// * `Result<Container, PostgresInstallerError>` - Container instance or error
149pub fn install_postgres(
150    config: PostgresInstallerConfig,
151) -> Result<Container, PostgresInstallerError> {
152    // Create the data directory if it doesn't exist and persistent storage is enabled
153    let data_dir = if config.persistent {
154        let dir = config.data_dir.unwrap_or_else(|| {
155            let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
156            format!("{}/.postgres-data", home_dir)
157        });
158
159        if !Path::new(&dir).exists() {
160            fs::create_dir_all(&dir).map_err(|e| PostgresInstallerError::IoError(e))?;
161        }
162
163        Some(dir)
164    } else {
165        None
166    };
167
168    // Build the image name
169    let image = format!("postgres:{}", config.version);
170
171    // Pull the PostgreSQL image to ensure we have the latest version
172    println!("Pulling PostgreSQL image: {}...", image);
173    let pull_result = Command::new("nerdctl")
174        .args(&["pull", &image])
175        .output()
176        .map_err(|e| PostgresInstallerError::IoError(e))?;
177
178    if !pull_result.status.success() {
179        return Err(PostgresInstallerError::NerdctlError(format!(
180            "Failed to pull PostgreSQL image: {}",
181            String::from_utf8_lossy(&pull_result.stderr)
182        )));
183    }
184
185    // Create the container
186    let mut container = Container::new(&config.container_name).map_err(|e| {
187        PostgresInstallerError::NerdctlError(format!("Failed to create container: {}", e))
188    })?;
189
190    // Set the image
191    container.image = Some(image);
192
193    // Set the port
194    container = container.with_port(&format!("{}:5432", config.port));
195
196    // Set environment variables
197    container = container.with_env("POSTGRES_USER", &config.username);
198    container = container.with_env("POSTGRES_PASSWORD", &config.password);
199    container = container.with_env("POSTGRES_DB", "postgres");
200
201    // Add custom environment variables
202    for (key, value) in &config.env_vars {
203        container = container.with_env(key, value);
204    }
205
206    // Add volume for persistent storage if enabled
207    if let Some(dir) = data_dir {
208        container = container.with_volume(&format!("{}:/var/lib/postgresql/data", dir));
209    }
210
211    // Set restart policy
212    container = container.with_restart_policy("unless-stopped");
213
214    // Set detach mode
215    container = container.with_detach(true);
216
217    // Build and start the container
218    let container = container.build().map_err(|e| {
219        PostgresInstallerError::NerdctlError(format!("Failed to build container: {}", e))
220    })?;
221
222    // Wait for PostgreSQL to start
223    println!("Waiting for PostgreSQL to start...");
224    thread::sleep(Duration::from_secs(5));
225
226    // Set environment variables for PostgreSQL client
227    env::set_var("POSTGRES_HOST", "localhost");
228    env::set_var("POSTGRES_PORT", config.port.to_string());
229    env::set_var("POSTGRES_USER", config.username);
230    env::set_var("POSTGRES_PASSWORD", config.password);
231    env::set_var("POSTGRES_DB", "postgres");
232
233    Ok(container)
234}
235
236/// Create a new database in PostgreSQL
237///
238/// # Arguments
239///
240/// * `container` - PostgreSQL container
241/// * `db_name` - Database name
242///
243/// # Returns
244///
245/// * `Result<(), PostgresInstallerError>` - Ok if successful, Err otherwise
246pub fn create_database(container: &Container, db_name: &str) -> Result<(), PostgresInstallerError> {
247    // Check if container is running
248    if container.container_id.is_none() {
249        return Err(PostgresInstallerError::PostgresError(
250            "Container is not running".to_string(),
251        ));
252    }
253
254    // Execute the command to create the database
255    let command = format!(
256        "createdb -U {} {}",
257        env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
258        db_name
259    );
260
261    container.exec(&command).map_err(|e| {
262        PostgresInstallerError::NerdctlError(format!("Failed to create database: {}", e))
263    })?;
264
265    Ok(())
266}
267
268/// Execute a SQL script in PostgreSQL
269///
270/// # Arguments
271///
272/// * `container` - PostgreSQL container
273/// * `db_name` - Database name
274/// * `sql` - SQL script to execute
275///
276/// # Returns
277///
278/// * `Result<String, PostgresInstallerError>` - Output of the command or error
279pub fn execute_sql(
280    container: &Container,
281    db_name: &str,
282    sql: &str,
283) -> Result<String, PostgresInstallerError> {
284    // Check if container is running
285    if container.container_id.is_none() {
286        return Err(PostgresInstallerError::PostgresError(
287            "Container is not running".to_string(),
288        ));
289    }
290
291    // Create a temporary file with the SQL script
292    let temp_file = "/tmp/postgres_script.sql";
293    fs::write(temp_file, sql).map_err(|e| PostgresInstallerError::IoError(e))?;
294
295    // Copy the file to the container
296    let container_id = container.container_id.as_ref().unwrap();
297    let copy_result = Command::new("nerdctl")
298        .args(&[
299            "cp",
300            temp_file,
301            &format!("{}:/tmp/script.sql", container_id),
302        ])
303        .output()
304        .map_err(|e| PostgresInstallerError::IoError(e))?;
305
306    if !copy_result.status.success() {
307        return Err(PostgresInstallerError::PostgresError(format!(
308            "Failed to copy SQL script to container: {}",
309            String::from_utf8_lossy(&copy_result.stderr)
310        )));
311    }
312
313    // Execute the SQL script
314    let command = format!(
315        "psql -U {} -d {} -f /tmp/script.sql",
316        env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
317        db_name
318    );
319
320    let result = container.exec(&command).map_err(|e| {
321        PostgresInstallerError::NerdctlError(format!("Failed to execute SQL script: {}", e))
322    })?;
323
324    // Clean up
325    fs::remove_file(temp_file).ok();
326
327    Ok(result.stdout)
328}
329
330/// Check if PostgreSQL is running
331///
332/// # Arguments
333///
334/// * `container` - PostgreSQL container
335///
336/// # Returns
337///
338/// * `Result<bool, PostgresInstallerError>` - true if running, false otherwise, or error
339pub fn is_postgres_running(container: &Container) -> Result<bool, PostgresInstallerError> {
340    // Check if container is running
341    if container.container_id.is_none() {
342        return Ok(false);
343    }
344
345    // Execute a simple query to check if PostgreSQL is running
346    let command = format!(
347        "psql -U {} -c 'SELECT 1'",
348        env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string())
349    );
350
351    match container.exec(&command) {
352        Ok(_) => Ok(true),
353        Err(_) => Ok(false),
354    }
355}