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").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 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
95fn 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 crate::create_database_with_options(
138 &mut ssh,
139 &request.name,
140 Some(&request.owner),
141 request.encoding.as_deref(),
142 None, )
144 .await
145 .map_err(convert_error)?;
146
147 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 crate::create_user_with_options(
195 &mut ssh,
196 &request.username,
197 &request.password,
198 request.superuser,
199 false, false, Some(-1), )
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 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}