1use 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
17pub struct PostgresAdapter {
19 host: String,
20 port: u16,
21 ssh_auth: AuthMethod,
22}
23
24impl PostgresAdapter {
25 pub fn new(host: String, port: u16, ssh_auth: AuthMethod) -> Self {
33 Self {
34 host,
35 port,
36 ssh_auth,
37 }
38 }
39
40 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 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
94fn 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 crate::create_database_with_options(
137 &mut ssh,
138 &request.name,
139 Some(&request.owner),
140 request.encoding.as_deref(),
141 None, )
143 .await
144 .map_err(convert_error)?;
145
146 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 crate::create_user_with_options(
194 &mut ssh,
195 &request.username,
196 &request.password,
197 request.superuser,
198 false, false, Some(-1), )
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 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}