split CLI in multiple commands

- `config` to view & check config files
 - `database` for DB-related ops
 - `server` to run the server
This commit is contained in:
Quentin Gliech
2021-07-16 14:34:18 +02:00
parent ad136e757d
commit 58b16511db
9 changed files with 361 additions and 42 deletions

115
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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(())
}
}
}
}

View File

@@ -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(())
}
}

View File

@@ -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<Subcommand>,
}
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> {
RootConfig::load(&self.config).context("Could not load configuration")
}
}

View File

@@ -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(())
}
}

View File

@@ -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<PgPool, sqlx::Error> {
pub async fn connect(&self) -> anyhow::Result<PgPool> {
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")
}
}

View File

@@ -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<RootConfig, FigmentError> {
pub fn load<P>(path: P) -> Result<RootConfig, FigmentError>
where
P: AsRef<Path>,
{
Figment::new()
.merge(Env::prefixed("MAS_").split("_"))
.merge(Yaml::file("config.yaml"))
.merge(Yaml::file(path))
.extract()
}
}

View File

@@ -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
}