Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement rpc method in provisioner that allows for the deletion of resources #622

Merged
merged 3 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions proto/provisioner.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provisioner;

service Provisioner {
rpc ProvisionDatabase(DatabaseRequest) returns (DatabaseResponse);
rpc DeleteDatabase(DatabaseRequest) returns (DatabaseDeletionResponse);
}

message DatabaseRequest {
Expand All @@ -28,9 +29,7 @@ message AwsRds {
}
}

message RdsConfig {

}
message RdsConfig {}

message DatabaseResponse {
string username = 1;
Expand All @@ -41,3 +40,5 @@ message DatabaseResponse {
string address_public = 6;
string port = 7;
}

message DatabaseDeletionResponse {}
6 changes: 6 additions & 0 deletions provisioner/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
110 changes: 109 additions & 1 deletion provisioner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -315,6 +315,92 @@ impl MyProvisioner {
port: engine_to_port(engine),
})
}

async fn delete_shared_db(
&self,
project_name: &str,
engine: shared::Engine,
) -> Result<DatabaseDeletionResponse, Error> {
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<DatabaseDeletionResponse, Error> {
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]
Expand All @@ -340,6 +426,28 @@ impl Provisioner for MyProvisioner {

Ok(Response::new(reply))
}

#[tracing::instrument(skip(self))]
async fn delete_database(
&self,
request: Request<DatabaseRequest>,
) -> Result<Response<DatabaseDeletionResponse>, 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 {
Expand Down