testtool: add tool for integration tests

Summary:
Add a new tool `mononoke-testtool`, which will be used for commands that make
integration tests easier to write.

Since this tool is expected to only be used for tests, it will detect
production configurations and refuse to run.

Reviewed By: yancouto

Differential Revision: D33854930

fbshipit-source-id: c07cb73c726c8c60ef7a94a704f485fe9fc2576d
This commit is contained in:
Mark Juggurnauth-Thomas 2022-02-02 04:24:00 -08:00 committed by Facebook GitHub Bot
parent 0706eff178
commit 2f338593de
9 changed files with 172 additions and 56 deletions

View File

@ -27,10 +27,11 @@ use slog::Logger;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use tokio::runtime::Handle;
use crate::args::{ConfigArgs, RepoArg, RepoArgs, RepoBlobstoreArgs};
use crate::args::{ConfigArgs, ConfigMode, RepoArg, RepoArgs, RepoBlobstoreArgs};
pub struct MononokeApp {
pub fb: FacebookInit,
config_mode: ConfigMode,
args: ArgMatches,
env: Arc<MononokeEnvironment>,
storage_configs: StorageConfigs,
@ -41,6 +42,7 @@ pub struct MononokeApp {
impl MononokeApp {
pub(crate) fn new(
fb: FacebookInit,
config_mode: ConfigMode,
args: ArgMatches,
env: MononokeEnvironment,
) -> Result<Self> {
@ -55,6 +57,7 @@ impl MononokeApp {
Ok(MononokeApp {
fb,
config_mode,
args,
env,
storage_configs,
@ -129,6 +132,11 @@ impl MononokeApp {
&self.env
}
/// Returns true if this is a production configuration of Mononoke
pub fn is_production(&self) -> bool {
self.config_mode == ConfigMode::Production
}
/// Get repo config based on user-provided arguments.
pub fn repo_config(&self, repo_args: &RepoArgs) -> Result<(String, RepoConfig)> {
match repo_args.id_or_name()? {

View File

@ -34,8 +34,10 @@ pub struct ConfigArgs {
pub crypto_path_regex: Option<Vec<String>>,
}
const PRODUCTION_PREFIX: &str = "configerator://scm/mononoke/repos/tiers/";
fn configerator_config_path(tier: &str) -> String {
format!("configerator://scm/mononoke/repos/tiers/{}", tier)
format!("{}{}", PRODUCTION_PREFIX, tier)
}
impl ConfigArgs {
@ -50,4 +52,25 @@ impl ConfigArgs {
String::new()
}
}
pub fn mode(&self) -> ConfigMode {
if let Some(config_path) = &self.config_path {
// Any configuration that matches the production prefix is prod.
if config_path.starts_with(PRODUCTION_PREFIX) {
return ConfigMode::Production;
}
} else {
// Otherwise, we are prod if a prod tier is requested.
if self.prod || self.config_tier.is_some() {
return ConfigMode::Production;
}
}
ConfigMode::Development
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ConfigMode {
Production,
Development,
}

View File

@ -19,7 +19,7 @@ mod tunables;
pub use self::tunables::TunablesArgs;
pub use changeset::ChangesetArgs;
pub use config::ConfigArgs;
pub use config::{ConfigArgs, ConfigMode};
pub use hooks::HooksArgs;
pub use mysql::MysqlArgs;
pub use repo::{RepoArg, RepoArgs};

View File

@ -180,13 +180,14 @@ impl MononokeAppBuilder {
let args = app.get_matches();
let env_args = EnvironmentArgs::from_arg_matches(&args)?;
let config_mode = env_args.config_args.mode();
let mut env = self.build_environment(env_args)?;
for ext in self.arg_extensions.iter() {
ext.process_args(&args, &mut env)?;
}
MononokeApp::new(self.fb, args, env)
MononokeApp::new(self.fb, config_mode, args, env)
}
fn build_environment(&self, env_args: EnvironmentArgs) -> Result<MononokeEnvironment> {

View File

@ -13,3 +13,84 @@ mod extension;
pub use app::MononokeApp;
pub use builder::MononokeAppBuilder;
pub use extension::ArgExtension;
#[doc(hidden)]
pub mod macro_export {
pub use anyhow;
pub use clap;
pub use heck;
}
/// Define subcommands for a Mononoke App
///
/// This macro is a convenience macro for defining a Mononoke App with
/// subcommands, where each subcommand gets its own module.
///
/// To use this:
///
/// * Create `commands.rs` with a call to this macro. List each module
/// that defines a subcommand, which should be placed in a `commands`
/// directory.
///
/// ```
/// use mononoke_app::subcommands;
///
/// subcommands! {
/// mod first_command;
/// mod second_command;
/// }
/// ```
///
/// * Each command should provide two things at the root of its module:
///
/// * A `CommandArgs` type which should derive `clap::Parser` and represents
/// the options available to this subcommand.
///
/// * A function to run the command, with the signature:
/// `pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()>`.
///
/// * This macro gathers these together and provides a `subcommands` function
/// and a `dispatch` function.
///
/// * Call `commands::subcommands()` to construct the subcommands and pass
/// them to `MononokeAppBuilder::build_with_subcommands` for your app.
///
/// * When executing the app, call `commands::dispatch(app)` to run the
/// selected subcommand.
#[macro_export]
macro_rules! subcommands {
( $( mod $command:ident; )* ) => {
$( mod $command; )*
/// Add args for all commands.
pub(crate) fn subcommands<'help>() -> Vec<$crate::macro_export::clap::App<'help>> {
use $crate::macro_export::clap::IntoApp;
use $crate::macro_export::heck::KebabCase;
vec![
$(
$command::CommandArgs::into_app()
.name(stringify!($command).to_kebab_case()),
)*
]
}
/// Dispatch a command invocation.
pub(crate) async fn dispatch(app: $crate::MononokeApp) -> $crate::macro_export::anyhow::Result<()> {
use $crate::macro_export::clap::FromArgMatches;
use $crate::macro_export::heck::SnakeCase;
if let Some((name, matches)) = app.subcommand() {
match name.to_snake_case().as_str() {
$(
stringify!($command) => {
let args = $command::CommandArgs::from_arg_matches(matches)?;
$command::run(app, args).await
}
)*
_ => Err($crate::macro_export::anyhow::anyhow!("unrecognised subcommand: {}", name)),
}
} else {
Err($crate::macro_export::anyhow::anyhow!("no subcommand specified"))
}
}
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
mononoke_app::subcommands! {
mod blobstore;
mod blobstore_unlink;
mod fetch;
mod list_repos;
mod repo_info;
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
use anyhow::{anyhow, Result};
use clap::{App, FromArgMatches, IntoApp};
use heck::{KebabCase, SnakeCase};
use mononoke_app::MononokeApp;
macro_rules! commands {
( $( mod $command:ident; )* ) => {
$( mod $command; )*
/// Add args for all commands.
pub(crate) fn subcommands<'help>() -> Vec<App<'help>> {
vec![
$(
$command::CommandArgs::into_app()
.name(stringify!($command).to_kebab_case()),
)*
]
}
/// Dispatch a command invocation.
pub(crate) async fn dispatch(app: MononokeApp) -> Result<()> {
if let Some((name, matches)) = app.subcommand() {
match name.to_snake_case().as_str() {
$(
stringify!($command) => {
let args = $command::CommandArgs::from_arg_matches(matches)?;
$command::run(app, args).await
}
)*
_ => Err(anyhow!("unrecognised subcommand: {}", name)),
}
} else {
Err(anyhow!("no subcommand specified"))
}
}
}
}
commands! {
mod blobstore;
mod blobstore_unlink;
mod fetch;
mod list_repos;
mod repo_info;
}

View File

@ -0,0 +1,8 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
mononoke_app::subcommands! {}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
use anyhow::{anyhow, Result};
use clap::Parser;
use fbinit::FacebookInit;
use mononoke_app::{MononokeApp, MononokeAppBuilder};
mod commands;
/// Tools for Mononoke Tests
#[derive(Parser)]
struct TestToolArgs {}
#[fbinit::main]
fn main(fb: FacebookInit) -> Result<()> {
let subcommands = commands::subcommands();
let app = MononokeAppBuilder::new(fb).build_with_subcommands::<TestToolArgs>(subcommands)?;
app.run(async_main)
}
async fn async_main(app: MononokeApp) -> Result<()> {
if app.is_production() {
return Err(anyhow!(
"mononoke-testtool cannot be run against production"
));
}
commands::dispatch(app).await
}