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
66 changes: 66 additions & 0 deletions crates/tower-cmd/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,50 @@ pub async fn export_catalogs(
.await
}

pub async fn list_catalogs(
config: &Config,
env: &str,
all: bool,
) -> Result<
tower_api::models::ListCatalogsResponse,
Error<tower_api::apis::default_api::ListCatalogsError>,
> {
let api_config = &config.into();

let params = tower_api::apis::default_api::ListCatalogsParams {
environment: Some(env.to_string()),
all: Some(all),
page: None,
page_size: None,
};

unwrap_api_response(tower_api::apis::default_api::list_catalogs(
api_config, params,
))
.await
}

pub async fn describe_catalog(
config: &Config,
name: &str,
env: &str,
) -> Result<
tower_api::models::DescribeCatalogResponse,
Error<tower_api::apis::default_api::DescribeCatalogError>,
> {
let api_config = &config.into();

let params = tower_api::apis::default_api::DescribeCatalogParams {
name: name.to_string(),
environment: Some(env.to_string()),
};

unwrap_api_response(tower_api::apis::default_api::describe_catalog(
api_config, params,
))
.await
}

pub async fn list_secrets(
config: &Config,
env: &str,
Expand Down Expand Up @@ -609,6 +653,28 @@ impl ResponseEntity for tower_api::apis::default_api::ExportCatalogsSuccess {
}
}

impl ResponseEntity for tower_api::apis::default_api::ListCatalogsSuccess {
type Data = tower_api::models::ListCatalogsResponse;

fn extract_data(self) -> Option<Self::Data> {
match self {
Self::Status200(data) => Some(data),
Self::UnknownValue(_) => None,
}
}
}

impl ResponseEntity for tower_api::apis::default_api::DescribeCatalogSuccess {
type Data = tower_api::models::DescribeCatalogResponse;

fn extract_data(self) -> Option<Self::Data> {
match self {
Self::Status200(data) => Some(data),
Self::UnknownValue(_) => None,
}
}
}

impl ResponseEntity for tower_api::apis::default_api::CreateSecretSuccess {
type Data = tower_api::models::CreateSecretResponse;

Expand Down
2 changes: 1 addition & 1 deletion crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub async fn do_show(config: Config, cmd: &ArgMatches) {

let headers = vec!["#", "Status", "Start Time", "Elapsed Time"]
.into_iter()
.map(|h| h.to_string())
.map(str::to_string)
.collect();

let rows = runs
Expand Down
189 changes: 189 additions & 0 deletions crates/tower-cmd/src/catalogs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use clap::{value_parser, Arg, ArgMatches, Command};
use colored::Colorize;
use config::Config;

use crate::{api, output, util::cmd};

pub fn catalogs_cmd() -> Command {
Command::new("catalogs")
.about("Interact with the catalogs in your Tower account")
.arg_required_else_help(true)
.subcommand(
Command::new("list")
.arg(
Arg::new("environment")
.short('e')
.long("environment")
.default_value("default")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the right thing to do here. I guess that's what default is for?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure yeah, looks like we have it everywhere we have this argument

.value_parser(value_parser!(String))
.help("List catalogs in this environment")
.action(clap::ArgAction::Set),
)
.arg(
Arg::new("all")
.short('a')
.long("all")
.help("List catalogs across all environments")
.action(clap::ArgAction::SetTrue),
)
.about("List all of your catalogs"),
)
.subcommand(
Command::new("show")
.arg(
Arg::new("catalog_name")
.value_parser(value_parser!(String))
.index(1)
.required(true)
.help("Name of the catalog"),
)
.arg(
Arg::new("environment")
.short('e')
.long("environment")
.default_value("default")
.value_parser(value_parser!(String))
.help("Environment the catalog belongs to")
.action(clap::ArgAction::Set),
)
.about("Show the details of a catalog, including its property names"),
)
}

pub async fn do_list(config: Config, args: &ArgMatches) {
let all = cmd::get_bool_flag(args, "all");
let env = cmd::get_string_flag(args, "environment");

let list_response =
output::with_spinner("Listing catalogs", api::list_catalogs(&config, &env, all)).await;

let headers = vec!["Name", "Type", "Environment"]
.into_iter()
.map(str::to_string)
.collect();
let data = list_response
.catalogs
.iter()
.map(|catalog| {
vec![
catalog.name.clone(),
catalog.r#type.clone(),
catalog.environment.clone(),
]
})
.collect();
output::table(headers, data, Some(&list_response.catalogs));
}

pub async fn do_show(config: Config, args: &ArgMatches) {
let name = args
.get_one::<String>("catalog_name")
.expect("catalog_name is required");
let env = cmd::get_string_flag(args, "environment");

match api::describe_catalog(&config, name, &env).await {
Ok(response) => {
if output::get_output_mode().is_json() {
output::json(&response);
return;
}

let catalog = &response.catalog;

output::detail("Catalog", &catalog.name);
output::detail("Type", &catalog.r#type);
output::detail("Environment", &catalog.environment);

if !catalog.properties.is_empty() {
output::newline();
output::header("Properties");

let headers = vec!["Name", "Preview"]
.into_iter()
.map(str::to_string)
.collect();
let data = catalog
.properties
.iter()
.map(|prop| {
vec![
prop.name.clone(),
prop.preview.dimmed().to_string(),
]
})
.collect();
output::table(headers, data, Some(&response));
}
}
Err(err) => output::tower_error_and_die(err, "Fetching catalog details failed"),
}
}

#[cfg(test)]
mod tests {
use super::catalogs_cmd;

#[test]
fn list_defaults_to_default_environment() {
let matches = catalogs_cmd()
.try_get_matches_from(["catalogs", "list"])
.expect("list should parse with no args");

let (_, list_args) = matches.subcommand().expect("expected list subcommand");

assert_eq!(list_args.get_one::<String>("environment").unwrap(), "default");
assert_eq!(list_args.get_one::<bool>("all").copied(), Some(false));
}

#[test]
fn list_accepts_environment_flag() {
let matches = catalogs_cmd()
.try_get_matches_from(["catalogs", "list", "-e", "production"])
.expect("list -e should parse");

let (_, list_args) = matches.subcommand().expect("expected list subcommand");

assert_eq!(list_args.get_one::<String>("environment").unwrap(), "production");
}

#[test]
fn list_accepts_all_flag() {
let matches = catalogs_cmd()
.try_get_matches_from(["catalogs", "list", "--all"])
.expect("list --all should parse");

let (_, list_args) = matches.subcommand().expect("expected list subcommand");

assert_eq!(list_args.get_one::<bool>("all").copied(), Some(true));
}

#[test]
fn show_requires_catalog_name() {
let result = catalogs_cmd().try_get_matches_from(["catalogs", "show"]);
assert!(result.is_err());
}

#[test]
fn show_accepts_catalog_name() {
let matches = catalogs_cmd()
.try_get_matches_from(["catalogs", "show", "my-catalog"])
.expect("show with name should parse");

let (_, show_args) = matches.subcommand().expect("expected show subcommand");

assert_eq!(show_args.get_one::<String>("catalog_name").unwrap(), "my-catalog");
assert_eq!(show_args.get_one::<String>("environment").unwrap(), "default");
}

#[test]
fn show_accepts_environment_override() {
let matches = catalogs_cmd()
.try_get_matches_from(["catalogs", "show", "my-catalog", "-e", "production"])
.expect("show with -e should parse");

let (_, show_args) = matches.subcommand().expect("expected show subcommand");

assert_eq!(show_args.get_one::<String>("catalog_name").unwrap(), "my-catalog");
assert_eq!(show_args.get_one::<String>("environment").unwrap(), "production");
}
}
6 changes: 1 addition & 5 deletions crates/tower-cmd/src/environments.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use clap::{value_parser, Arg, ArgMatches, Command};
use colored::Colorize;
use config::Config;

use crate::{api, output};
Expand All @@ -26,10 +25,7 @@ pub fn environments_cmd() -> Command {
pub async fn do_list(config: Config) {
let resp = output::with_spinner("Listing environments", api::list_environments(&config)).await;

let headers = vec!["Name"]
.into_iter()
.map(|h| h.yellow().to_string())
.collect();
let headers = vec!["Name".to_string()];

let envs_data: Vec<Vec<String>> = resp
.environments
Expand Down
14 changes: 14 additions & 0 deletions crates/tower-cmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use config::{Config, Session};

pub mod api;
mod apps;
mod catalogs;
mod deploy;
mod environments;
pub mod error;
Expand Down Expand Up @@ -133,6 +134,18 @@ impl App {
}
}
}
Some(("catalogs", sub_matches)) => {
let catalogs_command = sub_matches.subcommand();

match catalogs_command {
Some(("list", args)) => catalogs::do_list(sessionized_config, args).await,
Some(("show", args)) => catalogs::do_show(sessionized_config, args).await,
_ => {
catalogs::catalogs_cmd().print_help().unwrap();
std::process::exit(2);
}
}
}
Some(("secrets", sub_matches)) => {
let secrets_command = sub_matches.subcommand();

Expand Down Expand Up @@ -232,6 +245,7 @@ fn root_cmd() -> Command {
.arg_required_else_help(false)
.subcommand(session::login_cmd())
.subcommand(apps::apps_cmd())
.subcommand(catalogs::catalogs_cmd())
.subcommand(schedules::schedules_cmd())
.subcommand(secrets::secrets_cmd())
.subcommand(environments::environments_cmd())
Expand Down
2 changes: 1 addition & 1 deletion crates/tower-cmd/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ pub fn table<T: Serialize>(headers: Vec<String>, data: Vec<Vec<String>>, json_da
.table()
.border(Border::builder().build())
.separator(separator)
.title(headers.iter().map(|h| h.yellow().to_string()));
.title(headers.iter().map(|h| h.bold().yellow().to_string()));

if let Err(err) = print_stdout(table) {
if err.kind() == io::ErrorKind::BrokenPipe {
Expand Down
11 changes: 4 additions & 7 deletions crates/tower-cmd/src/schedules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,10 @@ pub async fn do_list(config: Config, args: &ArgMatches) {
return;
}

let headers = vec![
"ID".yellow().to_string(),
"App".yellow().to_string(),
"Environment".yellow().to_string(),
"Cron".yellow().to_string(),
"Status".yellow().to_string(),
];
let headers = vec!["ID", "App", "Environment", "Cron", "Status"]
.into_iter()
.map(str::to_string)
.collect();

let rows: Vec<Vec<String>> = response
.schedules
Expand Down
18 changes: 8 additions & 10 deletions crates/tower-cmd/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,10 @@ pub async fn do_list(config: Config, args: &ArgMatches) {
)
.await;

let headers = vec![
"Secret".bold().yellow().to_string(),
"Environment".bold().yellow().to_string(),
"Value".bold().yellow().to_string(),
];
let headers = vec!["Secret", "Environment", "Value"]
.into_iter()
.map(str::to_string)
.collect();
let data = list_response
.secrets
.iter()
Expand All @@ -137,11 +136,10 @@ pub async fn do_list(config: Config, args: &ArgMatches) {
let list_response =
output::with_spinner("Listing secrets", api::list_secrets(&config, &env, all)).await;

let headers = vec![
"Secret".bold().yellow().to_string(),
"Environment".bold().yellow().to_string(),
"Preview".bold().yellow().to_string(),
];
let headers = vec!["Secret", "Environment", "Preview"]
.into_iter()
.map(str::to_string)
.collect();
let data = list_response
.secrets
.iter()
Expand Down
Loading
Loading