diff --git a/crates/tower-cmd/src/apps.rs b/crates/tower-cmd/src/apps.rs index f2072388..bf2de986 100644 --- a/crates/tower-cmd/src/apps.rs +++ b/crates/tower-cmd/src/apps.rs @@ -11,20 +11,12 @@ use crate::{ pub fn apps_cmd() -> Command { Command::new("apps") - .about("Interact with the apps that you own") + .about("Manage the apps in your current Tower account") .arg_required_else_help(true) .subcommand(Command::new("list").about("List all of your apps`")) .subcommand( Command::new("show") .allow_external_subcommands(true) - .arg( - Arg::new("name") - .short('n') - .long("name") - .value_parser(value_parser!(String)) - .required(true) - .action(clap::ArgAction::Set), - ) .about("Show the details about an app in Tower"), ) .subcommand( @@ -62,20 +54,12 @@ pub fn apps_cmd() -> Command { .subcommand( Command::new("delete") .allow_external_subcommands(true) - .arg( - Arg::new("slug") - .short('n') - .long("slug") - .value_parser(value_parser!(String)) - .required(true) - .action(clap::ArgAction::Set), - ) .about("Delete an app in Tower"), ) } -pub async fn do_logs_app(config: Config, cmd: Option<(&str, &ArgMatches)>) { - let (slug, seq) = extract_app_run(cmd); +pub async fn do_logs(config: Config, cmd: &ArgMatches) { + let (slug, seq) = extract_app_slug_and_run("logs", cmd.subcommand()); if let Ok(resp) = api::describe_run_logs(&config, &slug, seq).await { for line in resp.log_lines { @@ -84,10 +68,10 @@ pub async fn do_logs_app(config: Config, cmd: Option<(&str, &ArgMatches)>) { } } -pub async fn do_show_app(config: Config, args: &ArgMatches) { - let slug = args.get_one::("slug").unwrap(); +pub async fn do_show(config: Config, cmd: &ArgMatches) { + let slug = extract_app_slug("show", cmd.subcommand()); - match api::describe_app(&config, slug).await { + match api::describe_app(&config, &slug).await { Ok(app_response) => { let app = app_response.app; let runs = app_response.runs; @@ -201,7 +185,7 @@ pub async fn do_list_apps(config: Config) { } } -pub async fn do_create_app(config: Config, args: &ArgMatches) { +pub async fn do_create(config: Config, args: &ArgMatches) { let name = args.get_one::("name").unwrap_or_else(|| { output::die("App name (--name) is required"); }); @@ -221,11 +205,11 @@ pub async fn do_create_app(config: Config, args: &ArgMatches) { } -pub async fn do_delete_app(config: Config, args: &ArgMatches) { - let slug = args.get_one::("slug").unwrap(); +pub async fn do_delete(config: Config, cmd: &ArgMatches) { + let slug = extract_app_slug("delete", cmd.subcommand()); let mut spinner = output::spinner("Deleting app"); - if let Err(err) = api::delete_app(&config, slug).await { + if let Err(err) = api::delete_app(&config, &slug).await { spinner.failure(); output::tower_error(err); } else { @@ -234,17 +218,29 @@ pub async fn do_delete_app(config: Config, args: &ArgMatches) { } /// Extract app name and run number from command -fn extract_app_run(cmd: Option<(&str, &ArgMatches)>) -> (String, i64) { +fn extract_app_slug_and_run(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> (String, i64) { if let Some((slug, _)) = cmd { if let Some((slug, num)) = slug.split_once('#') { return ( slug.to_string(), num.parse::().unwrap_or_else(|_| { - output::die("Run number must be a valid number"); + output::die("Run number must be an actual number"); }), ); } - output::die("Run number is required (e.g. tower apps logs #)"); + + let line = format!("Run number is required. Example: tower apps {} #", subcmd); + output::die(&line); + } + let line = format!("App slug is required. Example: tower apps {} #", subcmd); + output::die(&line) +} + +fn extract_app_slug(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> String { + if let Some((slug, _)) = cmd { + return slug.to_string(); } - output::die("App name (e.g. tower apps logs #) is required"); + + let line = format!("App slug is required. Example: tower apps {} ", subcmd); + output::die(&line); } diff --git a/crates/tower-cmd/src/lib.rs b/crates/tower-cmd/src/lib.rs index e2b1d27d..3fe628c9 100644 --- a/crates/tower-cmd/src/lib.rs +++ b/crates/tower-cmd/src/lib.rs @@ -25,7 +25,6 @@ impl App { // environment variable. This is for programmatic use cases where we want to test the CLI // in automated environments, for instance. let session = if let Ok(token) = std::env::var("TOWER_JWT") { - // let's exchange the token for a session, what we'll load. Session::from_jwt(&token).ok() } else { Session::from_config_dir().ok() @@ -91,12 +90,10 @@ impl App { match apps_command { Some(("list", _)) => apps::do_list_apps(sessionized_config).await, - Some(("show", args)) => apps::do_show_app(sessionized_config, args).await, - Some(("logs", args)) => { - apps::do_logs_app(sessionized_config, args.subcommand()).await - } - Some(("create", args)) => apps::do_create_app(sessionized_config, args).await, - Some(("delete", args)) => apps::do_delete_app(sessionized_config, args).await, + Some(("create", args)) => apps::do_create(sessionized_config, args).await, + Some(("show", args)) => apps::do_show(sessionized_config, args).await, + Some(("logs", args)) => apps::do_logs(sessionized_config, args).await, + Some(("delete", args)) => apps::do_delete(sessionized_config, args).await, _ => { apps::apps_cmd().print_help().unwrap(); std::process::exit(2); @@ -107,15 +104,9 @@ impl App { let secrets_command = sub_matches.subcommand(); match secrets_command { - Some(("list", args)) => { - secrets::do_list_secrets(sessionized_config, args).await - } - Some(("create", args)) => { - secrets::do_create_secret(sessionized_config, args).await - } - Some(("delete", args)) => { - secrets::do_delete_secret(sessionized_config, args).await - } + Some(("list", args)) => secrets::do_list(sessionized_config, args).await, + Some(("create", args)) => secrets::do_create(sessionized_config, args).await, + Some(("delete", args)) => secrets::do_delete(sessionized_config, args).await, _ => { secrets::secrets_cmd().print_help().unwrap(); std::process::exit(2); @@ -128,8 +119,8 @@ impl App { let teams_command = sub_matches.subcommand(); match teams_command { - Some(("list", _)) => teams::do_list_teams(sessionized_config).await, - Some(("switch", args)) => teams::do_switch_team(sessionized_config, args).await, + Some(("list", _)) => teams::do_list(sessionized_config).await, + Some(("switch", args)) => teams::do_switch(sessionized_config, args).await, _ => { teams::teams_cmd().print_help().unwrap(); std::process::exit(2); diff --git a/crates/tower-cmd/src/secrets.rs b/crates/tower-cmd/src/secrets.rs index 60b26e9e..614a8595 100644 --- a/crates/tower-cmd/src/secrets.rs +++ b/crates/tower-cmd/src/secrets.rs @@ -15,6 +15,7 @@ use tower_api::{ use crate::{ output, api, + util::cmd, }; pub fn secrets_cmd() -> Command { @@ -76,37 +77,21 @@ pub fn secrets_cmd() -> Command { .subcommand( Command::new("delete") .allow_external_subcommands(true) - .arg( - Arg::new("name") - .short('n') - .long("name") - .value_parser(value_parser!(String)) - .required(true) - .help("Name of the secret to delete") - .action(clap::ArgAction::Set), - ) - .arg( - Arg::new("environment") - .short('e') - .long("environment") - .default_value("default") - .value_parser(value_parser!(String)) - .action(clap::ArgAction::Set) - .help("Environment the secret belongs to"), - ) .about("Delete a secret in Tower"), ) } -pub async fn do_list_secrets(config: Config, args: &ArgMatches) { - let show = args.get_one::("show").unwrap_or(&false); - let env = args.get_one::("environment").unwrap(); - let all = args.get_one::("all").unwrap_or(&false); +pub async fn do_list(config: Config, args: &ArgMatches) { + let all = cmd::get_bool_flag(args, "all"); + let show = cmd::get_bool_flag(args, "show"); + let env = cmd::get_string_flag(args, "environment"); + + log::debug!("listing secrets, environment={} all={} show={}", env, all, show); - if *show { + if show { let (private_key, public_key) = crypto::generate_key_pair(); - match api::export_secrets(&config, env, *all, public_key).await { + match api::export_secrets(&config, &env, all, public_key).await { Ok(list_response) => { let headers = vec![ "Secret".bold().yellow().to_string(), @@ -131,7 +116,7 @@ pub async fn do_list_secrets(config: Config, args: &ArgMatches) { Err(err) => output::tower_error(err), } } else { - match api::list_secrets(&config, env, *all).await { + match api::list_secrets(&config, &env, all).await { Ok(list_response) => { let headers = vec![ "Secret".bold().yellow().to_string(), @@ -152,14 +137,14 @@ pub async fn do_list_secrets(config: Config, args: &ArgMatches) { } } -pub async fn do_create_secret(config: Config, args: &ArgMatches) { - let name = args.get_one::("name").unwrap(); - let environment = args.get_one::("environment").unwrap(); - let value = args.get_one::("value").unwrap(); +pub async fn do_create(config: Config, args: &ArgMatches) { + let name = cmd::get_string_flag(args, "name"); + let environment = cmd::get_string_flag(args, "environment"); + let value = cmd::get_string_flag(args, "value"); let mut spinner = output::spinner("Creating secret..."); - match encrypt_and_create_secret(&config, name, value, environment).await { + match encrypt_and_create_secret(&config, &name, &value, &environment).await { Ok(_) => { spinner.success(); @@ -178,16 +163,13 @@ pub async fn do_create_secret(config: Config, args: &ArgMatches) { } } -pub async fn do_delete_secret(config: Config, args: &ArgMatches) { - let env_default = "default".to_string(); - let name = args.get_one::("name").unwrap(); - let environment = args - .get_one::("environment") - .unwrap_or(&env_default); +pub async fn do_delete(config: Config, args: &ArgMatches) { + let (environment, name) = extract_secret_environment_and_name("delete", args.subcommand()); + log::debug!("deleting secret, environment={} name={}", environment, name); let mut spinner = output::spinner("Deleting secret..."); - if let Ok(_) = api::delete_secret(&config, name, environment).await { + if let Ok(_) = api::delete_secret(&config, &name, &environment).await { spinner.success(); } else { spinner.failure(); @@ -232,3 +214,17 @@ async fn encrypt_and_create_secret( } } } + +fn extract_secret_environment_and_name(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> (String, String) { + if let Some((slug, _)) = cmd { + if let Some((env, name)) = slug.split_once('/') { + return (env.to_string(), name.to_string()); + } + + let line = format!("Secret name is required. Example: tower secrets {} /", subcmd); + output::die(&line); + } + + let line = format!("Secret name and environment is required. Example: tower secrets {} /", subcmd); + output::die(&line); +} diff --git a/crates/tower-cmd/src/teams.rs b/crates/tower-cmd/src/teams.rs index 22844d94..cabdae3b 100644 --- a/crates/tower-cmd/src/teams.rs +++ b/crates/tower-cmd/src/teams.rs @@ -1,4 +1,4 @@ -use clap::{value_parser, Arg, ArgMatches, Command}; +use clap::{ArgMatches, Command}; use colored::*; use config::Config; @@ -14,12 +14,8 @@ pub fn teams_cmd() -> Command { .subcommand(Command::new("list").about("List teams you belong to")) .subcommand( Command::new("switch") + .allow_external_subcommands(true) .about("Switch context to a different team") - .arg( - Arg::new("team_slug") - .value_parser(value_parser!(String)) - .action(clap::ArgAction::Set), - ), ) } @@ -60,7 +56,7 @@ async fn refresh_session(config: &Config) -> config::Session { } } -pub async fn do_list_teams(config: Config) { +pub async fn do_list(config: Config) { // Refresh the session and get the updated data let session = refresh_session(&config).await; @@ -121,24 +117,19 @@ pub async fn do_list_teams(config: Config) { output::newline(); } -pub async fn do_switch_team(config: Config, args: &ArgMatches) { - let team_slug = args - .get_one::("team_slug") - .map(|s| s.as_str()) - .unwrap_or_else(|| { - output::die("Team Slug (e.g. tower teams switch ) is required"); - }); +pub async fn do_switch(config: Config, args: &ArgMatches) { + let slug = extract_team_slug("switch", args.subcommand()); // Refresh the session first to ensure we have the latest teams data let session = refresh_session(&config).await; // Check if the provided team slug exists in the refreshed session - let team = session.teams.iter().find(|team| team.slug == team_slug); + let team = session.teams.iter().find(|team| team.slug == slug); match team { Some(team) => { // Team found, set it as active - match config.set_active_team_by_slug(team_slug) { + match config.set_active_team_by_slug(&slug) { Ok(_) => { output::success(&format!("Switched to team: {}", team.name)); } @@ -152,9 +143,18 @@ pub async fn do_switch_team(config: Config, args: &ArgMatches) { // Team not found output::failure(&format!( "Team '{}' not found. Use 'tower teams list' to see all your teams.", - team_slug + slug )); std::process::exit(1); } } } + +fn extract_team_slug(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> String { + if let Some((slug, _)) = cmd { + return slug.to_string(); + } + + let line = format!("Team slug is required. Example: tower teams {} ", subcmd); + output::die(&line); +} diff --git a/crates/tower-cmd/src/util/cmd.rs b/crates/tower-cmd/src/util/cmd.rs new file mode 100644 index 00000000..49148410 --- /dev/null +++ b/crates/tower-cmd/src/util/cmd.rs @@ -0,0 +1,18 @@ +use crate::output; +use clap::ArgMatches; + +pub fn get_string_flag(args: &ArgMatches, name: &str) -> String { + args.get_one::(name) + .unwrap_or_else(|| { + output::die(&format!("{} is required", name)); + }) + .to_string() +} + +pub fn get_bool_flag(args: &ArgMatches, name: &str) -> bool { + args.get_one::(name) + .unwrap_or_else(|| { + output::die(&format!("{} is required", name)); + }) + .to_owned() +} diff --git a/crates/tower-cmd/src/util/mod.rs b/crates/tower-cmd/src/util/mod.rs index 02520da4..23a6bb3f 100644 --- a/crates/tower-cmd/src/util/mod.rs +++ b/crates/tower-cmd/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod apps; pub mod deploy; pub mod progress; +pub mod cmd;