From 9dde52c1cdc8feb89d7bf21207f0e9628207c98e Mon Sep 17 00:00:00 2001 From: Sandro Eiler Date: Wed, 7 Feb 2024 12:07:03 +0100 Subject: [PATCH] build: make project deployable via docker/podman --- ...b978748046beff031d153699b351089c3bf9b.json | 17 +++ Dockerfile | 102 ++---------------- configuration.yaml => configuration/base.yaml | 3 +- configuration/local.yaml | 2 + configuration/production.yaml | 2 + src/configuration.rs | 70 ++++++++++-- src/main.rs | 15 +-- 7 files changed, 102 insertions(+), 109 deletions(-) create mode 100644 .sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json rename configuration.yaml => configuration/base.yaml (80%) create mode 100644 configuration/local.yaml create mode 100644 configuration/production.yaml diff --git a/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json b/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json new file mode 100644 index 0000000..e3d16e3 --- /dev/null +++ b/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO subscriptions (id, email, name, subscribed_at)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b" +} diff --git a/Dockerfile b/Dockerfile index 06d3a8b..31f71e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,94 +1,8 @@ -# Global ARGs - -ARG WORKDIR_ROOT=/usr/src - -ARG PROJECT_NAME=learn_axum - -ARG BUILD_TARGET=x86_64-unknown-linux-musl - -ARG BUILD_MODE=release - -ARG BUILD_BIN=${PROJECT_NAME} - -############################################################################### - -FROM clux/muslrust:stable AS build - -# Import global ARGs -ARG WORKDIR_ROOT -ARG PROJECT_NAME -ARG BUILD_TARGET -ARG BUILD_MODE -ARG BUILD_BIN - -WORKDIR ${WORKDIR_ROOT} - -# Docker build cache: Create and build an empty dummy project with all -# external dependencies to avoid redownloading them on subsequent builds -# if unchanged. -RUN USER=root \ - cargo new --bin ${PROJECT_NAME} -WORKDIR ${WORKDIR_ROOT}/${PROJECT_NAME} - -COPY [ \ - "Cargo.toml", \ - "Cargo.lock", \ - "./" ] - -# Build the dummy project(s), then delete all build artefacts that must(!) not be cached -# RUN cargo build --${BUILD_MODE} --target ${BUILD_TARGET} -#\ -# && \ -# rm -f ./target/${BUILD_TARGET}/${BUILD_MODE}/${PROJECT_NAME}* \ -# && \ -# rm -f ./target/${BUILD_TARGET}/${BUILD_MODE}/deps/${PROJECT_NAME}-* \ -# && \ -# rm -rf ./target/${BUILD_TARGET}/${BUILD_MODE}/.fingerprint/${PROJECT_NAME}-* - -# Copy all project (re-)sources that are required for building (ordered alphabetically) -COPY [ "migrations", "./migrations/" ] -COPY [ "src", "./src/" ] - -# Test and build the actual project -RUN cargo test --${BUILD_MODE} --target ${BUILD_TARGET} --workspace \ - && \ - cargo build --${BUILD_MODE} --target ${BUILD_TARGET} --bin ${BUILD_BIN} \ - && \ - strip ./target/${BUILD_TARGET}/${BUILD_MODE}/${BUILD_BIN} - -# Switch back to the root directory -# -# NOTE(2019-08-30, uklotzde): Otherwise copying from the build image fails -# during all subsequent builds of the 2nd stage with an unchanged 1st stage -# image. Tested with podman 1.5.x on Fedora 30. -WORKDIR / - - -############################################################################### -# 2nd Build Stage -FROM scratch - -# Import global ARGs -ARG WORKDIR_ROOT -ARG PROJECT_NAME -ARG BUILD_TARGET -ARG BUILD_MODE -ARG BUILD_BIN - -ARG DATA_VOLUME="/volume" - -ARG EXPOSE_PORT=8080 - -# Copy the statically-linked executable into the minimal scratch image -COPY --from=build [ \ - "${WORKDIR_ROOT}/${PROJECT_NAME}/target/${BUILD_TARGET}/${BUILD_MODE}/${BUILD_BIN}", \ - "./entrypoint" ] - -EXPOSE ${EXPOSE_PORT} - -VOLUME [ ${DATA_VOLUME} ] - -# Bind the exposed port to Rocket that is used as the web framework -ENV SERVER_PORT ${EXPOSE_PORT} - -ENTRYPOINT [ "./entrypoint" ] +FROM docker.io/rust:1.75.0 +WORKDIR /app +RUN apt update && apt install lld clang -y +COPY . . +ENV SQLX_OFFLINE true +RUN cargo build --release +ENV APP_ENVIRONMENT production +ENTRYPOINT ["./target/release/learn_axum"] diff --git a/configuration.yaml b/configuration/base.yaml similarity index 80% rename from configuration.yaml rename to configuration/base.yaml index ef95aab..2a531b3 100644 --- a/configuration.yaml +++ b/configuration/base.yaml @@ -1,4 +1,5 @@ -application_port: 8000 +application: + port: 8000 database: host: "127.0.0.1" port: 5432 diff --git a/configuration/local.yaml b/configuration/local.yaml new file mode 100644 index 0000000..c464c2f --- /dev/null +++ b/configuration/local.yaml @@ -0,0 +1,2 @@ +application: + host: 127.0.0.1 diff --git a/configuration/production.yaml b/configuration/production.yaml new file mode 100644 index 0000000..b936a88 --- /dev/null +++ b/configuration/production.yaml @@ -0,0 +1,2 @@ +application: + host: 0.0.0.0 diff --git a/src/configuration.rs b/src/configuration.rs index 9c4d6b6..4c7c463 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,17 +1,27 @@ use secrecy::{ExposeSecret, Secret}; #[derive(serde::Deserialize)] -/// The application's settings +/// The setting collection. /// /// * `database`: database settings -/// * `application_port`: the port the app is running on +/// * `application`: application settings pub struct Settings { pub database: DatabaseSettings, - pub application_port: u16, + pub application: ApplicationSettings, } #[derive(serde::Deserialize)] -/// The database settings +/// The application settings. +/// +/// * `port`: The port to listen on +/// * `host`: The host address to listen on +pub struct ApplicationSettings { + pub port: u16, + pub host: String, +} + +#[derive(serde::Deserialize)] +/// The database settings. /// /// * `username`: the DB username /// * `password`: the DB pasword @@ -28,14 +38,58 @@ pub struct DatabaseSettings { pub require_ssl: bool, } -/// Provides the application settings +/// The possible runtime environment for our application. +pub enum Environment { + Local, + Production, +} + +impl Environment { + pub fn as_str(&self) -> &'static str { + match self { + Environment::Local => "local", + Environment::Production => "production", + } + } +} + +impl TryFrom for Environment { + type Error = String; + + fn try_from(s: String) -> Result { + match s.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{} is not a supported environment. \ + Use either `local` or `production`.", + other + )), + } + } +} + +/// Provides the application settings. pub fn get_configuration() -> Result { + let base_path = std::env::current_dir().expect("Failed to determine the current directory"); + let configuration_directory = base_path.join("configuration"); + + // Detect the running environment. + // Default to `local` if unspecified. + let environment: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "local".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT."); + let environment_filename = format!("{}.yaml", environment.as_str()); let settings = config::Config::builder() - .add_source(config::File::new( - "configuration.yaml", - config::FileFormat::Yaml, + .add_source(config::File::from( + configuration_directory.join("base.yaml"), + )) + .add_source(config::File::from( + configuration_directory.join(environment_filename), )) .build()?; + settings.try_deserialize::() } diff --git a/src/main.rs b/src/main.rs index 2ad4ada..5cc7ded 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use learn_axum::configuration::get_configuration; use learn_axum::startup; use learn_axum::telemetry::{get_subscriber, init_subscriber}; use secrecy::ExposeSecret; -use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; use tokio::net::TcpListener; #[tokio::main] @@ -13,11 +13,14 @@ async fn main() { init_subscriber(subscriber); let configuration = get_configuration().expect("Failed to read configuration."); - let addr = format!("127.0.0.1:{}", configuration.application_port); + let addr = format!( + "{}:{}", + configuration.application.host, configuration.application.port + ); let listener = TcpListener::bind(addr).await.unwrap(); //.expect("Unable to bind to port"); - let connection_pool = - PgPool::connect(configuration.database.connection_string().expose_secret()) - .await - .expect("Failed to connect to Postgres."); + let connection_pool = PgPoolOptions::new() + .acquire_timeout(std::time::Duration::from_secs(2)) + .connect_lazy(configuration.database.connection_string().expose_secret()) + .expect("Failed to connect to Postgres."); startup::run(listener, connection_pool).await.unwrap(); }