From 1bd058d937ef4c78e7f0560ecf9bce8c3d81e8a2 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 8 Dec 2020 16:44:53 +0100 Subject: [PATCH] Add --all option to `light rm` command to remove all peers (#432) * Add --all option to `light rm` command to remove all peers * Add --yes option to skip confirmation * Update changelog --- CHANGELOG.md | 5 + relayer-cli/src/commands/light/rm.rs | 159 ++++++++++++++++++--------- relayer/src/config.rs | 2 +- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1b08d98d..7af6ac5ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ We also consolidated our TLA+ specs into an "IBC Core TLA+ specification," and a Special thanks to external contributors for this release: @CharlyCst ([#347], [#419]). +- [relayer-cli] + - Add `--all` option to `light rm` command to remove all peers for a given chain ([#431]) + +[#431]: https://github.com/informalsystems/ibc-rs/issues/431 + ### FEATURES - Update to tendermint-rs version `0.17-RC3` ([#403]) diff --git a/relayer-cli/src/commands/light/rm.rs b/relayer-cli/src/commands/light/rm.rs index 0847c94519..6833c56c95 100644 --- a/relayer-cli/src/commands/light/rm.rs +++ b/relayer-cli/src/commands/light/rm.rs @@ -1,119 +1,168 @@ -use std::ops::Deref; +use std::{io, io::Write, ops::Deref}; use crate::prelude::*; use abscissa_core::{application::fatal_error, error::BoxError, Command, Options, Runnable}; use ibc::ics24_host::identifier::ChainId; -use relayer::config::Config; +use relayer::config::PeersConfig; use tendermint_light_client::types::PeerId; #[derive(Command, Debug, Options)] pub struct RmCmd { - /// peer id for this client + /// identifiers of peers to remove #[options(free)] - peer_id: Option, + peer_id: Vec, + /// identifier of the chain to remove peers from #[options(short = "c")] - /// identifier of the chain chain_id: Option, /// force removal of primary peer #[options(short = "f")] force: bool, + + /// remove all peers, implies --force + #[options(no_short)] + all: bool, + + /// skip confirmation + yes: bool, } #[derive(Clone, Debug)] struct RmOptions { - /// identifier of the chain + /// identifier of the chain to remove peers from chain_id: ChainId, - /// peer id for this client - peer_id: PeerId, + /// identifiers of peers to remove + peer_ids: Vec, + + /// remove all peers, implies --force + all: bool, /// force removal of primary peer force: bool, + + /// skip confirmation + yes: bool, } impl RmOptions { fn from_cmd(cmd: &RmCmd) -> Result { let chain_id = cmd.chain_id.clone().ok_or("missing chain identifier")?; - let peer_id = cmd.peer_id.ok_or("missing peer identifier")?; - let force = cmd.force; + let peer_ids = if !cmd.all && cmd.peer_id.is_empty() { + return Err("missing peer identifier".into()); + } else { + cmd.peer_id.clone() + }; Ok(RmOptions { chain_id, - peer_id, - force, + peer_ids, + all: cmd.all, + yes: cmd.yes, + force: cmd.force, }) } } +impl Runnable for RmCmd { + fn run(&self) { + self.cmd() + .unwrap_or_else(|e| fatal_error(app_reader().deref(), &*e)) + } +} + impl RmCmd { - fn update_config(options: RmOptions, config: &mut Config) -> Result { + fn cmd(&self) -> Result<(), BoxError> { + let options = RmOptions::from_cmd(self).map_err(|e| format!("invalid options: {}", e))?; + let mut config = (*app_config()).clone(); + let chain_config = config .chains .iter_mut() .find(|c| c.id == options.chain_id) .ok_or_else(|| format!("could not find config for chain: {}", options.chain_id))?; - let peers_config = chain_config + let mut peers_config = chain_config .peers .as_mut() .ok_or_else(|| format!("no peers configured for chain: {}", options.chain_id))?; - // Check if the given peer actually exists already, if not throw an error. - let peer_exists = peers_config.light_client(options.peer_id).is_some(); - if !peer_exists { - return Err(format!("cannot find peer: {}", options.peer_id).into()); - } + if options.all && (options.yes || confirm(&options.chain_id)?) { + let removed_peers = get_all_peer_ids(&peers_config); + chain_config.peers = None; + status_ok!("Removed", "light client peers {:?}", removed_peers); + } else { + for peer_id in options.peer_ids { + let removed_peer = remove_peer(&mut peers_config, peer_id, options.force)?; + status_ok!("Removed", "light client peer '{}'", removed_peer); + } + }; - // Only allow remove the primary peer if the --force option is set - let is_primary = peers_config.primary == options.peer_id; - if is_primary && !options.force { - return Err("cannot remove primary peer, pass --force flag to force removal".into()); - } + let config_path = crate::config::config_path()?; + relayer::config::store(&config, config_path)?; - // Filter out the light client config with the specified peer id - peers_config - .light_clients - .retain(|p| p.peer_id != options.peer_id); - - // Disallow removing the last remaining peer - if peers_config.light_clients.is_empty() { - return Err( - "cannot remove last remaining peer, add other peers before removing this one" - .into(), - ); - } + Ok(()) + } +} - // If the peer we removed was the primary peer, use the next available peer as the primary - if is_primary { - let new_primary = peers_config.light_clients.first().unwrap(); // SAFETY: safe because of check above +fn remove_peer( + peers_config: &mut PeersConfig, + peer_id: PeerId, + force: bool, +) -> Result { + // Check if the given peer actually exists already, if not throw an error. + let peer_exists = peers_config.light_client(peer_id).is_some(); + if !peer_exists { + return Err(format!("cannot find peer: {}", peer_id).into()); + } + + // Only allow remove the primary peer if the --force option is set + let removed_primary = peers_config.primary == peer_id; + if removed_primary && !force { + return Err("cannot remove primary peer, pass --force flag to force removal".into()); + } + + // Filter out the light client config with the specified peer id + peers_config.light_clients.retain(|p| p.peer_id != peer_id); + + // If the peer we removed was the primary peer, use the next available peer as the primary, + // if any. + if removed_primary { + if let Some(new_primary) = peers_config.light_clients.first() { peers_config.primary = new_primary.peer_id; } - - Ok(options.peer_id) } - fn cmd(&self) -> Result<(), BoxError> { - let options = RmOptions::from_cmd(self).map_err(|e| format!("invalid options: {}", e))?; - let mut config = (*app_config()).clone(); + Ok(peer_id) +} - let removed_peer = Self::update_config(options, &mut config)?; +fn get_all_peer_ids(peers_config: &PeersConfig) -> Vec { + peers_config + .light_clients + .iter() + .map(|c| c.peer_id.to_string()) + .collect() +} - let config_path = crate::config::config_path()?; - relayer::config::store(&config, config_path)?; +fn confirm(chain_id: &ChainId) -> Result { + loop { + print!( + "\n? Do you really want to remove all peers for chain '{}'? (y/n) > ", + chain_id + ); - status_ok!("Removed", "light client peer '{}'", removed_peer); + io::stdout().flush()?; // need to flush stdout since stdout is often line-buffered - Ok(()) - } -} + let mut choice = String::new(); + io::stdin().read_line(&mut choice)?; -impl Runnable for RmCmd { - fn run(&self) { - self.cmd() - .unwrap_or_else(|e| fatal_error(app_reader().deref(), &*e)) + match choice.trim_end() { + "y" | "yes" => return Ok(true), + "n" | "no" => return Ok(false), + _ => continue, + } } } diff --git a/relayer/src/config.rs b/relayer/src/config.rs index c8a943be43..c7e9fef16d 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -219,7 +219,7 @@ pub fn parse(path: impl AsRef) -> Result { /// Serialize the given `Config` as TOML to the given config file. pub fn store(config: &Config, path: impl AsRef) -> Result<(), error::Error> { let mut file = if path.as_ref().exists() { - fs::OpenOptions::new().write(true).open(path) + fs::OpenOptions::new().write(true).truncate(true).open(path) } else { File::create(path) }