lmrc-postgres 0.3.16

PostgreSQL management library for the LMRC Stack - comprehensive library for managing PostgreSQL installations on remote servers via SSH
Documentation
//! # PostgreSQL Adapter
//!
//! Adapter implementation that wraps PostgreSQL user/database management functions
//! and implements the `DatabaseProvider` port trait from `lmrc-ports`.
//!
//! This adapter allows PostgreSQL to be used interchangeably with other database
//! providers in the LMRC Stack hexagonal architecture.

use async_trait::async_trait;
use crate::{Error as PostgresError, Privilege};
use lmrc_ports::{
    CreatedDatabase, DatabaseCreateRequest, DatabaseProvider, DatabaseUser, DatabaseUserRequest,
    PortError, PortResult,
};
use lmrc_ssh::{AuthMethod, SshClient};

/// PostgreSQL adapter implementing the DatabaseProvider port
///
/// This adapter is stateless and accepts connection details via request parameters.
pub struct PostgresAdapter;

impl PostgresAdapter {
    /// Create a new PostgreSQL adapter
    pub fn new() -> Self {
        Self
    }

    /// Create SSH client for database operations
    ///
    /// # Arguments
    ///
    /// * `host` - Database server host/IP
    /// * `ssh_key_path` - SSH private key path for server access
    async fn create_ssh_client(&self, host: &str, ssh_key_path: &str) -> PortResult<SshClient> {
        let ssh_auth = AuthMethod::PublicKey {
            username: "root".to_string(),
            private_key_path: ssh_key_path.to_string(),
            passphrase: None,
        };

        let client = SshClient::new(host, 22)
            .map_err(|e| PortError::NetworkError(format!("Failed to create SSH client: {}", e)))?
            .with_auth(ssh_auth)
            .connect()
            .map_err(|e| {
                PortError::NetworkError(format!("Failed to connect via SSH: {}", e))
            })?;

        Ok(client)
    }
}

/// Convert PostgreSQL error to PortError
fn convert_error(err: PostgresError) -> PortError {
    match err {
        PostgresError::Ssh(e) => PortError::NetworkError(format!("SSH error: {}", e)),
        PostgresError::SshExecution { message, command } => PortError::OperationFailed(format!(
            "SSH command '{}' failed: {}",
            command, message
        )),
        PostgresError::Installation(msg) => PortError::OperationFailed(format!("Installation failed: {}", msg)),
        PostgresError::Configuration(msg) => PortError::InvalidConfiguration(msg),
        PostgresError::NotInstalled => PortError::OperationFailed("PostgreSQL is not installed".to_string()),
        PostgresError::AlreadyInstalled(version) => PortError::AlreadyExists {
            resource_type: "PostgreSQL installation".to_string(),
            resource_id: version,
        },
        PostgresError::InvalidVersion(version) => {
            PortError::InvalidConfiguration(format!("Invalid PostgreSQL version: {}", version))
        }
        PostgresError::InvalidConfig { parameter, value } => {
            PortError::InvalidConfiguration(format!("Invalid config {} = {}", parameter, value))
        }
        PostgresError::MissingConfig(msg) => PortError::InvalidConfiguration(format!("Missing config: {}", msg)),
        PostgresError::ServiceError(msg) => PortError::OperationFailed(format!("Service error: {}", msg)),
        PostgresError::ConnectionTest(msg) => {
            PortError::OperationFailed(format!("Connection test failed: {}", msg))
        }
        PostgresError::Uninstallation(msg) => {
            PortError::OperationFailed(format!("Uninstallation failed: {}", msg))
        }
        PostgresError::Io(e) => PortError::OperationFailed(format!("IO error: {}", e)),
        PostgresError::Serialization(e) => PortError::OperationFailed(format!("Serialization error: {}", e)),
        PostgresError::Other(msg) => PortError::OperationFailed(msg),
    }
}

#[async_trait]
impl DatabaseProvider for PostgresAdapter {
    async fn create_database(&self, request: DatabaseCreateRequest) -> PortResult<CreatedDatabase> {
        let mut ssh = self.create_ssh_client(&request.host, &request.ssh_key_path).await?;

        // Create database with options
        // Signature: (ssh, database_name, owner, encoding, template)
        crate::create_database_with_options(
            &mut ssh,
            &request.name,
            Some(&request.owner),
            request.encoding.as_deref(),
            None, // template
        )
        .await
        .map_err(convert_error)?;

        // Build connection string
        let connection_string = format!(
            "postgresql://{}@{}:{}/{}",
            request.owner, request.host, request.port, request.name
        );

        Ok(CreatedDatabase {
            name: request.name.clone(),
            owner: request.owner.clone(),
            connection_string,
            host: request.host,
            port: request.port,
        })
    }

    async fn drop_database(&self, name: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;

        crate::drop_database(&mut ssh, name)
            .await
            .map_err(convert_error)?;

        Ok(())
    }

    async fn list_databases(&self, host: &str, port: u16, ssh_key_path: &str) -> PortResult<Vec<String>> {
        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;

        let databases = crate::list_databases(&mut ssh)
            .await
            .map_err(convert_error)?;

        Ok(databases.into_iter().map(|db| db.name).collect())
    }

    async fn database_exists(&self, name: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<bool> {
        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;

        crate::database_exists(&mut ssh, name)
            .await
            .map_err(convert_error)
    }

    async fn create_user(&self, request: DatabaseUserRequest) -> PortResult<DatabaseUser> {
        let mut ssh = self.create_ssh_client(&request.host, &request.ssh_key_path).await?;

        // Create user with options
        crate::create_user_with_options(
            &mut ssh,
            &request.username,
            &request.password,
            request.superuser,
            false, // create_db
            false, // create_role
            Some(-1),    // connection_limit (-1 = unlimited)
        )
        .await
        .map_err(convert_error)?;

        Ok(DatabaseUser {
            username: request.username,
            superuser: request.superuser,
        })
    }

    async fn drop_user(&self, username: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;

        crate::drop_user(&mut ssh, username)
            .await
            .map_err(convert_error)?;

        Ok(())
    }

    async fn grant_privileges(&self, database: &str, username: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;

        // Grant all privileges on the database
        // Signature: (ssh, database, username, privileges)
        crate::grant_privileges(
            &mut ssh,
            database,
            username,
            &[Privilege::All],
        )
        .await
        .map_err(convert_error)?;

        Ok(())
    }
}