lmrc_postgres/
adapter.rs

1//! # PostgreSQL Adapter
2//!
3//! Adapter implementation that wraps PostgreSQL user/database management functions
4//! and implements the `DatabaseProvider` port trait from `lmrc-ports`.
5//!
6//! This adapter allows PostgreSQL to be used interchangeably with other database
7//! providers in the LMRC Stack hexagonal architecture.
8
9use async_trait::async_trait;
10use crate::{Error as PostgresError, Privilege};
11use lmrc_ports::{
12    CreatedDatabase, DatabaseCreateRequest, DatabaseProvider, DatabaseUser, DatabaseUserRequest,
13    PortError, PortResult,
14};
15use lmrc_ssh::{AuthMethod, SshClient};
16
17/// PostgreSQL adapter implementing the DatabaseProvider port
18pub struct PostgresAdapter {
19    host: String,
20    port: u16,
21    ssh_auth: AuthMethod,
22}
23
24impl PostgresAdapter {
25    /// Create a new PostgreSQL adapter
26    ///
27    /// # Arguments
28    ///
29    /// * `host` - Database server hostname/IP
30    /// * `port` - PostgreSQL port (usually 5432)
31    /// * `ssh_auth` - SSH authentication method
32    pub fn new(host: String, port: u16, ssh_auth: AuthMethod) -> Self {
33        Self {
34            host,
35            port,
36            ssh_auth,
37        }
38    }
39
40    /// Create adapter from environment variables
41    ///
42    /// Reads:
43    /// - `POSTGRES_HOST` - Database server hostname/IP
44    /// - `POSTGRES_PORT` - PostgreSQL port (default: 5432)
45    /// - `POSTGRES_SSH_USER` - SSH username (default: "root")
46    /// - `POSTGRES_SSH_KEY` - SSH private key path (default: ~/.ssh/id_rsa)
47    ///
48    /// # Errors
49    ///
50    /// Returns error if required environment variables are missing
51    pub fn from_env() -> PortResult<Self> {
52        let host = std::env::var("POSTGRES_HOST").map_err(|_| {
53            PortError::InvalidConfiguration(
54                "POSTGRES_HOST environment variable is required".to_string(),
55            )
56        })?;
57
58        let port = std::env::var("POSTGRES_PORT")
59            .unwrap_or_else(|_| "5432".to_string())
60            .parse()
61            .map_err(|_| {
62                PortError::InvalidConfiguration("Invalid POSTGRES_PORT value".to_string())
63            })?;
64
65        let ssh_user = std::env::var("POSTGRES_SSH_USER").unwrap_or_else(|_| "root".to_string());
66
67        let ssh_key_path = std::env::var("POSTGRES_SSH_KEY").unwrap_or_else(|_| {
68            let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
69            format!("{}/.ssh/id_rsa", home)
70        });
71
72        let ssh_auth = AuthMethod::PublicKey {
73            username: ssh_user,
74            private_key_path: ssh_key_path,
75            passphrase: None,
76        };
77
78        Ok(Self::new(host, port, ssh_auth))
79    }
80
81    /// Create SSH client for database operations
82    async fn create_ssh_client(&self) -> PortResult<SshClient> {
83        let client = SshClient::new(&self.host, 22)
84            .map_err(|e| PortError::NetworkError(format!("Failed to create SSH client: {}", e)))?
85            .with_auth(self.ssh_auth.clone())
86            .connect()
87            .map_err(|e| {
88                PortError::NetworkError(format!("Failed to connect via SSH: {}", e))
89            })?;
90
91        Ok(client)
92    }
93}
94
95/// Convert PostgreSQL error to PortError
96fn convert_error(err: PostgresError) -> PortError {
97    match err {
98        PostgresError::Ssh(e) => PortError::NetworkError(format!("SSH error: {}", e)),
99        PostgresError::SshExecution { message, command } => PortError::OperationFailed(format!(
100            "SSH command '{}' failed: {}",
101            command, message
102        )),
103        PostgresError::Installation(msg) => PortError::OperationFailed(format!("Installation failed: {}", msg)),
104        PostgresError::Configuration(msg) => PortError::InvalidConfiguration(msg),
105        PostgresError::NotInstalled => PortError::OperationFailed("PostgreSQL is not installed".to_string()),
106        PostgresError::AlreadyInstalled(version) => PortError::AlreadyExists {
107            resource_type: "PostgreSQL installation".to_string(),
108            resource_id: version,
109        },
110        PostgresError::InvalidVersion(version) => {
111            PortError::InvalidConfiguration(format!("Invalid PostgreSQL version: {}", version))
112        }
113        PostgresError::InvalidConfig { parameter, value } => {
114            PortError::InvalidConfiguration(format!("Invalid config {} = {}", parameter, value))
115        }
116        PostgresError::MissingConfig(msg) => PortError::InvalidConfiguration(format!("Missing config: {}", msg)),
117        PostgresError::ServiceError(msg) => PortError::OperationFailed(format!("Service error: {}", msg)),
118        PostgresError::ConnectionTest(msg) => {
119            PortError::OperationFailed(format!("Connection test failed: {}", msg))
120        }
121        PostgresError::Uninstallation(msg) => {
122            PortError::OperationFailed(format!("Uninstallation failed: {}", msg))
123        }
124        PostgresError::Io(e) => PortError::OperationFailed(format!("IO error: {}", e)),
125        PostgresError::Serialization(e) => PortError::OperationFailed(format!("Serialization error: {}", e)),
126        PostgresError::Other(msg) => PortError::OperationFailed(msg),
127    }
128}
129
130#[async_trait]
131impl DatabaseProvider for PostgresAdapter {
132    async fn create_database(&self, request: DatabaseCreateRequest) -> PortResult<CreatedDatabase> {
133        let mut ssh = self.create_ssh_client().await?;
134
135        // Create database with options
136        // Signature: (ssh, database_name, owner, encoding, template)
137        crate::create_database_with_options(
138            &mut ssh,
139            &request.name,
140            Some(&request.owner),
141            request.encoding.as_deref(),
142            None, // template
143        )
144        .await
145        .map_err(convert_error)?;
146
147        // Build connection string
148        let connection_string = format!(
149            "postgresql://{}@{}:{}/{}",
150            request.owner, self.host, self.port, request.name
151        );
152
153        Ok(CreatedDatabase {
154            name: request.name,
155            owner: request.owner,
156            connection_string,
157            host: self.host.clone(),
158            port: self.port,
159        })
160    }
161
162    async fn drop_database(&self, name: &str) -> PortResult<()> {
163        let mut ssh = self.create_ssh_client().await?;
164
165        crate::drop_database(&mut ssh, name)
166            .await
167            .map_err(convert_error)?;
168
169        Ok(())
170    }
171
172    async fn list_databases(&self) -> PortResult<Vec<String>> {
173        let mut ssh = self.create_ssh_client().await?;
174
175        let databases = crate::list_databases(&mut ssh)
176            .await
177            .map_err(convert_error)?;
178
179        Ok(databases.into_iter().map(|db| db.name).collect())
180    }
181
182    async fn database_exists(&self, name: &str) -> PortResult<bool> {
183        let mut ssh = self.create_ssh_client().await?;
184
185        crate::database_exists(&mut ssh, name)
186            .await
187            .map_err(convert_error)
188    }
189
190    async fn create_user(&self, request: DatabaseUserRequest) -> PortResult<DatabaseUser> {
191        let mut ssh = self.create_ssh_client().await?;
192
193        // Create user with options
194        crate::create_user_with_options(
195            &mut ssh,
196            &request.username,
197            &request.password,
198            request.superuser,
199            false, // create_db
200            false, // create_role
201            Some(-1),    // connection_limit (-1 = unlimited)
202        )
203        .await
204        .map_err(convert_error)?;
205
206        Ok(DatabaseUser {
207            username: request.username,
208            superuser: request.superuser,
209        })
210    }
211
212    async fn drop_user(&self, username: &str) -> PortResult<()> {
213        let mut ssh = self.create_ssh_client().await?;
214
215        crate::drop_user(&mut ssh, username)
216            .await
217            .map_err(convert_error)?;
218
219        Ok(())
220    }
221
222    async fn grant_privileges(&self, database: &str, username: &str) -> PortResult<()> {
223        let mut ssh = self.create_ssh_client().await?;
224
225        // Grant all privileges on the database
226        // Signature: (ssh, database, username, privileges)
227        crate::grant_privileges(
228            &mut ssh,
229            database,
230            username,
231            &[Privilege::All],
232        )
233        .await
234        .map_err(convert_error)?;
235
236        Ok(())
237    }
238}