From 58b16511db2a384bb6bde14665d99593e20d8a4b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 16 Jul 2021 14:34:18 +0200 Subject: [PATCH] split CLI in multiple commands - `config` to view & check config files - `database` for DB-related ops - `server` to run the server --- Cargo.lock | 115 ++++++++++++++++++ matrix-authentication-service/Cargo.toml | 2 + .../src/cli/config.rs | 65 ++++++++++ .../src/cli/database.rs | 46 +++++++ matrix-authentication-service/src/cli/mod.rs | 63 ++++++++++ .../src/cli/server.rs | 52 ++++++++ .../src/config/database.rs | 4 +- .../src/config/mod.rs | 9 +- matrix-authentication-service/src/main.rs | 47 ++----- 9 files changed, 361 insertions(+), 42 deletions(-) create mode 100644 matrix-authentication-service/src/cli/config.rs create mode 100644 matrix-authentication-service/src/cli/database.rs create mode 100644 matrix-authentication-service/src/cli/mod.rs create mode 100644 matrix-authentication-service/src/cli/server.rs diff --git a/Cargo.lock b/Cargo.lock index 10d2e864f..2a4426286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,6 +397,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -645,6 +656,38 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -1355,6 +1398,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "indoc" version = "1.0.3" @@ -1497,6 +1550,7 @@ dependencies = [ "async-std", "async-trait", "chrono", + "clap", "csrf", "data-encoding", "figment", @@ -1505,6 +1559,7 @@ dependencies = [ "schemars", "serde", "serde_with", + "serde_yaml", "sqlx", "tera", "thiserror", @@ -1611,6 +1666,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + [[package]] name = "parking" version = "2.0.0" @@ -1827,6 +1888,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2616,6 +2701,24 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.26" @@ -2923,6 +3026,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -2981,6 +3090,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.3" diff --git a/matrix-authentication-service/Cargo.toml b/matrix-authentication-service/Cargo.toml index 985ec474e..63ecf15d5 100644 --- a/matrix-authentication-service/Cargo.toml +++ b/matrix-authentication-service/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" license = "Apache-2.0" [dependencies] +clap = "3.0.0-beta.2" serde = { version = "1.0.126", features = ["derive"] } async-std = { version = "1.9.0", features = ["attributes"] } tide = "0.16.0" @@ -26,3 +27,4 @@ mime = "0.3.16" sqlx = { version = "0.5.5", features = ["runtime-async-std-rustls", "postgres", "migrate", "chrono"] } serde_with = { version = "1.9.4", features = ["hex", "chrono"] } schemars = { version = "0.8.3", features = ["url", "chrono"] } +serde_yaml = "0.8.17" diff --git a/matrix-authentication-service/src/cli/config.rs b/matrix-authentication-service/src/cli/config.rs new file mode 100644 index 000000000..7027cad85 --- /dev/null +++ b/matrix-authentication-service/src/cli/config.rs @@ -0,0 +1,65 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Clap; +use schemars::schema_for; +use tracing::info; + +use super::RootCommand; +use crate::config::RootConfig; + +#[derive(Clap, Debug)] +pub(super) struct ConfigCommand { + #[clap(subcommand)] + subcommand: ConfigSubcommand, +} + +#[derive(Clap, Debug)] +enum ConfigSubcommand { + /// Dump the current config as YAML + Dump, + + /// Print the JSON Schema that validates configuration files + Schema, + + /// Check a config file + Check, +} + +impl ConfigCommand { + pub async fn run(&self, root: &RootCommand) -> anyhow::Result<()> { + use ConfigSubcommand as SC; + match &self.subcommand { + SC::Dump => { + let config = root.load_config()?; + + serde_yaml::to_writer(std::io::stdout(), &config)?; + + Ok(()) + } + SC::Schema => { + let schema = schema_for!(RootConfig); + + serde_yaml::to_writer(std::io::stdout(), &schema)?; + + Ok(()) + } + SC::Check => { + root.load_config()?; + info!(path = ?root.config, "Configuration file looks good"); + Ok(()) + } + } + } +} diff --git a/matrix-authentication-service/src/cli/database.rs b/matrix-authentication-service/src/cli/database.rs new file mode 100644 index 000000000..5b08e7361 --- /dev/null +++ b/matrix-authentication-service/src/cli/database.rs @@ -0,0 +1,46 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Context; +use clap::Clap; + +use super::RootCommand; +use crate::storage::MIGRATOR; + +#[derive(Clap, Debug)] +pub(super) struct DatabaseCommand { + #[clap(subcommand)] + subcommand: DatabaseSubcommand, +} + +#[derive(Clap, Debug)] +enum DatabaseSubcommand { + /// Run database migrations + Migrate, +} + +impl DatabaseCommand { + pub async fn run(&self, root: &RootCommand) -> anyhow::Result<()> { + let config = root.load_config()?; + let pool = config.database.connect().await?; + + // Run pending migrations + MIGRATOR + .run(&pool) + .await + .context("could not run migrations")?; + + Ok(()) + } +} diff --git a/matrix-authentication-service/src/cli/mod.rs b/matrix-authentication-service/src/cli/mod.rs new file mode 100644 index 000000000..3419665ac --- /dev/null +++ b/matrix-authentication-service/src/cli/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::PathBuf; + +use anyhow::Context; +use clap::Clap; + +use self::{config::ConfigCommand, database::DatabaseCommand, server::ServerCommand}; +use crate::config::RootConfig; + +mod config; +mod database; +mod server; + +#[derive(Clap, Debug)] +enum Subcommand { + /// Configuration-related commands + Config(ConfigCommand), + + /// Manage the database + Database(DatabaseCommand), + + /// Runs the web server + Server(ServerCommand), +} + +#[derive(Clap, Debug)] +pub struct RootCommand { + /// Path to the configuration file + #[clap(short, long, global = true, default_value = "config.yaml")] + config: PathBuf, + + #[clap(subcommand)] + subcommand: Option, +} + +impl RootCommand { + pub async fn run(&self) -> anyhow::Result<()> { + use Subcommand as S; + match &self.subcommand { + Some(S::Config(c)) => c.run(self).await, + Some(S::Database(c)) => c.run(self).await, + Some(S::Server(c)) => c.run(self).await, + None => ServerCommand::default().run(self).await, + } + } + + fn load_config(&self) -> anyhow::Result { + RootConfig::load(&self.config).context("Could not load configuration") + } +} diff --git a/matrix-authentication-service/src/cli/server.rs b/matrix-authentication-service/src/cli/server.rs new file mode 100644 index 000000000..cde417e50 --- /dev/null +++ b/matrix-authentication-service/src/cli/server.rs @@ -0,0 +1,52 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Context; +use clap::Clap; + +use super::RootCommand; +use crate::state::State; + +#[derive(Clap, Debug, Default)] +pub(super) struct ServerCommand; + +impl ServerCommand { + pub async fn run(&self, root: &RootCommand) -> anyhow::Result<()> { + let config = root.load_config()?; + + // Connect to the database + let pool = config.database.connect().await?; + + // Load and compile the templates + let templates = crate::templates::load().context("could not load templates")?; + + // Create the shared state + let state = State::new(config, templates, pool); + state + .storage() + .load_static_clients(&state.config().oauth2.clients) + .await; + + // Start the server + let address = state.config().http.address.clone(); + let mut app = tide::with_state(state); + app.with(tide_tracing::TraceMiddleware::new()); + crate::handlers::install(&mut app); + app.listen(address) + .await + .context("could not start server")?; + + Ok(()) + } +} diff --git a/matrix-authentication-service/src/config/database.rs b/matrix-authentication-service/src/config/database.rs index e39f958cb..e6d8b9e34 100644 --- a/matrix-authentication-service/src/config/database.rs +++ b/matrix-authentication-service/src/config/database.rs @@ -14,6 +14,7 @@ use std::time::Duration; +use anyhow::Context; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -78,7 +79,7 @@ pub struct DatabaseConfig { impl DatabaseConfig { #[tracing::instrument(err)] - pub async fn connect(&self) -> Result { + pub async fn connect(&self) -> anyhow::Result { PgPoolOptions::new() .max_connections(self.max_connections) .min_connections(self.min_connections) @@ -87,5 +88,6 @@ impl DatabaseConfig { .max_lifetime(self.max_lifetime) .connect(&self.uri) .await + .context("could not connect to the database") } } diff --git a/matrix-authentication-service/src/config/mod.rs b/matrix-authentication-service/src/config/mod.rs index 9203f10eb..1a410a872 100644 --- a/matrix-authentication-service/src/config/mod.rs +++ b/matrix-authentication-service/src/config/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::path::Path; + use figment::{ error::Error as FigmentError, providers::{Env, Format, Yaml}, @@ -51,10 +53,13 @@ pub struct RootConfig { } impl RootConfig { - pub fn load() -> Result { + pub fn load

(path: P) -> Result + where + P: AsRef, + { Figment::new() .merge(Env::prefixed("MAS_").split("_")) - .merge(Yaml::file("config.yaml")) + .merge(Yaml::file(path)) .extract() } } diff --git a/matrix-authentication-service/src/main.rs b/matrix-authentication-service/src/main.rs index 45e2418da..584a5d691 100644 --- a/matrix-authentication-service/src/main.rs +++ b/matrix-authentication-service/src/main.rs @@ -18,9 +18,10 @@ #![allow(clippy::module_name_repetitions)] use anyhow::Context; -use tracing::{info_span, Instrument}; +use clap::Clap; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; +mod cli; mod config; mod csrf; mod handlers; @@ -29,10 +30,10 @@ mod state; mod storage; mod templates; -use self::{config::RootConfig, state::State, storage::MIGRATOR}; +use self::cli::RootCommand; #[async_std::main] -async fn main() -> tide::Result<()> { +async fn main() -> anyhow::Result<()> { // Setup logging & tracing let fmt_layer = tracing_subscriber::fmt::layer(); let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; @@ -42,41 +43,9 @@ async fn main() -> tide::Result<()> { .try_init() .context("could not initialize logging")?; - // Loading the config - let config = RootConfig::load().context("could not load config")?; + // Parse the CLI arguments + let opts = RootCommand::parse(); - // Connect to the database - let pool = config - .database - .connect() - .await - .context("could not connect to database")?; - - // Load and compile the templates - let templates = self::templates::load().context("could not load templates")?; - - // Create the shared state - let state = State::new(config, templates, pool); - state - .storage() - .load_static_clients(&state.config().oauth2.clients) - .await; - - // Run pending migrations - // TODO: make this a separate command - MIGRATOR - .run(state.storage().pool()) - .instrument(info_span!("migrations")) - .await - .context("could not run migrations")?; - - // Start the server - let address = state.config().http.address.clone(); - let mut app = tide::with_state(state); - app.with(tide_tracing::TraceMiddleware::new()); - self::handlers::install(&mut app); - app.listen(address) - .await - .context("could not start server")?; - Ok(()) + // And run the command + opts.run().await }