Skip to content
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
56 changes: 26 additions & 30 deletions crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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::<String>("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;
Expand Down Expand Up @@ -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::<String>("name").unwrap_or_else(|| {
output::die("App name (--name) is required");
});
Expand All @@ -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::<String>("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 {
Expand All @@ -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::<i64>().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 <app name>#<run number>)");

let line = format!("Run number is required. Example: tower apps {} <app name>#<run number>", subcmd);
output::die(&line);
}
let line = format!("App slug is required. Example: tower apps {} <app name>#<run number>", 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 <app name>#<run number>) is required");

let line = format!("App slug is required. Example: tower apps {} <app name>", subcmd);
output::die(&line);
}
27 changes: 9 additions & 18 deletions crates/tower-cmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
70 changes: 33 additions & 37 deletions crates/tower-cmd/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use tower_api::{
use crate::{
output,
api,
util::cmd,
};

pub fn secrets_cmd() -> Command {
Expand Down Expand Up @@ -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::<bool>("show").unwrap_or(&false);
let env = args.get_one::<String>("environment").unwrap();
let all = args.get_one::<bool>("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(),
Expand All @@ -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(),
Expand All @@ -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::<String>("name").unwrap();
let environment = args.get_one::<String>("environment").unwrap();
let value = args.get_one::<String>("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();

Expand All @@ -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::<String>("name").unwrap();
let environment = args
.get_one::<String>("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();
Expand Down Expand Up @@ -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 {} <environment>/<secret name>", subcmd);
output::die(&line);
}

let line = format!("Secret name and environment is required. Example: tower secrets {} <environment>/<secret name>", subcmd);
output::die(&line);
}
34 changes: 17 additions & 17 deletions crates/tower-cmd/src/teams.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{value_parser, Arg, ArgMatches, Command};
use clap::{ArgMatches, Command};
use colored::*;
use config::Config;

Expand All @@ -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),
),
)
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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::<String>("team_slug")
.map(|s| s.as_str())
.unwrap_or_else(|| {
output::die("Team Slug (e.g. tower teams switch <team_slug>) 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));
}
Expand All @@ -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 {} <team name>", subcmd);
output::die(&line);
}
18 changes: 18 additions & 0 deletions crates/tower-cmd/src/util/cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::output;
use clap::ArgMatches;

pub fn get_string_flag(args: &ArgMatches, name: &str) -> String {
args.get_one::<String>(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::<bool>(name)
.unwrap_or_else(|| {
output::die(&format!("{} is required", name));
})
.to_owned()
}
1 change: 1 addition & 0 deletions crates/tower-cmd/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod apps;
pub mod deploy;
pub mod progress;
pub mod cmd;
Loading