Browse Source

config server launch

appflowy 3 years ago
parent
commit
54342850b2

+ 2 - 0
backend/Cargo.toml

@@ -23,9 +23,11 @@ log = "0.4.14"
 serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_repr = "0.1"
+serde-aux = "1.0.1"
 derive_more = {version = "0.99", features = ["display"]}
 protobuf = {version = "2.20.0"}
 uuid = { version = "0.8", features = ["serde", "v4"] }
+config = { version = "0.10.1", default-features = false, features = ["yaml"] }
 
 flowy-log = { path = "../rust-lib/flowy-log" }
 flowy-user = { path = "../rust-lib/flowy-user" }

+ 9 - 0
backend/configuration/base.yaml

@@ -0,0 +1,9 @@
+application:
+  port: 8000
+  host: 0.0.0.0
+database:
+  host: "localhost"
+  port: 5433
+  username: "postgres"
+  password: "password"
+  database_name: "flowy"

+ 5 - 0
backend/configuration/local.yaml

@@ -0,0 +1,5 @@
+application:
+  host: 127.0.0.1
+  base_url: "http://127.0.0.1"
+database:
+  require_ssl: false

+ 4 - 0
backend/configuration/production.yaml

@@ -0,0 +1,4 @@
+application:
+  host: 0.0.0.0
+database:
+  require_ssl: true

+ 16 - 0
backend/scripts/database/db_init.sh

@@ -2,6 +2,22 @@
 set -x
 set -eo pipefail
 
+if ! [ -x "$(command -v psql)" ]; then
+  echo >&2 "Error: `psql` is not installed."
+  echo >&2 "install using brew: brew install libpq."
+  echo >&2 "link to /usr/local/bin: brew link --force libpq ail"
+
+  exit 1
+fi
+
+if ! [ -x "$(command -v sqlx)" ]; then
+  echo >&2 "Error: `sqlx` is not installed."
+  echo >&2 "Use:"
+  echo >&2 "    cargo install --version=0.5.5 sqlx-cli --no-default-features --features postgres"
+  echo >&2 "to install it."
+  exit 1
+fi
+
 until psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q';
 do
   >&2 echo "Postgres is still unavailable - sleeping"

+ 83 - 0
backend/src/application.rs

@@ -0,0 +1,83 @@
+use crate::{
+    config::{get_configuration, DatabaseSettings, Settings},
+    context::AppContext,
+    routers::*,
+    user_service::Auth,
+    ws_service::WSServer,
+};
+use actix::Actor;
+use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope};
+use sqlx::{postgres::PgPoolOptions, PgPool};
+use std::{net::TcpListener, sync::Arc};
+
+pub struct Application {
+    port: u16,
+    server: Server,
+    app_ctx: Arc<AppContext>,
+}
+
+impl Application {
+    pub async fn build(configuration: Settings) -> Result<Self, std::io::Error> {
+        let app_ctx = init_app_context(&configuration).await;
+        let address = format!(
+            "{}:{}",
+            configuration.application.host, configuration.application.port
+        );
+        let listener = TcpListener::bind(&address)?;
+        let port = listener.local_addr().unwrap().port();
+        let server = run(listener, app_ctx.clone())?;
+        Ok(Self {
+            port,
+            server,
+            app_ctx,
+        })
+    }
+
+    pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { self.server.await }
+}
+
+pub fn run(listener: TcpListener, app_ctx: Arc<AppContext>) -> Result<Server, std::io::Error> {
+    let server = HttpServer::new(move || {
+        App::new()
+            .wrap(middleware::Logger::default())
+            .app_data(web::JsonConfig::default().limit(4096))
+            .service(ws_scope())
+            .service(user_scope())
+            .app_data(Data::new(app_ctx.ws_server.clone()))
+            .app_data(Data::new(app_ctx.db_pool.clone()))
+            .app_data(Data::new(app_ctx.auth.clone()))
+    })
+    .listen(listener)?
+    .run();
+    Ok(server)
+}
+
+fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) }
+
+fn user_scope() -> Scope {
+    web::scope("/user").service(web::resource("/register").route(web::post().to(user::register)))
+}
+
+async fn init_app_context(configuration: &Settings) -> Arc<AppContext> {
+    let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build();
+    let pg_pool = Arc::new(
+        get_connection_pool(&configuration.database)
+            .await
+            .expect("Failed to connect to Postgres."),
+    );
+
+    let ws_server = WSServer::new().start();
+
+    let auth = Arc::new(Auth::new(pg_pool.clone()));
+
+    let ctx = AppContext::new(ws_server, pg_pool, auth);
+
+    Arc::new(ctx)
+}
+
+pub async fn get_connection_pool(configuration: &DatabaseSettings) -> Result<PgPool, sqlx::Error> {
+    PgPoolOptions::new()
+        .connect_timeout(std::time::Duration::from_secs(2))
+        .connect_with(configuration.with_db())
+        .await
+}

+ 0 - 48
backend/src/config/config.rs

@@ -1,48 +0,0 @@
-use crate::config::DatabaseConfig;
-use std::{convert::TryFrom, sync::Arc};
-
-pub struct Config {
-    pub http_port: u16,
-    pub database: Arc<DatabaseConfig>,
-}
-
-impl Config {
-    pub fn new() -> Self {
-        Config {
-            http_port: 3030,
-            database: Arc::new(DatabaseConfig::default()),
-        }
-    }
-
-    pub fn server_addr(&self) -> String { format!("0.0.0.0:{}", self.http_port) }
-}
-
-pub enum Environment {
-    Local,
-    Production,
-}
-
-impl Environment {
-    #[allow(dead_code)]
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            Environment::Local => "local",
-            Environment::Production => "production",
-        }
-    }
-}
-
-impl TryFrom<String> for Environment {
-    type Error = String;
-
-    fn try_from(s: String) -> Result<Self, Self::Error> {
-        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
-            )),
-        }
-    }
-}

+ 99 - 0
backend/src/config/configuration.rs

@@ -0,0 +1,99 @@
+use serde_aux::field_attributes::deserialize_number_from_string;
+use sqlx::postgres::{PgConnectOptions, PgSslMode};
+use std::convert::{TryFrom, TryInto};
+
+#[derive(serde::Deserialize, Clone)]
+pub struct Settings {
+    pub database: DatabaseSettings,
+    pub application: ApplicationSettings,
+}
+
+#[derive(serde::Deserialize, Clone)]
+pub struct ApplicationSettings {
+    #[serde(deserialize_with = "deserialize_number_from_string")]
+    pub port: u16,
+    pub host: String,
+    pub base_url: String,
+}
+
+#[derive(serde::Deserialize, Clone)]
+pub struct DatabaseSettings {
+    pub username: String,
+    pub password: String,
+    #[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 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)
+            .port(self.port)
+            .ssl_mode(ssl_mode)
+    }
+
+    pub fn with_db(&self) -> PgConnectOptions { self.without_db().database(&self.database_name) }
+}
+
+pub fn get_configuration() -> Result<Settings, config::ConfigError> {
+    let mut settings = config::Config::default();
+    let base_path = std::env::current_dir().expect("Failed to determine the current directory");
+    let configuration_dir = base_path.join("configuration");
+
+    settings.merge(config::File::from(configuration_dir.join("base")).required(true))?;
+
+    let environment: Environment = std::env::var("APP_ENVIRONMENT")
+        .unwrap_or_else(|_| "local".into())
+        .try_into()
+        .expect("Failed to parse APP_ENVIRONMENT.");
+
+    settings
+        .merge(config::File::from(configuration_dir.join(environment.as_str())).required(true))?;
+
+    // Add in settings from environment variables (with a prefix of APP and '__' as
+    // separator) E.g. `APP_APPLICATION__PORT=5001 would set
+    // `Settings.application.port`
+    settings.merge(config::Environment::with_prefix("app").separator("__"))?;
+
+    settings.try_into()
+}
+
+/// 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<String> for Environment {
+    type Error = String;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        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
+            )),
+        }
+    }
+}

+ 0 - 5
backend/src/config/database/config.toml

@@ -1,5 +0,0 @@
-host = "localhost"
-port = 5433
-username = "postgres"
-password = "password"
-database_name = "flowy"

+ 0 - 33
backend/src/config/database/database.rs

@@ -1,33 +0,0 @@
-use serde::Deserialize;
-
-#[derive(Deserialize)]
-pub struct DatabaseConfig {
-    username: String,
-    password: String,
-    port: u16,
-    host: String,
-    database_name: String,
-}
-
-impl DatabaseConfig {
-    pub fn connect_url(&self) -> String {
-        format!(
-            "postgres://{}:{}@{}:{}/{}",
-            self.username, self.password, self.host, self.port, self.database_name
-        )
-    }
-
-    pub fn set_env_db_url(&self) {
-        let url = self.connect_url();
-        std::env::set_var("DATABASE_URL", url);
-    }
-}
-
-impl std::default::Default for DatabaseConfig {
-    fn default() -> DatabaseConfig {
-        let toml_str: &str = include_str!("config.toml");
-        let config: DatabaseConfig = toml::from_str(toml_str).unwrap();
-        config.set_env_db_url();
-        config
-    }
-}

+ 0 - 3
backend/src/config/database/mod.rs

@@ -1,3 +0,0 @@
-mod database;
-
-pub use database::*;

+ 2 - 4
backend/src/config/mod.rs

@@ -1,7 +1,5 @@
-mod config;
+mod configuration;
 mod const_define;
-mod database;
 
-pub use config::*;
+pub use configuration::*;
 pub use const_define::*;
-pub use database::*;

+ 2 - 9
backend/src/context.rs

@@ -1,25 +1,18 @@
-use crate::{config::Config, user_service::Auth, ws_service::WSServer};
+use crate::{user_service::Auth, ws_service::WSServer};
 use actix::Addr;
 
 use sqlx::PgPool;
 use std::sync::Arc;
 
 pub struct AppContext {
-    pub config: Arc<Config>,
     pub ws_server: Addr<WSServer>,
     pub db_pool: Arc<PgPool>,
     pub auth: Arc<Auth>,
 }
 
 impl AppContext {
-    pub fn new(
-        config: Arc<Config>,
-        ws_server: Addr<WSServer>,
-        db_pool: Arc<PgPool>,
-        auth: Arc<Auth>,
-    ) -> Self {
+    pub fn new(ws_server: Addr<WSServer>, db_pool: Arc<PgPool>, auth: Arc<Auth>) -> Self {
         AppContext {
-            config,
             ws_server,
             db_pool,
             auth,

+ 2 - 2
backend/src/lib.rs

@@ -1,7 +1,7 @@
-mod config;
+pub mod application;
+pub mod config;
 mod context;
 mod entities;
 mod routers;
-pub mod startup;
 pub mod user_service;
 pub mod ws_service;

+ 6 - 5
backend/src/main.rs

@@ -1,10 +1,11 @@
-use backend::startup::{init_app_context, run};
+use backend::{application::Application, config::get_configuration};
 use std::net::TcpListener;
 
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {
-    let app_ctx = init_app_context().await;
-    let listener =
-        TcpListener::bind(app_ctx.config.server_addr()).expect("Failed to bind server address");
-    run(app_ctx, listener)?.await
+    let configuration = get_configuration().expect("Failed to read configuration.");
+    let application = Application::build(configuration).await?;
+    application.run_until_stopped().await?;
+
+    Ok(())
 }

+ 0 - 50
backend/src/startup.rs

@@ -1,50 +0,0 @@
-use crate::{
-    config::Config,
-    context::AppContext,
-    routers::*,
-    user_service::Auth,
-    ws_service::WSServer,
-};
-use actix::Actor;
-use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope};
-use sqlx::PgPool;
-use std::{net::TcpListener, sync::Arc};
-
-pub fn run(app_ctx: Arc<AppContext>, listener: TcpListener) -> Result<Server, std::io::Error> {
-    let server = HttpServer::new(move || {
-        App::new()
-            .wrap(middleware::Logger::default())
-            .app_data(web::JsonConfig::default().limit(4096))
-            .service(ws_scope())
-            .service(user_scope())
-            .app_data(Data::new(app_ctx.ws_server.clone()))
-            .app_data(Data::new(app_ctx.db_pool.clone()))
-            .app_data(Data::new(app_ctx.auth.clone()))
-    })
-    .listen(listener)?
-    .run();
-    Ok(server)
-}
-
-fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) }
-
-fn user_scope() -> Scope {
-    web::scope("/user").service(web::resource("/register").route(web::post().to(user::register)))
-}
-
-pub async fn init_app_context() -> Arc<AppContext> {
-    let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build();
-    let config = Arc::new(Config::new());
-
-    // TODO: what happened when PgPool connect fail?
-    let db_pool = Arc::new(
-        PgPool::connect(&config.database.connect_url())
-            .await
-            .expect("Failed to connect to Postgres."),
-    );
-    let ws_server = WSServer::new().start();
-    let auth = Arc::new(Auth::new(db_pool.clone()));
-
-    let ctx = AppContext::new(config, ws_server, db_pool, auth);
-    Arc::new(ctx)
-}