Skip to content

Commit

Permalink
feat: AWS RDS (#180)
Browse files Browse the repository at this point in the history
* feat: simple RDS support

* refactor: cleanup and prep

* feat: Simple AWS RDS Postgres tuple

* feat: MySql and MariaDB support

* refactor: add engine to DB info

* refactor: 'master' for username

* refactor: clippy suggestions

* refactor: better debug logs

* refactor: set region

* refactor: increase tracing

* refactor: try more timeouts

* refactor: try manual profile

* refactor: try manual imds provider

* refactor: try manual client

* refactor: set read timeout

* refactor: try endpoint override

* refactor: trying aws update

* refactor: finally found it

* refactor: remove region

* bug: increase hop limit

* feat: add policy to handle RDS

* refactor: try subnet group

* refactor: more permission fixes

* feat: add rds subnet

* refactor: cleanup

* feat: switch to attribute annotation

* refactor: move to aws::rds

* feat: aws::rds example

* refactor: move to shared::Postgres

* refactor: improve rds waiting

* feat: open acl ports for acl

* bug: running tokio inside tide (async-std)

* docs: service configuration using attributes

* bug: fix uuid/v4 in test

* refactor: clippy suggestions

* tests: update sqlx tests

* refactor: make attribute config required

* refactor: make transition error better

* tests: trybuild

* refactor: better hints

* refactor: builders vec

* refactor: update examples to main

* refactor: make tf use account id

* feat: add AWS RDS to proto

* feat: local runs for AWS RDS

* refactor: sort Cargo.toml

* tests: provisioner await

* refactor: undo secrets patch

* refactor: fix pg_isready

* refactor: args for public and private PG addresses

* refactor: clippy suggestions

* bug: supervisord args

* bug: supervisord args v2

* fix: test helper

* bug: security group

* refactor: tide postgres version

* refactor: more helper fixes

* bug: INTERNAL_ADDRESS

* refactor: remove redundant security group link

* bug: PG timeout

* Apply suggestions from code review

Co-authored-by: Max <42641081+bmoxb@users.noreply.github.com>

Co-authored-by: Max <42641081+bmoxb@users.noreply.github.com>
  • Loading branch information
chesedo and bmoxb authored Jun 20, 2022
1 parent ada23de commit 6a11b03
Show file tree
Hide file tree
Showing 45 changed files with 1,703 additions and 386 deletions.
959 changes: 766 additions & 193 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions api/src/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ impl Deployment {

let mut factory = ShuttleFactory::new(
context.provisioner_client.clone(),
context.provisioner_address.clone(),
meta.project.clone(),
);
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
Expand Down Expand Up @@ -289,7 +288,6 @@ pub(crate) struct DeploymentSystem {
job_queue: JobQueue,
router: Arc<Router>,
fqdn: String,
pub(crate) provisioner_address: String,
}

const JOB_QUEUE_SIZE: usize = 200;
Expand Down Expand Up @@ -340,7 +338,6 @@ pub(crate) struct Context {
build_system: Box<dyn BuildSystem>,
deployments: Arc<RwLock<Deployments>>,
provisioner_client: ProvisionerClient<Channel>,
provisioner_address: String,
}

impl DeploymentSystem {
Expand Down Expand Up @@ -384,7 +381,6 @@ impl DeploymentSystem {
build_system,
deployments: deployments.clone(),
provisioner_client,
provisioner_address: provisioner_address.clone(),
};

let job_queue = JobQueue::new(context, tx).await;
Expand All @@ -400,7 +396,6 @@ impl DeploymentSystem {
job_queue,
router,
fqdn,
provisioner_address,
}
}

Expand Down
21 changes: 13 additions & 8 deletions api/src/factory.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
use async_trait::async_trait;
use proto::provisioner::{provisioner_client::ProvisionerClient, DatabaseRequest};
use proto::provisioner::{
database_request::DbType, provisioner_client::ProvisionerClient, DatabaseRequest,
};
use shuttle_common::{project::ProjectName, DatabaseReadyInfo};
use shuttle_service::Factory;
use shuttle_service::{database::Type, Factory};
use tonic::{transport::Channel, Request};

pub(crate) struct ShuttleFactory {
project_name: ProjectName,
provisioner_client: ProvisionerClient<Channel>,
provisioner_address: String,
info: Option<DatabaseReadyInfo>,
}

impl ShuttleFactory {
pub(crate) fn new(
provisioner_client: ProvisionerClient<Channel>,
provisioner_address: String,
project_name: ProjectName,
) -> Self {
Self {
provisioner_client,
provisioner_address,
project_name,
info: None,
}
Expand All @@ -32,13 +31,19 @@ impl ShuttleFactory {

#[async_trait]
impl Factory for ShuttleFactory {
async fn get_sql_connection_string(&mut self) -> Result<String, shuttle_service::Error> {
async fn get_sql_connection_string(
&mut self,
db_type: Type,
) -> Result<String, shuttle_service::Error> {
if let Some(ref info) = self.info {
return Ok(info.connection_string(&self.provisioner_address));
return Ok(info.connection_string_private());
}

let db_type: DbType = db_type.into();

let request = Request::new(DatabaseRequest {
project_name: self.project_name.to_string(),
db_type: Some(db_type),
});

let response = self
Expand All @@ -49,7 +54,7 @@ impl Factory for ShuttleFactory {
.into_inner();

let info: DatabaseReadyInfo = response.into();
let conn_str = info.connection_string(&self.provisioner_address);
let conn_str = info.connection_string_private();
self.info = Some(info);

debug!("giving a sql connection string: {}", conn_str);
Expand Down
3 changes: 1 addition & 2 deletions api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,7 @@ async fn project_secrets(
.await?;

if let Some(database_deployment) = &deployment.database_deployment {
let conn_str =
database_deployment.connection_string(&state.deployment_manager.provisioner_address);
let conn_str = database_deployment.connection_string_private();
let conn = sqlx::PgPool::connect(&conn_str)
.await
.map_err(|e| DeploymentApiError::Internal(e.to_string()))?;
Expand Down
1 change: 1 addition & 0 deletions api/users.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ projects = [
'authentication-rocket-app',
'hello-world-tide-app',
'hello-world-tower-app',
'postgres-tide-app',
]
163 changes: 134 additions & 29 deletions cargo-shuttle/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ use bollard::{
};
use colored::Colorize;
use crossterm::{
cursor::MoveUp,
cursor::{MoveDown, MoveUp},
terminal::{Clear, ClearType},
QueueableCommand,
};
use futures::StreamExt;
use portpicker::pick_unused_port;
use shuttle_common::{project::ProjectName, DatabaseReadyInfo};
use shuttle_service::{error::CustomError, Factory};
use shuttle_common::{database::AwsRdsEngine, project::ProjectName, DatabaseReadyInfo};
use shuttle_service::{database::Type, error::CustomError, Factory};
use std::{collections::HashMap, io::stdout, time::Duration};
use tokio::time::sleep;

Expand All @@ -34,14 +34,26 @@ impl LocalFactory {
}
}

const PG_PASSWORD: &str = "password";
const PG_IMAGE: &str = "postgres:11";

#[async_trait]
impl Factory for LocalFactory {
async fn get_sql_connection_string(&mut self) -> Result<String, shuttle_service::Error> {
async fn get_sql_connection_string(
&mut self,
db_type: Type,
) -> Result<String, shuttle_service::Error> {
trace!("getting sql string for project '{}'", self.project);
let container_name = format!("shuttle_{}_postgres", self.project);

let EngineConfig {
r#type,
image,
engine,
username,
password,
database_name,
port,
env,
is_ready_cmd,
} = db_type_to_config(db_type);
let container_name = format!("shuttle_{}_{}", self.project, r#type);

let container = match self.docker.inspect_container(&container_name, None).await {
Ok(container) => {
Expand All @@ -51,17 +63,15 @@ impl Factory for LocalFactory {
Err(bollard::errors::Error::DockerResponseServerError { status_code, .. })
if status_code == 404 =>
{
self.pull_image(PG_IMAGE)
.await
.expect("failed to pull image");
self.pull_image(&image).await.expect("failed to pull image");
trace!("will create DB container {container_name}");
let options = Some(CreateContainerOptions {
name: container_name.clone(),
});
let mut port_bindings = HashMap::new();
let host_port = pick_unused_port().expect("system to have a free port");
port_bindings.insert(
"5432/tcp".to_string(),
port.clone(),
Some(vec![PortBinding {
host_port: Some(host_port.to_string()),
..Default::default()
Expand All @@ -72,10 +82,9 @@ impl Factory for LocalFactory {
..Default::default()
};

let password_env = format!("POSTGRES_PASSWORD={PG_PASSWORD}");
let config = Config {
image: Some(PG_IMAGE),
env: Some(vec![&password_env]),
image: Some(image),
env,
host_config: Some(host_config),
..Default::default()
};
Expand All @@ -101,10 +110,10 @@ impl Factory for LocalFactory {
.expect("container to have host config")
.port_bindings
.expect("port bindings on container")
.get("5432/tcp")
.expect("a '5432/tcp' port bindings entry")
.get(&port)
.expect("a port bindings entry")
.as_ref()
.expect("a '5432/tcp' port bindings")
.expect("a port bindings")
.first()
.expect("at least one port binding")
.host_port
Expand All @@ -125,16 +134,19 @@ impl Factory for LocalFactory {
.expect("failed to start none running container");
}

self.wait_for_ready(&container_name).await?;
self.wait_for_ready(&container_name, is_ready_cmd).await?;

let db_info = DatabaseReadyInfo::new(
"postgres".to_string(),
PG_PASSWORD.to_string(),
"postgres".to_string(),
engine,
username,
password,
database_name,
port,
"localhost".to_string(),
"localhost".to_string(),
);

let conn_str = db_info.connection_string("localhost");
let conn_str = db_info.connection_string_private();

println!(
"{:>12} can be reached at {}\n",
Expand All @@ -147,13 +159,18 @@ impl Factory for LocalFactory {
}

impl LocalFactory {
async fn wait_for_ready(&self, container_name: &str) -> Result<(), shuttle_service::Error> {
async fn wait_for_ready(
&self,
container_name: &str,
is_ready_cmd: Vec<String>,
) -> Result<(), shuttle_service::Error> {
loop {
trace!("waiting for '{container_name}' to be ready for connections");

let config = CreateExecOptions {
cmd: Some(vec!["pg_isready"]),
cmd: Some(is_ready_cmd.clone()),
attach_stdout: Some(true),
attach_stderr: Some(true),
..Default::default()
};

Expand All @@ -171,12 +188,12 @@ impl LocalFactory {

if let bollard::exec::StartExecResults::Attached { mut output, .. } = ready_result {
while let Some(line) = output.next().await {
if let bollard::container::LogOutput::StdOut { message } =
trace!("line: {:?}", line);

if let bollard::container::LogOutput::StdOut { .. } =
line.expect("output to have a log line")
{
if message.ends_with(b"accepting connections\n") {
return Ok(());
}
return Ok(());
}
}
}
Expand Down Expand Up @@ -213,6 +230,13 @@ impl LocalFactory {
print_layers(&layers);
}

// Undo last MoveUps
stdout()
.queue(MoveDown(
layers.len().try_into().expect("to convert usize to u16"),
))
.expect("to reset cursor position");

Ok(())
}
}
Expand Down Expand Up @@ -254,3 +278,84 @@ fn print_layers(layers: &Vec<CreateImageInfo>) {
))
.expect("to reset cursor position");
}

struct EngineConfig {
r#type: String,
image: String,
engine: String,
username: String,
password: String,
database_name: String,
port: String,
env: Option<Vec<String>>,
is_ready_cmd: Vec<String>,
}

fn db_type_to_config(db_type: Type) -> EngineConfig {
match db_type {
Type::Shared => EngineConfig {
r#type: "shared_postgres".to_string(),
image: "postgres:11".to_string(),
engine: "postgres".to_string(),
username: "postgres".to_string(),
password: "postgres".to_string(),
database_name: "postgres".to_string(),
port: "5432/tcp".to_string(),
env: Some(vec!["POSTGRES_PASSWORD=postgres".to_string()]),
is_ready_cmd: vec![
"/bin/sh".to_string(),
"-c".to_string(),
"pg_isready | grep 'accepting connections'".to_string(),
],
},
Type::AwsRds(AwsRdsEngine::Postgres) => EngineConfig {
r#type: "aws_rds_postgres".to_string(),
image: "postgres:13.4".to_string(),
engine: "postgres".to_string(),
username: "postgres".to_string(),
password: "postgres".to_string(),
database_name: "postgres".to_string(),
port: "5432/tcp".to_string(),
env: Some(vec!["POSTGRES_PASSWORD=postgres".to_string()]),
is_ready_cmd: vec![
"/bin/sh".to_string(),
"-c".to_string(),
"pg_isready | grep 'accepting connections'".to_string(),
],
},
Type::AwsRds(AwsRdsEngine::MariaDB) => EngineConfig {
r#type: "aws_rds_mariadb".to_string(),
image: "mariadb:10.6.7".to_string(),
engine: "mariadb".to_string(),
username: "root".to_string(),
password: "mariadb".to_string(),
database_name: "mysql".to_string(),
port: "3306/tcp".to_string(),
env: Some(vec!["MARIADB_ROOT_PASSWORD=mariadb".to_string()]),
is_ready_cmd: vec![
"mysql".to_string(),
"-pmariadb".to_string(),
"--silent".to_string(),
"-e".to_string(),
"show databases;".to_string(),
],
},
Type::AwsRds(AwsRdsEngine::MySql) => EngineConfig {
r#type: "aws_rds_mysql".to_string(),
image: "mysql:8.0.28".to_string(),
engine: "mysql".to_string(),
username: "root".to_string(),
password: "mysql".to_string(),
database_name: "mysql".to_string(),
port: "3306/tcp".to_string(),
env: Some(vec!["MYSQL_ROOT_PASSWORD=mysql".to_string()]),
is_ready_cmd: vec![
"mysql".to_string(),
"-pmysql".to_string(),
"--silent".to_string(),
"-e".to_string(),
"show databases;".to_string(),
],
},
}
}
4 changes: 3 additions & 1 deletion codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ description = "Proc-macro code generator for the shuttle.rs service"
proc-macro = true

[dependencies]
proc-macro-error = "1.0"
proc-macro2 = "1.0.39"
quote = "1.0.18"
syn = { version = "1.0.96", features = ["full"] }
syn = { version = "1.0.96", features = ["full", "extra-traits"] }

[dev-dependencies]
pretty_assertions = "1.2.1"
trybuild = "1.0"
Loading

0 comments on commit 6a11b03

Please sign in to comment.