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")
68            .or_else(|_| std::env::var("SSH_KEY_PATH"))
69            .unwrap_or_else(|_| ".ssh/id_rsa".to_string());
70
71        let ssh_auth = AuthMethod::PublicKey {
72            username: ssh_user,
73            private_key_path: ssh_key_path,
74            passphrase: None,
75        };
76
77        Ok(Self::new(host, port, ssh_auth))
78    }
79
80    /// Create SSH client for database operations
81    async fn create_ssh_client(&self) -> PortResult<SshClient> {
82        let client = SshClient::new(&self.host, 22)
83            .map_err(|e| PortError::NetworkError(format!("Failed to create SSH client: {}", e)))?
84            .with_auth(self.ssh_auth.clone())
85            .connect()
86            .map_err(|e| {
87                PortError::NetworkError(format!("Failed to connect via SSH: {}", e))
88            })?;
89
90        Ok(client)
91    }
92}
93
94/// Convert PostgreSQL error to PortError
95fn convert_error(err: PostgresError) -> PortError {
96    match err {
97        PostgresError::Ssh(e) => PortError::NetworkError(format!("SSH error: {}", e)),
98        PostgresError::SshExecution { message, command } => PortError::OperationFailed(format!(
99            "SSH command '{}' failed: {}",
100            command, message
101        )),
102        PostgresError::Installation(msg) => PortError::OperationFailed(format!("Installation failed: {}", msg)),
103        PostgresError::Configuration(msg) => PortError::InvalidConfiguration(msg),
104        PostgresError::NotInstalled => PortError::OperationFailed("PostgreSQL is not installed".to_string()),
105        PostgresError::AlreadyInstalled(version) => PortError::AlreadyExists {
106            resource_type: "PostgreSQL installation".to_string(),
107            resource_id: version,
108        },
109        PostgresError::InvalidVersion(version) => {
110            PortError::InvalidConfiguration(format!("Invalid PostgreSQL version: {}", version))
111        }
112        PostgresError::InvalidConfig { parameter, value } => {
113            PortError::InvalidConfiguration(format!("Invalid config {} = {}", parameter, value))
114        }
115        PostgresError::MissingConfig(msg) => PortError::InvalidConfiguration(format!("Missing config: {}", msg)),
116        PostgresError::ServiceError(msg) => PortError::OperationFailed(format!("Service error: {}", msg)),
117        PostgresError::ConnectionTest(msg) => {
118            PortError::OperationFailed(format!("Connection test failed: {}", msg))
119        }
120        PostgresError::Uninstallation(msg) => {
121            PortError::OperationFailed(format!("Uninstallation failed: {}", msg))
122        }
123        PostgresError::Io(e) => PortError::OperationFailed(format!("IO error: {}", e)),
124        PostgresError::Serialization(e) => PortError::OperationFailed(format!("Serialization error: {}", e)),
125        PostgresError::Other(msg) => PortError::OperationFailed(msg),
126    }
127}
128
129#[async_trait]
130impl DatabaseProvider for PostgresAdapter {
131    async fn create_database(&self, request: DatabaseCreateRequest) -> PortResult<CreatedDatabase> {
132        let mut ssh = self.create_ssh_client().await?;
133
134        // Create database with options
135        // Signature: (ssh, database_name, owner, encoding, template)
136        crate::create_database_with_options(
137            &mut ssh,
138            &request.name,
139            Some(&request.owner),
140            request.encoding.as_deref(),
141            None, // template
142        )
143        .await
144        .map_err(convert_error)?;
145
146        // Build connection string
147        let connection_string = format!(
148            "postgresql://{}@{}:{}/{}",
149            request.owner, self.host, self.port, request.name
150        );
151
152        Ok(CreatedDatabase {
153            name: request.name,
154            owner: request.owner,
155            connection_string,
156            host: self.host.clone(),
157            port: self.port,
158        })
159    }
160
161    async fn drop_database(&self, name: &str) -> PortResult<()> {
162        let mut ssh = self.create_ssh_client().await?;
163
164        crate::drop_database(&mut ssh, name)
165            .await
166            .map_err(convert_error)?;
167
168        Ok(())
169    }
170
171    async fn list_databases(&self) -> PortResult<Vec<String>> {
172        let mut ssh = self.create_ssh_client().await?;
173
174        let databases = crate::list_databases(&mut ssh)
175            .await
176            .map_err(convert_error)?;
177
178        Ok(databases.into_iter().map(|db| db.name).collect())
179    }
180
181    async fn database_exists(&self, name: &str) -> PortResult<bool> {
182        let mut ssh = self.create_ssh_client().await?;
183
184        crate::database_exists(&mut ssh, name)
185            .await
186            .map_err(convert_error)
187    }
188
189    async fn create_user(&self, request: DatabaseUserRequest) -> PortResult<DatabaseUser> {
190        let mut ssh = self.create_ssh_client().await?;
191
192        // Create user with options
193        crate::create_user_with_options(
194            &mut ssh,
195            &request.username,
196            &request.password,
197            request.superuser,
198            false, // create_db
199            false, // create_role
200            Some(-1),    // connection_limit (-1 = unlimited)
201        )
202        .await
203        .map_err(convert_error)?;
204
205        Ok(DatabaseUser {
206            username: request.username,
207            superuser: request.superuser,
208        })
209    }
210
211    async fn drop_user(&self, username: &str) -> PortResult<()> {
212        let mut ssh = self.create_ssh_client().await?;
213
214        crate::drop_user(&mut ssh, username)
215            .await
216            .map_err(convert_error)?;
217
218        Ok(())
219    }
220
221    async fn grant_privileges(&self, database: &str, username: &str) -> PortResult<()> {
222        let mut ssh = self.create_ssh_client().await?;
223
224        // Grant all privileges on the database
225        // Signature: (ssh, database, username, privileges)
226        crate::grant_privileges(
227            &mut ssh,
228            database,
229            username,
230            &[Privilege::All],
231        )
232        .await
233        .map_err(convert_error)?;
234
235        Ok(())
236    }
237}