diff --git a/Cargo.lock b/Cargo.lock index 98adf82..380497d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,6 +2018,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -3198,6 +3209,7 @@ dependencies = [ "reqwest", "secrecy", "serde", + "serde-aux", "sqlx", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 212bde5..f6636d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ chrono = "0.4.38" config = "0.14.0" secrecy = { version = "0.8.0", features = ["serde"] } serde = { version = "1.0.203", features = ["derive"] } +serde-aux = "4.5.0" tokio = {version = "1.37.0",features = ["macros", "rt-multi-thread"]} tracing = { version = "0.1.40", features = ["log"] } tracing-actix-web = "0.7.11" diff --git a/configuration/local.yaml b/configuration/local.yaml index c464c2f..8fd67fa 100644 --- a/configuration/local.yaml +++ b/configuration/local.yaml @@ -1,2 +1,4 @@ application: host: 127.0.0.1 +database: + require_ssl: false diff --git a/configuration/production.yaml b/configuration/production.yaml index b936a88..cd4608a 100644 --- a/configuration/production.yaml +++ b/configuration/production.yaml @@ -1,2 +1,4 @@ application: host: 0.0.0.0 +database: + require_ssl: true diff --git a/spec.yaml b/spec.yaml index 48998f0..0f1dfc1 100644 --- a/spec.yaml +++ b/spec.yaml @@ -1,39 +1,43 @@ -#! spec.yaml name: zero2prod -# Check https://www.digitalocean.com/docs/app-platform/#regional-availability -# for a list of all the available options. -# You can get region slugs from -# https://www.digitalocean.com/docs/platform/availability-matrix/ -# They must specified lowercased. -# `fra` stands for Frankfurt (Germany - EU) region: fra services: - name: zero2prod - # Relative to the repository root dockerfile_path: Dockerfile source_dir: . github: - # Depending on when you created the repository, - # the default branch on GitHub might have been named `master` branch: master - # Deploy a new version on every commit to `main`! - # Continuous Deployment, here we come! deploy_on_push: true - # !!! Fill in with your details - # e.g. LukeMathWalker/zero-to-production repo: josemoura212/zero2prod - # Active probe used by DigitalOcean's to ensure our application is healthy health_check: - # The path to our health check endpoint! - # It turned out to be useful in the end! http_path: /health_check - # The port the application will be listening on for incoming requests - # It should match what we specified in our configuration/production.yaml file! http_port: 8000 - # For production workloads we'd go for at least two! - # But let's try to keep the bill under control for now... instance_count: 1 instance_size_slug: basic-xxs - # All incoming requests should be routed to our app routes: - path: / + envs: + - key: APP_APPLICATION__BASE_URL + scope: RUN_TIME + value: ${APP_URL} + - key: APP_DATABASE__USERNAME + scope: RUN_TIME + value: ${newsletter.USERNAME} + - key: APP_DATABASE__PASSWORD + scope: RUN_TIME + value: ${newsletter.PASSWORD} + - key: APP_DATABASE__HOST + scope: RUN_TIME + value: ${newsletter.HOSTNAME} + - key: APP_DATABASE__PORT + scope: RUN_TIME + value: ${newsletter.PORT} + - key: APP_DATABASE__DATABASE_NAME + scope: RUN_TIME + value: ${newsletter.DATABASE} + +databases: + - engine: PG + name: newsletter + num_nodes: 1 + size: db-s-dev-database + version: "12" diff --git a/src/configuration.rs b/src/configuration.rs index 28a9734..39d2094 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,7 @@ use secrecy::{ExposeSecret, Secret}; +use serde_aux::field_attributes::deserialize_number_from_string; +use sqlx::postgres::{PgConnectOptions, PgSslMode}; +use sqlx::ConnectOptions; use std::convert::{TryFrom, TryInto}; #[derive(serde::Deserialize)] @@ -9,6 +12,7 @@ pub struct Settings { #[derive(serde::Deserialize)] pub struct ApplicationSettings { + #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, } @@ -17,30 +21,30 @@ pub struct ApplicationSettings { pub struct DatabaseSettings { pub username: String, pub password: Secret, + #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub database_name: String, + pub require_ssl: bool, } impl DatabaseSettings { - pub fn connection_string(&self) -> String { - format!( - "postgres://{}:{}@{}:{}/{}", - self.username, - self.password.expose_secret(), - self.host, - self.port, - self.database_name - ) + pub fn without_db(&self) -> PgConnectOptions { + let ssl_mode = if self.require_ssl { + PgSslMode::Require + } else { + PgSslMode::Prefer + }; + PgConnectOptions::new() + .host(&self.host) + .username(&self.username) + .password(self.password.expose_secret()) + .port(self.port) + .ssl_mode(ssl_mode) } - pub fn connection_string_without_db(&self) -> String { - format!( - "postgres://{}:{}@{}:{}", - self.username, - self.password.expose_secret(), - self.host, - self.port - ) + pub fn with_db(&self) -> PgConnectOptions { + let options = self.without_db().database(&self.database_name); + options.log_statements(tracing::log::LevelFilter::Trace) } } @@ -78,6 +82,8 @@ pub fn get_configuration() -> Result { config::File::from(configuration_directory.join(environment.as_str())).required(true), ); + settings = settings.add_source(config::Environment::with_prefix("app").separator("__")); + let settings = settings.build()?; settings.try_into() diff --git a/src/main.rs b/src/main.rs index 82af595..6f7247e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; use std::net::TcpListener; use zero2prod::configuration::get_configuration; use zero2prod::startup::run; @@ -11,13 +11,11 @@ async fn main() -> std::io::Result<()> { let configuration = get_configuration().expect("Failed to read configuration."); - let connection_pool = PgPool::connect_lazy(&configuration.database.connection_string()) - .expect("Failed to connect to Postgres."); + //todo - Old way of connecting to Postgres + // let connection_pool = PgPool::connect_lazy(&configuration.database.with_db()) + // .expect("Failed to connect to Postgres."); - //todo - implements error - // let connection_pool = PgPoolOptions::new() - // .connect_timeout(std::time::Duration::from_secs(2)) - // .connect_lazy(&configuration.database.connection_string()); + let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration.database.with_db()); let address = format!( "{}:{}", diff --git a/tests/health_check.rs b/tests/health_check.rs index c80805c..adbb02e 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -53,18 +53,20 @@ async fn spawn_app() -> TestApp { pub async fn configure_database(config: &DatabaseSettings) -> PgPool { // Create database - let mut connection = PgConnection::connect(&config.connection_string_without_db()) + let mut connection = PgConnection::connect_with(&config.without_db()) .await .expect("Failed to connect to Postgres"); + connection .execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str()) .await .expect("Failed to create database."); // Migrate database - let connection_pool = PgPool::connect(&config.connection_string()) + let connection_pool = PgPool::connect_with(config.with_db()) .await .expect("Failed to connect to Postgres."); + sqlx::migrate!("./migrations") .run(&connection_pool) .await