diff --git a/proto/provisioner.proto b/proto/provisioner.proto index ab67da88c..66d166e98 100644 --- a/proto/provisioner.proto +++ b/proto/provisioner.proto @@ -3,6 +3,7 @@ package provisioner; service Provisioner { rpc ProvisionDatabase(DatabaseRequest) returns (DatabaseResponse); + rpc DeleteDatabase(DatabaseRequest) returns (DatabaseDeletionResponse); } message DatabaseRequest { @@ -28,9 +29,7 @@ message AwsRds { } } -message RdsConfig { - -} +message RdsConfig {} message DatabaseResponse { string username = 1; @@ -41,3 +40,5 @@ message DatabaseResponse { string address_public = 6; string port = 7; } + +message DatabaseDeletionResponse {} diff --git a/provisioner/src/error.rs b/provisioner/src/error.rs index 21bac6bc1..e8b522c0c 100644 --- a/provisioner/src/error.rs +++ b/provisioner/src/error.rs @@ -14,9 +14,15 @@ pub enum Error { #[error("failed to update role: {0}")] UpdateRole(String), + #[error("failed to drop role: {0}")] + DeleteRole(String), + #[error("failed to create DB: {0}")] CreateDB(String), + #[error("failed to drop DB: {0}")] + DeleteDB(String), + #[error("unexpected sqlx error: {0}")] UnexpectedSqlx(#[from] sqlx::Error), diff --git a/provisioner/src/lib.rs b/provisioner/src/lib.rs index 1d82cb4b9..9a57c3e8f 100644 --- a/provisioner/src/lib.rs +++ b/provisioner/src/lib.rs @@ -6,11 +6,11 @@ use aws_sdk_rds::{error::ModifyDBInstanceErrorKind, model::DbInstance, types::Sd pub use error::Error; use mongodb::{bson::doc, options::ClientOptions}; use rand::Rng; -use shuttle_proto::provisioner::provisioner_server::Provisioner; pub use shuttle_proto::provisioner::provisioner_server::ProvisionerServer; use shuttle_proto::provisioner::{ aws_rds, database_request::DbType, shared, AwsRds, DatabaseRequest, DatabaseResponse, Shared, }; +use shuttle_proto::provisioner::{provisioner_server::Provisioner, DatabaseDeletionResponse}; use sqlx::{postgres::PgPoolOptions, ConnectOptions, Executor, PgPool}; use tokio::time::sleep; use tonic::{Request, Response, Status}; @@ -315,6 +315,92 @@ impl MyProvisioner { port: engine_to_port(engine), }) } + + async fn delete_shared_db( + &self, + project_name: &str, + engine: shared::Engine, + ) -> Result { + match engine { + shared::Engine::Postgres(_) => self.delete_pg(project_name).await?, + shared::Engine::Mongodb(_) => self.delete_mongodb(project_name).await?, + } + Ok(DatabaseDeletionResponse {}) + } + + async fn delete_pg(&self, project_name: &str) -> Result<(), Error> { + let database_name = format!("db-{project_name}"); + let role_name = format!("user-{project_name}"); + + // Idenfitiers cannot be used as query parameters + let drop_db_query = format!("DROP DATABASE \"{database_name}\";"); + + // Drop the database. Note that this can fail if there are still active connections to it + sqlx::query(&drop_db_query) + .execute(&self.pool) + .await + .map_err(|e| Error::DeleteRole(e.to_string()))?; + + // Drop the role + let drop_role_query = format!("DROP ROLE IF EXISTS \"{role_name}\""); + sqlx::query(&drop_role_query) + .execute(&self.pool) + .await + .map_err(|e| Error::DeleteDB(e.to_string()))?; + + Ok(()) + } + + async fn delete_mongodb(&self, project_name: &str) -> Result<(), Error> { + let database_name = format!("mongodb-{project_name}"); + let db = self.mongodb_client.database(&database_name); + + // dropping a database in mongodb doesn't delete any associated users + // so do that first + + let drop_users_command = doc! { + "dropAllUsersFromDatabase": 1 + }; + + db.run_command(drop_users_command, None) + .await + .map_err(|e| Error::DeleteRole(e.to_string()))?; + + // drop the actual database + + db.drop(None) + .await + .map_err(|e| Error::DeleteDB(e.to_string()))?; + + Ok(()) + } + + async fn delete_aws_rds( + &self, + project_name: &str, + engine: aws_rds::Engine, + ) -> Result { + let client = &self.rds_client; + let instance_name = format!("{project_name}-{engine}"); + + // try to delete the db instance + let delete_result = client + .delete_db_instance() + .db_instance_identifier(&instance_name) + .send() + .await; + + // Did we get an error that wasn't "db instance not found" + if let Err(SdkError::ServiceError { err, .. }) = delete_result { + if !err.is_db_instance_not_found_fault() { + return Err(Error::Plain(format!( + "got unexpected error from AWS RDS service: {err}" + ))); + } + } + + Ok(DatabaseDeletionResponse {}) + } } #[tonic::async_trait] @@ -340,6 +426,28 @@ impl Provisioner for MyProvisioner { Ok(Response::new(reply)) } + + #[tracing::instrument(skip(self))] + async fn delete_database( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let db_type = request.db_type.unwrap(); + + let reply = match db_type { + DbType::Shared(Shared { engine }) => { + self.delete_shared_db(&request.project_name, engine.expect("oneof to be set")) + .await? + } + DbType::AwsRds(AwsRds { engine }) => { + self.delete_aws_rds(&request.project_name, engine.expect("oneof to be set")) + .await? + } + }; + + Ok(Response::new(reply)) + } } fn generate_password() -> String {