Add a "check-config" command to validate config files & tests for it

This commit is contained in:
Andreas Fuchs 2024-10-04 10:49:08 -04:00
parent 70ee206031
commit 26ba489aa3
24 changed files with 492 additions and 0 deletions

193
Cargo.lock generated
View file

@ -80,6 +80,22 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38fa22307249f86fb7fad906fcae77f2564caeb56d7209103c551cd1cf4798f"
[[package]]
name = "assert_cmd"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "assign"
version = "1.1.1"
@ -316,6 +332,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
dependencies = [
"memchr",
"regex-automata 0.4.7",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -445,6 +472,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "console"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -559,6 +598,12 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.7"
@ -570,6 +615,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "ed25519"
version = "2.2.3"
@ -601,6 +652,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@ -666,6 +723,15 @@ dependencies = [
"miniz_oxide 0.8.0",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -801,6 +867,7 @@ name = "grapevine"
version = "0.1.0"
dependencies = [
"argon2",
"assert_cmd",
"async-trait",
"axum",
"axum-extra",
@ -816,6 +883,7 @@ dependencies = [
"hyper",
"hyper-util",
"image",
"insta",
"jsonwebtoken",
"lru-cache",
"nix",
@ -829,6 +897,7 @@ dependencies = [
"parking_lot",
"phf",
"pin-project-lite",
"predicates",
"prometheus",
"proxy-header",
"rand",
@ -1153,6 +1222,22 @@ dependencies = [
"serde",
]
[[package]]
name = "insta"
version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f72d3e19488cf7d8ea52d2fc0f8754fc933398b337cd3cbdb28aaeb35159ef"
dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"pest",
"pest_derive",
"regex",
"serde",
"similar",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
@ -1461,6 +1546,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1679,6 +1770,51 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "phf"
version = "0.11.2"
@ -1797,6 +1933,36 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931"
[[package]]
name = "predicates-tree"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@ -2673,6 +2839,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "similar"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
[[package]]
name = "simple_asn1"
version = "0.6.2"
@ -2805,6 +2977,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "thiserror"
version = "1.0.64"
@ -3281,6 +3459,12 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -3359,6 +3543,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.1"

View file

@ -148,6 +148,17 @@ xdg = "2.5.2"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", features = ["resource", "time"] }
[dev-dependencies]
assert_cmd = "2.0.16"
insta = { version = "1.40.0", features = ["filters", "json", "redactions"] }
predicates = "3.1.2"
[profile.dev.package.insta]
opt-level = 3
[profile.dev.package.similar]
opt-level = 3
[features]
default = ["rocksdb", "sqlite", "systemd"]

View file

@ -1,6 +1,7 @@
# Keep sorted
{ buildPlatform
, default
, cargo-insta
, engage
, inputs
, jq
@ -28,6 +29,7 @@ mkShell {
inputs.fenix.packages.${buildPlatform.system}.latest.rustfmt
# Keep sorted
cargo-insta
engage
jq
lychee

View file

@ -12,6 +12,7 @@ use crate::{
error, observability,
};
mod check_config;
mod serve;
/// Command line arguments
@ -29,6 +30,18 @@ pub(crate) struct Args {
pub(crate) enum Command {
/// Run the server.
Serve(ServeArgs),
/// Check the configuration file for syntax and semantic errors.
CheckConfig(CheckConfigArgs),
}
#[derive(clap::Args)]
pub(crate) struct CheckConfigArgs {
#[clap(flatten)]
config: ConfigArg,
#[clap(flatten)]
observability: ObservabilityArgs,
}
/// Wrapper for the `--config` arg.
@ -83,6 +96,9 @@ impl Args {
match self.command {
Command::Serve(args) => serve::run(args).await?,
Command::CheckConfig(args) => {
check_config::run(args.config).await?;
}
}
Ok(())
}
@ -93,6 +109,10 @@ impl Command {
// All subcommands other than `serve` should return `Some`. Keep these
// match arms sorted by the enum variant name.
match self {
Command::CheckConfig(args) => Some((
args.observability.log_format,
args.observability.log_filter.clone(),
)),
Command::Serve(_) => None,
}
}

11
src/cli/check_config.rs Normal file
View file

@ -0,0 +1,11 @@
use tracing::info;
use crate::{cli::ConfigArg, config, error};
pub(crate) async fn run(
args: ConfigArg,
) -> Result<(), error::CheckConfigCommand> {
let _config = config::load(args.config.as_ref()).await?;
info!("Configuration looks good");
Ok(())
}

View file

@ -45,6 +45,9 @@ pub(crate) enum Main {
#[error("failed to install global default tracing subscriber")]
SetSubscriber(#[from] tracing::subscriber::SetGlobalDefaultError),
#[error(transparent)]
CheckConfigCommand(#[from] CheckConfigCommand),
}
/// Errors returned from the `serve` CLI subcommand.
@ -72,6 +75,16 @@ pub(crate) enum ServeCommand {
ServerNameChanged(#[from] ServerNameChanged),
}
/// Errors returned from the `check-config` CLI subcommand.
// Missing docs are allowed here since that kind of information should be
// encoded in the error messages themselves anyway.
#[allow(missing_docs)]
#[derive(Error, Debug)]
pub(crate) enum CheckConfigCommand {
#[error("failed to validate configuration")]
Config(#[from] Config),
}
/// Error generated if `server_name` has changed or if checking this failed
// Missing docs are allowed here since that kind of information should be
// encoded in the error messages themselves anyway.

View file

@ -0,0 +1,95 @@
use std::{
path::{Path, PathBuf},
process::Output,
};
use assert_cmd::Command;
type TestError = Box<dyn std::error::Error>;
type TestResult = Result<(), TestError>;
fn fixture_path<P>(name: P) -> PathBuf
where
P: AsRef<Path>,
{
PathBuf::from("tests/integrations/fixtures/check_config").join(name)
}
fn run<P>(file: P) -> Result<Output, TestError>
where
P: AsRef<Path>,
{
Command::cargo_bin("grapevine")?
.args(["check-config", "--log-format=json", "-c"])
.arg(fixture_path(file))
.output()
.map_err(Into::into)
}
macro_rules! make_snapshot_test {
($name:ident, $description:expr, $fixture_name:expr $(,)?) => {
#[test]
fn $name() -> TestResult {
let output = run($fixture_name)?;
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
let status_code = output.status.code();
insta::with_settings!({
description => $description,
omit_expression => true,
snapshot_suffix => "stdout",
}, {
insta::assert_snapshot!(stdout);
});
let stderr_parse = serde_json::Deserializer::from_str(&stderr)
.into_iter::<serde_json::Value>()
.collect::<Result<Vec<_>, _>>();
insta::with_settings!({
description => $description,
omit_expression => true,
snapshot_suffix => "stderr",
}, {
if let Ok(stderr_json) = stderr_parse {
insta::assert_json_snapshot!(stderr_json, {
".*.timestamp" => "[timestamp]"
});
} else {
insta::assert_snapshot!(stderr);
}
});
insta::with_settings!({
description => $description,
omit_expression => true,
snapshot_suffix => "status_code",
}, {
insta::assert_debug_snapshot!(status_code);
});
Ok(())
}
};
}
make_snapshot_test!(valid_config, "A normal config is valid", "valid.toml");
make_snapshot_test!(
minimal_valid_config,
"A configuration containing only the required keys is valid",
"minimal-valid.toml",
);
make_snapshot_test!(
invalid_keys,
"A config with invalid keys fails",
"invalid-keys.toml",
);
make_snapshot_test!(
invalid_values,
"A config with invalid values fails",
"invalid-values.toml",
);

View file

@ -0,0 +1,2 @@
some_name = "example.com"
prort = 6167

View file

@ -0,0 +1,2 @@
server_name = 6667
port = "ircd"

View file

@ -0,0 +1,8 @@
server_name = "example.com"
[server_discovery]
client.base_url = "https://matrix.example.com"
[database]
backend = "rocksdb"
path = "/var/lib/grapevine"

View file

@ -0,0 +1,21 @@
server_name = "example.com"
allow_registration = false
max_request_size = 20_000_000
[server_discovery]
server.authority = "matrix.example.com:443"
client.base_url = "https://matrix.example.com"
[database]
backend = "rocksdb"
path = "/var/lib/grapevine"
[federation]
enable = true
trusted_servers = ["matrix.org"]
[[listen]]
type="tcp"
address = "0.0.0.0"

4
tests/integrations/main.rs Executable file
View file

@ -0,0 +1,4 @@
// <https://github.com/rust-lang/rust-clippy/issues/11024>
#![allow(clippy::tests_outside_test_module)]
mod check_config;

View file

@ -0,0 +1,8 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid keys fails
snapshot_kind: text
---
Some(
1,
)

View file

@ -0,0 +1,12 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid keys fails
snapshot_kind: text
---
Error: failed to validate configuration
Caused by: failed to parse configuration file "tests/integrations/fixtures/check_config/invalid-keys.toml"
Caused by: TOML parse error at line 1, column 1
|
1 | some_name = "example.com"
| ^^^^^^^^^^^^^^^^^^^^^^^^^
missing field `server_name`

View file

@ -0,0 +1,6 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid keys fails
snapshot_kind: text
---

View file

@ -0,0 +1,8 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid values fails
snapshot_kind: text
---
Some(
1,
)

View file

@ -0,0 +1,12 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid values fails
snapshot_kind: text
---
Error: failed to validate configuration
Caused by: failed to parse configuration file "tests/integrations/fixtures/check_config/invalid-values.toml"
Caused by: TOML parse error at line 1, column 15
|
1 | server_name = 6667
| ^^^^
invalid type: integer `6667`, expected a string

View file

@ -0,0 +1,6 @@
---
source: tests/integrations/check_config.rs
description: A config with invalid values fails
snapshot_kind: text
---

View file

@ -0,0 +1,8 @@
---
source: tests/integrations/check_config.rs
description: A configuration containing only the required keys is valid
snapshot_kind: text
---
Some(
0,
)

View file

@ -0,0 +1,15 @@
---
source: tests/integrations/check_config.rs
description: A configuration containing only the required keys is valid
snapshot_kind: text
---
[
{
"fields": {
"message": "Configuration looks good"
},
"level": "INFO",
"target": "grapevine::cli::check_config",
"timestamp": "[timestamp]"
}
]

View file

@ -0,0 +1,6 @@
---
source: tests/integrations/check_config.rs
description: A configuration containing only the required keys is valid
snapshot_kind: text
---

View file

@ -0,0 +1,8 @@
---
source: tests/integrations/check_config.rs
description: A normal config is valid
snapshot_kind: text
---
Some(
0,
)

View file

@ -0,0 +1,15 @@
---
source: tests/integrations/check_config.rs
description: A normal config is valid
snapshot_kind: text
---
[
{
"fields": {
"message": "Configuration looks good"
},
"level": "INFO",
"target": "grapevine::cli::check_config",
"timestamp": "[timestamp]"
}
]

View file

@ -0,0 +1,6 @@
---
source: tests/integrations/check_config.rs
description: A normal config is valid
snapshot_kind: text
---