diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 808f999..9108647 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,6 +29,8 @@ jobs: run: sudo apt-get update && sudo apt-get install -y postgresql-client - name: Migrate database run: SKIP_DOCKER=true ./scripts/init_db + - name: Check sqlx-data.json is up to date + run: cargo sqlx prepare --workspace --check - name: Run tests run: cargo test fmt: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d25727 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM rust:1.77.0 +WORKDIR /app +RUN apt-get update && apt-get install lld clang -y +COPY . . +ENV SQLX_OFFLINE true +RUN cargo build --release +ENV APP_ENVIRONMENT production +ENTRYPOINT ["./target/release/zero2prod"] diff --git a/configuration.toml b/config/base.toml similarity index 81% rename from configuration.toml rename to config/base.toml index 5830bbe..dfb02df 100644 --- a/configuration.toml +++ b/config/base.toml @@ -1,4 +1,5 @@ -application_port = 8000 +[application] +port = 8000 [database] host = "127.0.0.1" diff --git a/config/local.toml b/config/local.toml new file mode 100644 index 0000000..8ef25b1 --- /dev/null +++ b/config/local.toml @@ -0,0 +1,2 @@ +[application] +host = "127.0.0.1" diff --git a/config/production.toml b/config/production.toml new file mode 100644 index 0000000..d0de4b1 --- /dev/null +++ b/config/production.toml @@ -0,0 +1,2 @@ +[application] +host = "0.0.0.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..30526ff --- /dev/null +++ b/src/config.rs @@ -0,0 +1,93 @@ +use std::fmt::Display; + +use secrecy::{ExposeSecret, Secret}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Settings { + pub database: DatabaseSettings, + pub application: ApplicationSettings, +} + +#[derive(Debug, Deserialize)] +pub struct DatabaseSettings { + pub username: String, + pub password: Secret, + pub port: u16, + pub host: String, + pub database_name: String, +} +#[derive(Debug, Deserialize)] +pub struct ApplicationSettings { + pub port: u16, + pub host: String, +} + +impl DatabaseSettings { + pub fn to_string_no_db(&self) -> Secret { + Secret::new(format!( + "postgres://{}:{}@{}:{}", + self.username, + self.password.expose_secret(), + self.host, + self.port + )) + } + pub fn to_string(&self) -> Secret { + Secret::new(format!( + "postgres://{}:{}@{}:{}/{}", + self.username, + self.password.expose_secret(), + self.host, + self.port, + self.database_name + )) + } +} + +pub fn get_config() -> Result { + let base_path = std::env::current_dir().expect("Failed to determine current directory"); + let config_directory = base_path.join("config"); + let env: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "local".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT"); + + let env_filename = format!("{}.toml", &env); + + let settings = config::Config::builder() + .add_source(config::File::from(config_directory.join("base.toml"))) + .add_source(config::File::from(config_directory.join(env_filename))) + .build()?; + settings.try_deserialize::() +} + +#[derive(Debug)] +pub enum Environment { + Local, + Production, +} + +impl Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Environment::Local => write!(f, "local"), + Environment::Production => write!(f, "production"), + } + } +} + +impl TryFrom for Environment { + type Error = String; + fn try_from(value: String) -> Result { + match value.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{} is not supported environment. \ + Use either `local` or `production`.", + other + )), + } + } +} diff --git a/src/configuation.rs b/src/configuation.rs deleted file mode 100644 index 0010c41..0000000 --- a/src/configuation.rs +++ /dev/null @@ -1,49 +0,0 @@ -use secrecy::{ExposeSecret, Secret}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Settings { - pub database: DatabaseSettings, - pub application_port: u16, -} - -#[derive(Debug, Deserialize)] -pub struct DatabaseSettings { - pub username: String, - pub password: Secret, - pub port: u16, - pub host: String, - pub database_name: String, -} - -impl DatabaseSettings { - pub fn to_string_no_db(&self) -> Secret { - Secret::new(format!( - "postgres://{}:{}@{}:{}", - self.username, - self.password.expose_secret(), - self.host, - self.port - )) - } - pub fn to_string(&self) -> Secret { - Secret::new(format!( - "postgres://{}:{}@{}:{}/{}", - self.username, - self.password.expose_secret(), - self.host, - self.port, - self.database_name - )) - } -} - -pub fn get_configuration() -> Result { - let settings = config::Config::builder() - .add_source(config::File::new( - "configuration.toml", - config::FileFormat::Toml, - )) - .build()?; - settings.try_deserialize::() -} diff --git a/src/lib.rs b/src/lib.rs index fc9c78d..0276897 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ -pub mod configuation; +pub mod config; pub mod routes; pub mod telemetry; diff --git a/src/main.rs b/src/main.rs index 0f0e5fe..edf654e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,8 @@ -use std::net::SocketAddr; - use secrecy::ExposeSecret; use sqlx::postgres::PgPoolOptions; use tokio::net::TcpListener; use zero2prod::{ - configuation::get_configuration, + config::get_config, routes::route, telemetry::{get_subscriber, init_subscriber}, }; @@ -13,12 +11,11 @@ use zero2prod::{ async fn main() -> Result<(), std::io::Error> { let subscriber = get_subscriber("zero2prod", "info", std::io::stdout); init_subscriber(subscriber); - let configuation = get_configuration().expect("Failed to read configuation."); + let config = get_config().expect("Failed to read configuation."); let pool = PgPoolOptions::new() - .connect(&configuation.database.to_string().expose_secret()) - .await - .expect("Failed to connect to Postgres."); - let addr = SocketAddr::from(([127, 0, 0, 1], configuation.application_port)); + .connect_lazy(&config.database.to_string().expose_secret()) + .expect("Failed to create Postgres connection pool."); + let addr = format!("{}:{}", config.application.host, config.application.port); let listener = TcpListener::bind(addr) .await .expect("Failed to bind port 8000."); diff --git a/tests/health_check.rs b/tests/health_check.rs index 0508b81..37dd84c 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -5,7 +5,7 @@ use sqlx::{postgres::PgPoolOptions, Connection, Executor, PgConnection, PgPool}; use tokio::net::TcpListener; use uuid::Uuid; use zero2prod::{ - configuation::{get_configuration, DatabaseSettings}, + config::{get_config, DatabaseSettings}, routes::route, telemetry::{get_subscriber, init_subscriber}, }; @@ -28,8 +28,8 @@ async fn health_check() { #[tokio::test] async fn subscribe_returns_200_for_valid_form_data() { let app = spawn_app().await; - let configuration = get_configuration().expect("Failed to read configuration."); - let mut connection = PgConnection::connect(&configuration.database.to_string().expose_secret()) + let config = get_config().expect("Failed to read configuration."); + let mut connection = PgConnection::connect(&config.database.to_string().expose_secret()) .await .expect("Failed to connect to Postgres."); let client = Client::new(); @@ -106,7 +106,7 @@ async fn spawn_app() -> TestApp { .expect("Failed to bind random port"); let port = listener.local_addr().unwrap().port(); let address = format!("http://127.0.0.1:{}", port); - let mut config = get_configuration().expect("Failed to read configuration."); + let mut config = get_config().expect("Failed to read configuration."); config.database.database_name = Uuid::new_v4().to_string(); let pool = configure_database(&config.database).await;