1use 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#[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
50pub struct PostgresInstallerConfig {
52 pub container_name: String,
54 pub version: String,
56 pub port: u16,
58 pub username: String,
60 pub password: String,
62 pub data_dir: Option<String>,
64 pub env_vars: HashMap<String, String>,
66 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 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn container_name(mut self, name: &str) -> Self {
93 self.container_name = name.to_string();
94 self
95 }
96
97 pub fn version(mut self, version: &str) -> Self {
99 self.version = version.to_string();
100 self
101 }
102
103 pub fn port(mut self, port: u16) -> Self {
105 self.port = port;
106 self
107 }
108
109 pub fn username(mut self, username: &str) -> Self {
111 self.username = username.to_string();
112 self
113 }
114
115 pub fn password(mut self, password: &str) -> Self {
117 self.password = password.to_string();
118 self
119 }
120
121 pub fn data_dir(mut self, data_dir: &str) -> Self {
123 self.data_dir = Some(data_dir.to_string());
124 self
125 }
126
127 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 pub fn persistent(mut self, persistent: bool) -> Self {
135 self.persistent = persistent;
136 self
137 }
138}
139
140pub fn install_postgres(
150 config: PostgresInstallerConfig,
151) -> Result<Container, PostgresInstallerError> {
152 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 let image = format!("postgres:{}", config.version);
170
171 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 let mut container = Container::new(&config.container_name).map_err(|e| {
187 PostgresInstallerError::NerdctlError(format!("Failed to create container: {}", e))
188 })?;
189
190 container.image = Some(image);
192
193 container = container.with_port(&format!("{}:5432", config.port));
195
196 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 for (key, value) in &config.env_vars {
203 container = container.with_env(key, value);
204 }
205
206 if let Some(dir) = data_dir {
208 container = container.with_volume(&format!("{}:/var/lib/postgresql/data", dir));
209 }
210
211 container = container.with_restart_policy("unless-stopped");
213
214 container = container.with_detach(true);
216
217 let container = container.build().map_err(|e| {
219 PostgresInstallerError::NerdctlError(format!("Failed to build container: {}", e))
220 })?;
221
222 println!("Waiting for PostgreSQL to start...");
224 thread::sleep(Duration::from_secs(5));
225
226 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
236pub fn create_database(container: &Container, db_name: &str) -> Result<(), PostgresInstallerError> {
247 if container.container_id.is_none() {
249 return Err(PostgresInstallerError::PostgresError(
250 "Container is not running".to_string(),
251 ));
252 }
253
254 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
268pub fn execute_sql(
280 container: &Container,
281 db_name: &str,
282 sql: &str,
283) -> Result<String, PostgresInstallerError> {
284 if container.container_id.is_none() {
286 return Err(PostgresInstallerError::PostgresError(
287 "Container is not running".to_string(),
288 ));
289 }
290
291 let temp_file = "/tmp/postgres_script.sql";
293 fs::write(temp_file, sql).map_err(|e| PostgresInstallerError::IoError(e))?;
294
295 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(©_result.stderr)
310 )));
311 }
312
313 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 fs::remove_file(temp_file).ok();
326
327 Ok(result.stdout)
328}
329
330pub fn is_postgres_running(container: &Container) -> Result<bool, PostgresInstallerError> {
340 if container.container_id.is_none() {
342 return Ok(false);
343 }
344
345 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}