diff --git a/Cargo.lock b/Cargo.lock index fb91072..28e29f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,6 +926,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "comfy-table" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab77dbd8adecaf3f0db40581631b995f312a8a5ae3aa9993188bb8f23d83a5b" +dependencies = [ + "crossterm 0.26.1", + "strum 0.24.1", + "strum_macros", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.3.0" @@ -1038,6 +1050,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -2496,7 +2524,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c" dependencies = [ - "crossterm", + "crossterm 0.25.0", "qrcode", ] @@ -2844,6 +2872,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -2899,7 +2933,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum", + "strum 0.25.0", "thiserror", "time", "tracing", @@ -3537,12 +3571,31 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3592,9 +3645,10 @@ dependencies = [ [[package]] name = "tfcli" -version = "0.2.0" +version = "0.2.1" dependencies = [ "clap", + "comfy-table", "dirs", "ipnet", "qr2term", @@ -4003,6 +4057,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/tfcli/Cargo.toml b/tfcli/Cargo.toml index 7b4fb23..8fcf1ce 100644 --- a/tfcli/Cargo.toml +++ b/tfcli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tfcli" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "Command-line client for managing trifid-api" license = "GPL-3.0-or-later" @@ -20,3 +20,4 @@ dirs = "5" qr2term = "0.3" ipnet = "2.7" serde_json = "1" +comfy-table = "7" diff --git a/tfcli/src/main.rs b/tfcli/src/main.rs index adb6529..e3cc317 100644 --- a/tfcli/src/main.rs +++ b/tfcli/src/main.rs @@ -1,7 +1,8 @@ use std::error::Error; +use std::fmt::{Display, Formatter}; use std::fs; use std::net::{Ipv4Addr, SocketAddrV4}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use ipnet::Ipv4Net; use url::Url; use crate::account::account_main; @@ -93,7 +94,10 @@ pub enum AccountCommands { #[derive(Subcommand, Debug)] pub enum NetworkCommands { /// List all networks associated with your trifid account. - List {}, + List { + #[clap(short = 'T', long, default_value_t = TableStyle::Basic)] + table_style: TableStyle + }, /// Lookup a specific network by ID. Lookup { #[clap(short, long)] @@ -123,7 +127,10 @@ pub enum RoleCommands { rules_json: String }, /// List all roles attached to your organization - List {}, + List { + #[clap(short = 'T', long, default_value_t = TableStyle::Basic)] + table_style: TableStyle + }, /// Lookup a specific role by it's ID Lookup { #[clap(short, long)] @@ -240,6 +247,27 @@ pub enum HostOverrideCommands { } } +#[derive(Debug, Clone, ValueEnum)] +pub enum TableStyle { + List, + Basic, + Pretty +} +impl Default for TableStyle { + fn default() -> Self { + Self::Basic + } +} +impl Display for TableStyle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::List => write!(f, "list"), + Self::Basic => write!(f, "basic"), + Self::Pretty => write!(f, "pretty") + } + } +} + #[tokio::main] async fn main() { match main2().await { diff --git a/tfcli/src/network.rs b/tfcli/src/network.rs index 6a59574..e4db499 100644 --- a/tfcli/src/network.rs +++ b/tfcli/src/network.rs @@ -1,13 +1,16 @@ use std::error::Error; use std::fs; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::presets::UTF8_FULL; +use comfy_table::Table; use serde::Deserialize; use url::Url; use crate::api::APIErrorResponse; -use crate::NetworkCommands; +use crate::{NetworkCommands, TableStyle}; pub async fn network_main(command: NetworkCommands, server: Url) -> Result<(), Box> { match command { - NetworkCommands::List {} => list_networks(server).await, + NetworkCommands::List { table_style } => list_networks(server, table_style).await, NetworkCommands::Lookup {id} => get_network(id, server).await } } @@ -32,7 +35,7 @@ pub struct Network { pub name: String } -pub async fn list_networks(server: Url) -> Result<(), Box> { +pub async fn list_networks(server: Url, table_style: TableStyle) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token @@ -48,20 +51,39 @@ pub async fn list_networks(server: Url) -> Result<(), Box> { if res.status().is_success() { let resp: NetworkListResp = res.json().await?; - for network in &resp.data { - println!(" Network: {}", network.id); - println!(" CIDR: {}", network.cidr); - println!(" Organization: {}", network.organization_id); - println!(" Signing CA: {}", network.signing_ca_id); - println!("Dedicated Relays: {}", !network.lighthouses_as_relays); - println!(" Name: {}", network.name); - println!(" Created At: {}", network.created_at); - println!(); - } - if resp.data.is_empty() { println!("No networks found"); + return Ok(()); } + + if matches!(table_style, TableStyle::List) { + for network in &resp.data { + println!(" Network: {}", network.id); + println!(" CIDR: {}", network.cidr); + println!(" Organization: {}", network.organization_id); + println!(" Signing CA: {}", network.signing_ca_id); + println!("Dedicated Relays: {}", !network.lighthouses_as_relays); + println!(" Name: {}", network.name); + println!(" Created At: {}", network.created_at); + println!(); + } + return Ok(()); + } + + let mut table = Table::new(); + match table_style { + TableStyle::List => unreachable!(), + TableStyle::Basic => (), + TableStyle::Pretty => { table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS) ; }, + }; + + table.set_header(vec!["ID", "Name", "CIDR", "Organization ID", "Signing CA ID", "Dedicated Relays", "Created At"]); + + for network in &resp.data { + table.add_row(vec![&network.id, &network.name, &network.cidr, &network.organization_id, &network.signing_ca_id, (!network.lighthouses_as_relays).to_string().as_str(), &network.created_at]); + } + + println!("{table}"); } else { let resp: APIErrorResponse = res.json().await?; diff --git a/tfcli/src/role.rs b/tfcli/src/role.rs index a6ffd9c..441b1f9 100644 --- a/tfcli/src/role.rs +++ b/tfcli/src/role.rs @@ -1,13 +1,16 @@ use std::error::Error; use std::fs; +use comfy_table::modifiers::UTF8_ROUND_CORNERS; +use comfy_table::presets::UTF8_FULL; +use comfy_table::Table; use serde::{Deserialize, Serialize}; use url::Url; use crate::api::APIErrorResponse; -use crate::{RoleCommands}; +use crate::{RoleCommands, TableStyle}; pub async fn role_main(command: RoleCommands, server: Url) -> Result<(), Box> { match command { - RoleCommands::List {} => list_roles(server).await, + RoleCommands::List { table_style } => list_roles(server, table_style).await, RoleCommands::Lookup {id} => get_role(id, server).await, RoleCommands::Create { name, description, rules_json } => create_role(name, description, rules_json, server).await, RoleCommands::Delete { id } => delete_role(id, server).await, @@ -47,7 +50,7 @@ pub struct RoleFirewallRulePortRange { pub to: u16 } -pub async fn list_roles(server: Url) -> Result<(), Box> { +pub async fn list_roles(server: Url, table_style: TableStyle) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token @@ -63,22 +66,40 @@ pub async fn list_roles(server: Url) -> Result<(), Box> { if res.status().is_success() { let resp: RoleListResp = res.json().await?; - for role in &resp.data { - println!(" Role: {} ({})", role.name, role.id); - println!(" Description: {}", role.description); - for rule in &role.firewall_rules { - println!("Rule Description: {}", rule.description); - println!(" Allowed Role: {}", rule.allowed_role_id.as_ref().unwrap_or(&"All roles".to_string())); - println!(" Protocol: {}", rule.protocol); - println!(" Port Range: {}", if let Some(pr) = rule.port_range.as_ref() { format!("{}-{}", pr.from, pr.to) } else { "Any".to_string() }); - } - println!(" Created: {}", role.created_at); - println!(" Updated: {}", role.modified_at); - } - if resp.data.is_empty() { println!("No roles found"); + return Ok(()); } + + if matches!(table_style, TableStyle::List) { + for role in &resp.data { + println!(" Role: {} ({})", role.name, role.id); + println!(" Description: {}", role.description); + for rule in &role.firewall_rules { + println!("Rule Description: {}", rule.description); + println!(" Allowed Role: {}", rule.allowed_role_id.as_ref().unwrap_or(&"All roles".to_string())); + println!(" Protocol: {}", rule.protocol); + println!(" Port Range: {}", if let Some(pr) = rule.port_range.as_ref() { format!("{}-{}", pr.from, pr.to) } else { "Any".to_string() }); + } + println!(" Created: {}", role.created_at); + println!(" Updated: {}", role.modified_at); + } + return Ok(()); + } + + let mut table = Table::new(); + match table_style { + TableStyle::List => unreachable!(), + TableStyle::Basic => (), + TableStyle::Pretty => { table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); }, + }; + + table.set_header(vec!["ID", "Name", "Description", "Rule Count", "Created", "Updated"]); + for role in &resp.data { + table.add_row(vec![&role.id, &role.name, &role.description, role.firewall_rules.len().to_string().as_str(), &role.created_at, &role.modified_at]); + } + + println!("{table}"); } else { let resp: APIErrorResponse = res.json().await?;