Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syntactic validation of configuration file #1128

Merged
merged 9 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

## Unreleased

### FEATURES

- [ibc-relayer-cli]
- Added `config validate` CLI to Hermes ([#600])

### IMPROVEMENTS

- Update to `tendermint-rs` v0.20.0 ([#1125])

[#600]: https://github.com/informalsystems/ibc-rs/issues/600
[#1125]: https://github.com/informalsystems/ibc-rs/issues/1125


Expand Down
8 changes: 6 additions & 2 deletions relayer-cli/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ use abscissa_core::terminal::component::Terminal;
use abscissa_core::{
application::{self, AppCell},
component::Component,
config, Application, Configurable, FrameworkError, StandardPaths,
config, Application, Configurable, FrameworkError, FrameworkErrorKind, StandardPaths,
};

use crate::components::{JsonTracing, PrettyTracing};
use crate::entry::EntryPoint;
use crate::{commands::CliCmd, config::Config};
use crate::{commands::CliCmd, config::validate_config, config::Config};

/// Application state
pub static APPLICATION: AppCell<CliApp> = AppCell::new();
Expand Down Expand Up @@ -110,7 +110,11 @@ impl Application for CliApp {
fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> {
// Configure components
self.state.components.after_config(&config)?;

validate_config(&config)
.map_err(|validation_err| FrameworkErrorKind::ConfigError.context(validation_err))?;
self.config = Some(config);

Ok(())
}

Expand Down
17 changes: 8 additions & 9 deletions relayer-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ use crate::config::Config;
use crate::DEFAULT_CONFIG_PATH;

use self::{
create::CreateCmds, keys::KeysCmd, listen::ListenCmd, query::QueryCmd, start::StartCmd,
tx::TxCmd, update::UpdateCmds, upgrade::UpgradeCmds, version::VersionCmd,
config::ConfigCmd, create::CreateCmds, keys::KeysCmd, listen::ListenCmd,
misbehaviour::MisbehaviourCmd, query::QueryCmd, start::StartCmd, tx::TxCmd, update::UpdateCmds,
upgrade::UpgradeCmds, version::VersionCmd,
};
use crate::commands::misbehaviour::MisbehaviourCmd;

mod config;
mod create;
Expand All @@ -38,18 +38,17 @@ pub fn default_config_file() -> Option<PathBuf> {
dirs_next::home_dir().map(|home| home.join(DEFAULT_CONFIG_PATH))
}

// TODO: Re-add the `config` subcommand
// /// The `config` subcommand
// #[options(help = "manipulate the relayer configuration")]
// Config(ConfigCmd),

/// Cli Subcommands
#[derive(Command, Debug, Options, Runnable)]
pub enum CliCmd {
/// The `help` subcommand
#[options(help = "Get usage information")]
Help(Help<Self>),

/// The `config` subcommand
#[options(help = "Validate Hermes configuration file")]
Config(ConfigCmd),
romac marked this conversation as resolved.
Show resolved Hide resolved

/// The `keys` subcommand
#[options(help = "Manage keys in the relayer for each chain")]
Keys(KeysCmd),
Expand All @@ -68,7 +67,7 @@ pub enum CliCmd {

/// The `start` subcommand
#[options(help = "Start the relayer in multi-chain mode. \
Relays packets and channel handshake messages between all chains in the config.")]
Relays packets and open handshake messages between all chains in the config.")]
Start(StartCmd),

/// The `query` subcommand
Expand Down
10 changes: 6 additions & 4 deletions relayer-cli/src/commands/config/validate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use abscissa_core::{Command, Options, Runnable};

use crate::conclude::Output;
use crate::config;
use crate::prelude::*;

#[derive(Command, Debug, Options)]
Expand All @@ -10,10 +11,11 @@ impl Runnable for ValidateCmd {
/// Validate the loaded configuration.
fn run(&self) {
let config = app_config();
info!("Loaded configuration: {:?}", *config);
trace!("loaded configuration: {:#?}", *config);

// TODO: Validate configuration

Output::with_success().exit();
match config::validate_config(&config) {
Ok(_) => Output::success("validation passed successfully").exit(),
Err(e) => Output::error(format!("{}", e)).exit(),
}
}
}
6 changes: 6 additions & 0 deletions relayer-cli/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use ibc_relayer::config::Config;
use ibc_relayer::supervisor::Supervisor;

use crate::conclude::Output;
use crate::config;
use crate::prelude::*;

#[derive(Clone, Command, Debug, Options)]
Expand All @@ -15,6 +16,11 @@ impl Runnable for StartCmd {
fn run(&self) {
let config = app_config();

// No chain is preconfigured
if config.chains.is_empty() {
Output::error(config::Error::ZeroChains).exit();
};

match spawn_supervisor(config.clone()).and_then(|s| {
info!("Hermes has started");
s.run()
Expand Down
31 changes: 25 additions & 6 deletions relayer-cli/src/components.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io;

use abscissa_core::{Component, FrameworkError};
use abscissa_core::{Component, FrameworkError, FrameworkErrorKind};
use tracing_subscriber::{
fmt::{
format::{DefaultFields, Format, Full, Json, JsonFields},
Expand All @@ -14,6 +14,8 @@ use tracing_subscriber::{

use ibc_relayer::config::GlobalConfig;

use crate::config;

/// Custom types to simplify the `Tracing` definition below
type JsonFormatter = TracingFormatter<JsonFields, Format<Json, SystemTime>, StdWriter>;
type PrettyFormatter = TracingFormatter<DefaultFields, Format<Full, SystemTime>, StdWriter>;
Expand All @@ -34,7 +36,7 @@ impl JsonTracing {
/// Creates a new [`Tracing`] component
#[allow(trivial_casts)]
pub fn new(cfg: GlobalConfig) -> Result<Self, FrameworkError> {
let filter = build_tracing_filter(cfg.log_level);
let filter = build_tracing_filter(cfg.log_level.to_string())?;
// Note: JSON formatter is un-affected by ANSI 'color' option. Set to 'false'.
let use_color = false;

Expand Down Expand Up @@ -65,7 +67,7 @@ impl PrettyTracing {
/// Creates a new [`Tracing`] component
#[allow(trivial_casts)]
pub fn new(cfg: GlobalConfig) -> Result<Self, FrameworkError> {
let filter = build_tracing_filter(cfg.log_level);
let filter = build_tracing_filter(cfg.log_level.to_string())?;

// Construct a tracing subscriber with the supplied filter and enable reloading.
let builder = FmtSubscriber::builder()
Expand All @@ -92,12 +94,29 @@ fn enable_ansi() -> bool {
atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stderr)
}

fn build_tracing_filter(log_level: String) -> String {
/// Builds a tracing filter based on the input `log_level`.
/// Enables tracing exclusively for the relayer crates.
/// Returns error if the filter failed to build.
fn build_tracing_filter(log_level: String) -> Result<EnvFilter, FrameworkError> {
let target_crates = ["ibc_relayer", "ibc_relayer_cli"];

target_crates
// SAFETY: unwrap() below works as long as `target_crates` is not empty.
let directive_raw = target_crates
.iter()
.map(|&c| format!("{}={}", c, log_level))
.reduce(|a, b| format!("{},{}", a, b))
.unwrap()
.unwrap();

// Build the filter directive
match EnvFilter::try_new(directive_raw.clone()) {
Ok(out) => Ok(out),
Err(e) => {
let our_err = config::Error::InvalidLogLevel(log_level, e.to_string());
eprintln!(
"Unable to initialize Hermes from filter directive {:?}: {}",
directive_raw, e
);
Err(FrameworkErrorKind::ConfigError.context(our_err).into())
}
}
}
44 changes: 32 additions & 12 deletions relayer-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@
//! application's configuration file and/or command-line options
//! for specifying it.

use std::path::PathBuf;
use std::collections::BTreeSet;

use abscissa_core::{error::BoxError, EntryPoint, Options};

use crate::commands::CliCmd;
use thiserror::Error;

use ibc::ics24_host::identifier::ChainId;
pub use ibc_relayer::config::Config;

/// Get the path to configuration file
pub fn config_path() -> Result<PathBuf, BoxError> {
let mut args = std::env::args();
assert!(args.next().is_some(), "expected one argument but got zero");
let args = args.collect::<Vec<_>>();
let app = EntryPoint::<CliCmd>::parse_args_default(args.as_slice())?;
let config_path = app.config.ok_or("no config file specified")?;
Ok(config_path)
/// Specifies all the possible syntactic errors
/// that a Hermes configuration file could contain.
#[derive(Error, Debug)]
pub enum Error {
/// No chain is configured
#[error("config file does not specify any chain")]
ZeroChains,

/// The log level is invalid
#[error("config file specifies an invalid log level ('{0}'), caused by: {1}")]
InvalidLogLevel(String, String),

/// Duplicate chains configured
#[error("config file has duplicate entry for the chain with id {0}")]
DuplicateChains(ChainId),
}

/// Method for syntactic validation of the input
/// configuration file.
pub fn validate_config(config: &Config) -> Result<(), Error> {
// Check for duplicate chain configuration.
let mut unique_chain_ids = BTreeSet::new();
for chain_id in config.chains.iter().map(|c| c.id.clone()) {
if !unique_chain_ids.insert(chain_id.clone()) {
return Err(Error::DuplicateChains(chain_id));
}
}

Ok(())
}
44 changes: 38 additions & 6 deletions relayer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ pub mod default {
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub global: GlobalConfig,
#[serde(default)]
pub telemetry: TelemetryConfig,
Expand Down Expand Up @@ -83,26 +85,55 @@ impl Default for Strategy {
}
}

/// Log levels are wrappers over [`tracing_core::Level`].
///
/// [`tracing_core::Level`]: https://docs.rs/tracing-core/0.1.17/tracing_core/struct.Level.html
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
romac marked this conversation as resolved.
Show resolved Hide resolved
Trace,
Debug,
Info,
Warn,
Error,
}

impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}

impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Trace => write!(f, "trace"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Info => write!(f, "info"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::Error => write!(f, "error"),
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct GlobalConfig {
#[serde(default)]
pub strategy: Strategy,

/// All valid log levels, as defined in tracing:
/// https://docs.rs/tracing-core/0.1.17/tracing_core/struct.Level.html
pub log_level: String,
pub log_level: LogLevel,
}

impl Default for GlobalConfig {
fn default() -> Self {
Self {
strategy: Strategy::default(),
log_level: "info".to_string(),
log_level: LogLevel::default(),
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TelemetryConfig {
pub enabled: bool,
pub host: String,
Expand All @@ -120,6 +151,7 @@ impl Default for TelemetryConfig {
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ChainConfig {
pub id: ChainId,
pub rpc_addr: tendermint_rpc::Url,
Expand Down