From 785699cdd63ddcf9d4c24e30164332e28524422a Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Wed, 3 Jun 2026 17:54:58 -0400 Subject: [PATCH 1/4] Add visibility for egress --- tvc/src/client.rs | 17 ++++++++++++++ tvc/src/commands/app/create.rs | 14 ++++++++++-- tvc/src/commands/app/list.rs | 32 ++++++++++++++++++++++----- tvc/src/commands/app/status.rs | 5 +++++ tvc/src/commands/deploy/get_status.rs | 5 +++++ tvc/src/commands/deploy/status.rs | 5 +++++ tvc/src/commands/display.rs | 24 ++++++++++++++++++++ tvc/src/commands/mod.rs | 1 + tvc/src/config/app.rs | 13 ++++++++--- 9 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 tvc/src/commands/display.rs diff --git a/tvc/src/client.rs b/tvc/src/client.rs index 5fdf0912..634890ef 100644 --- a/tvc/src/client.rs +++ b/tvc/src/client.rs @@ -4,6 +4,8 @@ use crate::config::turnkey::{Config, StoredApiKey}; use anyhow::{anyhow, bail, Context, Result}; use tracing::debug; use turnkey_api_key_stamper::TurnkeyP256ApiKey; +use turnkey_client::generated::external::data::v1::TvcApp; +use turnkey_client::generated::GetTvcAppRequest; use turnkey_client::TurnkeyClient; /// Number of *required* auth env vars: org_id, api_key_public, api_key_private. @@ -53,6 +55,21 @@ pub async fn build_client() -> Result { build_authed_client(&org_id, &api_base_url, &api_key_public, &api_key_private) } +pub async fn fetch_tvc_app(auth: &AuthenticatedClient, app_id: &str) -> Result { + let response = auth + .client + .get_tvc_app(GetTvcAppRequest { + organization_id: auth.org_id.clone(), + tvc_app_id: app_id.to_string(), + }) + .await + .context("failed to fetch app")?; + + response + .tvc_app + .ok_or_else(|| anyhow!("app not found: {app_id}")) +} + async fn load_credentials_from_config() -> Result<(String, String, String, String)> { let config = Config::load().await?; diff --git a/tvc/src/commands/app/create.rs b/tvc/src/commands/app/create.rs index 782a7e80..3721cba6 100644 --- a/tvc/src/commands/app/create.rs +++ b/tvc/src/commands/app/create.rs @@ -146,7 +146,7 @@ fn build_create_tvc_app_intent(app_config: &AppConfig) -> CreateTvcAppIntent { .map(to_tvc_operator_set_params), share_set_id: app_config.share_set_id.clone(), share_set_params: share_set_params.as_ref().map(to_tvc_operator_set_params), - enable_egress: app_config.enable_egress, + enable_egress: Some(app_config.enable_egress), enable_debug_mode_deployments: app_config.dangerous_enable_debug_mode_deployments.into(), } } @@ -184,7 +184,7 @@ mod tests { AppConfig { name: "test-app".to_string(), quorum_public_key: KNOWN_QUORUM_KEY.to_string(), - enable_egress: Some(false), + enable_egress: false, manifest_set_id: None, manifest_set_params: Some(OperatorSetParams { name: "manifest-set".to_string(), @@ -212,6 +212,16 @@ mod tests { assert!(share_set_params.existing_operator_ids.is_empty()); } + #[test] + fn build_intent_sends_enable_egress() { + let mut config = valid_config(); + config.enable_egress = true; + + let intent = build_create_tvc_app_intent(&config); + + assert_eq!(intent.enable_egress, Some(true)); + } + #[test] fn build_intent_uses_custom_share_set_params_when_configured() { let mut config = valid_config(); diff --git a/tvc/src/commands/app/list.rs b/tvc/src/commands/app/list.rs index 1f98bb6e..04861a37 100644 --- a/tvc/src/commands/app/list.rs +++ b/tvc/src/commands/app/list.rs @@ -49,15 +49,27 @@ fn filter_by_name(apps: &mut Vec, name: Option<&str>) { } fn render_app(app: &TvcApp) { - println!("Name: {}", app.name); - println!("ID: {}", app.id); - println!("Quorum Public Key: {}", app.quorum_public_key); + for line in render_app_lines(app) { + println!("{line}"); + } +} + +fn render_app_lines(app: &TvcApp) -> Vec { let live = app.live_deployment_id.as_deref().unwrap_or("(none)"); - println!("Live Deployment: {live}"); + let mut lines = vec![ + format!("Name: {}", app.name), + format!("ID: {}", app.id), + format!("Quorum Public Key: {}", app.quorum_public_key), + format!("Live Deployment: {live}"), + crate::commands::display::egress_enabled_line(app.enable_egress), + ]; + if !app.public_domain.is_empty() { - println!("Public Domain: {}", app.public_domain); + lines.push(format!("Public Domain: {}", app.public_domain)); } - println!("{}", "─".repeat(40)); + + lines.push("─".repeat(40)); + lines } #[cfg(test)] @@ -145,4 +157,12 @@ mod tests { "(none)" ); } + + #[test] + fn app_render_lines_include_egress_status() { + let mut app = make_app("my-app"); + app.enable_egress = true; + + assert!(render_app_lines(&app).contains(&"Egress Enabled: yes".to_string())); + } } diff --git a/tvc/src/commands/app/status.rs b/tvc/src/commands/app/status.rs index 2d8cd0c6..dcd18fcc 100644 --- a/tvc/src/commands/app/status.rs +++ b/tvc/src/commands/app/status.rs @@ -33,9 +33,14 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .app_status .ok_or_else(|| anyhow!("no status returned for app: {}", args.app_id))?, ); + let app = crate::client::fetch_tvc_app(&auth, &args.app_id).await?; println!("App ID: {}", app_status.app_id); println!("Targeted Deployment: {}", app_status.targeted_deployment_id); + println!( + "{}", + crate::commands::display::egress_enabled_line(app.enable_egress) + ); if app_status.deployments.is_empty() { println!(); diff --git a/tvc/src/commands/deploy/get_status.rs b/tvc/src/commands/deploy/get_status.rs index 6897a026..76879c4e 100644 --- a/tvc/src/commands/deploy/get_status.rs +++ b/tvc/src/commands/deploy/get_status.rs @@ -49,9 +49,14 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .app_status .ok_or_else(|| anyhow!("no status returned for app: {}", deployment.app_id))?, ); + let app = crate::client::fetch_tvc_app(&auth, &deployment.app_id).await?; println!("Deployment: {}", deployment.id); println!("App ID: {}", app_status.app_id); + println!( + "{}", + crate::commands::display::egress_enabled_line(app.enable_egress) + ); println!( "Is Targeted Deployment: {}", if app_status.targeted_deployment_id == args.deploy_id { diff --git a/tvc/src/commands/deploy/status.rs b/tvc/src/commands/deploy/status.rs index c326b705..a4c6bab8 100644 --- a/tvc/src/commands/deploy/status.rs +++ b/tvc/src/commands/deploy/status.rs @@ -37,9 +37,14 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .manifest .as_ref() .ok_or_else(|| anyhow::anyhow!("manifest not found in deployment"))?; + let app = crate::client::fetch_tvc_app(&auth, &deployment.app_id).await?; println!("Deployment: {}", deployment.id); println!("App ID: {}", deployment.app_id); + println!( + "{}", + crate::commands::display::egress_enabled_line(app.enable_egress) + ); println!("Manifest ID: {}", manifest.id); println!("QOS Version: {}", deployment.qos_version); println!("{}", format_marked_for_deletion(&deployment)); diff --git a/tvc/src/commands/display.rs b/tvc/src/commands/display.rs new file mode 100644 index 00000000..dcf388fa --- /dev/null +++ b/tvc/src/commands/display.rs @@ -0,0 +1,24 @@ +//! Shared display helpers for CLI output. + +pub fn yes_no(value: bool) -> &'static str { + if value { + "yes" + } else { + "no" + } +} + +pub fn egress_enabled_line(enable_egress: bool) -> String { + format!("Egress Enabled: {}", yes_no(enable_egress)) +} + +#[cfg(test)] +mod tests { + use super::egress_enabled_line; + + #[test] + fn egress_enabled_line_formats_yes_and_no() { + assert_eq!(egress_enabled_line(true), "Egress Enabled: yes"); + assert_eq!(egress_enabled_line(false), "Egress Enabled: no"); + } +} diff --git a/tvc/src/commands/mod.rs b/tvc/src/commands/mod.rs index 4cba7dd1..de2f7376 100644 --- a/tvc/src/commands/mod.rs +++ b/tvc/src/commands/mod.rs @@ -8,5 +8,6 @@ pub mod app; pub mod app_status; pub mod confirmation; pub mod deploy; +pub mod display; pub mod keys; pub mod login; diff --git a/tvc/src/config/app.rs b/tvc/src/config/app.rs index 0551b1dc..88aa973d 100644 --- a/tvc/src/config/app.rs +++ b/tvc/src/config/app.rs @@ -13,7 +13,7 @@ pub struct AppConfig { pub name: String, pub quorum_public_key: String, #[serde(default)] - pub enable_egress: Option, + pub enable_egress: bool, #[serde(default)] pub manifest_set_id: Option, #[serde(default)] @@ -63,7 +63,7 @@ impl AppConfig { Self { name: "".to_string(), quorum_public_key: KNOWN_QUORUM_KEY.to_string(), - enable_egress: Some(false), + enable_egress: false, manifest_set_id: None, manifest_set_params: Some(OperatorSetParams { name: "".to_string(), @@ -224,7 +224,14 @@ mod tests { json["enableEgress"] = json!(true); let config: AppConfig = serde_json::from_value(json).unwrap(); - assert_eq!(config.enable_egress, Some(true)); + assert!(config.enable_egress); + } + + #[test] + fn config_defaults_enable_egress_to_false() { + let config: AppConfig = serde_json::from_value(valid_config_json()).unwrap(); + + assert!(!config.enable_egress); } #[test] From d564057e40dd6f66ec7b442638c6e5e1ebbc7079 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Mon, 15 Jun 2026 14:22:24 -0400 Subject: [PATCH 2/4] Address review feedback: unify egress display helper - Rename egress_enabled_line -> format_egress_enabled as the single canonical helper, mirroring format_marked_for_deletion structure - Refactor format_marked_for_deletion to use shared yes_no helper - Update all call sites (deploy status/get_status, app status/list) to use format_egress_enabled - deploy/status.rs: import fetch_tvc_app and use short call - app/create.rs: use .into() for enable_egress - app/list.rs: inline render_app_lines into render_app, add SEPARATOR_WIDTH const --- tvc/src/commands/app/create.rs | 2 +- tvc/src/commands/app/list.rs | 22 +++++++++++----------- tvc/src/commands/app/status.rs | 2 +- tvc/src/commands/deploy/get_status.rs | 2 +- tvc/src/commands/deploy/status.rs | 15 ++++++--------- tvc/src/commands/display.rs | 10 +++++----- 6 files changed, 25 insertions(+), 28 deletions(-) diff --git a/tvc/src/commands/app/create.rs b/tvc/src/commands/app/create.rs index 3721cba6..6806a85f 100644 --- a/tvc/src/commands/app/create.rs +++ b/tvc/src/commands/app/create.rs @@ -146,7 +146,7 @@ fn build_create_tvc_app_intent(app_config: &AppConfig) -> CreateTvcAppIntent { .map(to_tvc_operator_set_params), share_set_id: app_config.share_set_id.clone(), share_set_params: share_set_params.as_ref().map(to_tvc_operator_set_params), - enable_egress: Some(app_config.enable_egress), + enable_egress: app_config.enable_egress.into(), enable_debug_mode_deployments: app_config.dangerous_enable_debug_mode_deployments.into(), } } diff --git a/tvc/src/commands/app/list.rs b/tvc/src/commands/app/list.rs index 04861a37..85557b0c 100644 --- a/tvc/src/commands/app/list.rs +++ b/tvc/src/commands/app/list.rs @@ -5,6 +5,8 @@ use clap::Args as ClapArgs; use turnkey_client::generated::external::data::v1::TvcApp; use turnkey_client::generated::GetTvcAppsRequest; +const SEPARATOR_WIDTH: usize = 40; + /// List apps. #[derive(Debug, ClapArgs)] #[command(about, long_about = None)] @@ -49,27 +51,22 @@ fn filter_by_name(apps: &mut Vec, name: Option<&str>) { } fn render_app(app: &TvcApp) { - for line in render_app_lines(app) { - println!("{line}"); - } -} - -fn render_app_lines(app: &TvcApp) -> Vec { let live = app.live_deployment_id.as_deref().unwrap_or("(none)"); let mut lines = vec![ format!("Name: {}", app.name), format!("ID: {}", app.id), format!("Quorum Public Key: {}", app.quorum_public_key), format!("Live Deployment: {live}"), - crate::commands::display::egress_enabled_line(app.enable_egress), + crate::commands::display::format_egress_enabled(app.enable_egress), ]; if !app.public_domain.is_empty() { lines.push(format!("Public Domain: {}", app.public_domain)); } - lines.push("─".repeat(40)); - lines + lines.push("─".repeat(SEPARATOR_WIDTH)); + + println!("{}", lines.join("\n")); } #[cfg(test)] @@ -159,10 +156,13 @@ mod tests { } #[test] - fn app_render_lines_include_egress_status() { + fn egress_enabled_line_reflects_app_setting() { let mut app = make_app("my-app"); app.enable_egress = true; - assert!(render_app_lines(&app).contains(&"Egress Enabled: yes".to_string())); + assert_eq!( + crate::commands::display::format_egress_enabled(app.enable_egress), + "Egress Enabled: yes" + ); } } diff --git a/tvc/src/commands/app/status.rs b/tvc/src/commands/app/status.rs index dcd18fcc..5be851c8 100644 --- a/tvc/src/commands/app/status.rs +++ b/tvc/src/commands/app/status.rs @@ -39,7 +39,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { println!("Targeted Deployment: {}", app_status.targeted_deployment_id); println!( "{}", - crate::commands::display::egress_enabled_line(app.enable_egress) + crate::commands::display::format_egress_enabled(app.enable_egress) ); if app_status.deployments.is_empty() { diff --git a/tvc/src/commands/deploy/get_status.rs b/tvc/src/commands/deploy/get_status.rs index 76879c4e..355cb3a9 100644 --- a/tvc/src/commands/deploy/get_status.rs +++ b/tvc/src/commands/deploy/get_status.rs @@ -55,7 +55,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { println!("App ID: {}", app_status.app_id); println!( "{}", - crate::commands::display::egress_enabled_line(app.enable_egress) + crate::commands::display::format_egress_enabled(app.enable_egress) ); println!( "Is Targeted Deployment: {}", diff --git a/tvc/src/commands/deploy/status.rs b/tvc/src/commands/deploy/status.rs index a4c6bab8..70e31090 100644 --- a/tvc/src/commands/deploy/status.rs +++ b/tvc/src/commands/deploy/status.rs @@ -5,6 +5,9 @@ use clap::Args as ClapArgs; use turnkey_client::generated::external::data::v1::TvcDeployment; use turnkey_client::generated::GetTvcDeploymentRequest; +use crate::client::fetch_tvc_app; +use crate::commands::display::{format_egress_enabled, yes_no}; + /// Get the status of a deployment. #[derive(Debug, ClapArgs)] #[command(about, long_about = None)] @@ -37,14 +40,11 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .manifest .as_ref() .ok_or_else(|| anyhow::anyhow!("manifest not found in deployment"))?; - let app = crate::client::fetch_tvc_app(&auth, &deployment.app_id).await?; + let app = fetch_tvc_app(&auth, &deployment.app_id).await?; println!("Deployment: {}", deployment.id); println!("App ID: {}", deployment.app_id); - println!( - "{}", - crate::commands::display::egress_enabled_line(app.enable_egress) - ); + println!("{}", format_egress_enabled(app.enable_egress)); println!("Manifest ID: {}", manifest.id); println!("QOS Version: {}", deployment.qos_version); println!("{}", format_marked_for_deletion(&deployment)); @@ -72,10 +72,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { } fn format_marked_for_deletion(deployment: &TvcDeployment) -> String { - format!( - "Marked for deletion: {}", - if deployment.delete { "yes" } else { "no" } - ) + format!("Marked for deletion: {}", yes_no(deployment.delete)) } #[cfg(test)] diff --git a/tvc/src/commands/display.rs b/tvc/src/commands/display.rs index dcf388fa..0ee78c53 100644 --- a/tvc/src/commands/display.rs +++ b/tvc/src/commands/display.rs @@ -8,17 +8,17 @@ pub fn yes_no(value: bool) -> &'static str { } } -pub fn egress_enabled_line(enable_egress: bool) -> String { +pub fn format_egress_enabled(enable_egress: bool) -> String { format!("Egress Enabled: {}", yes_no(enable_egress)) } #[cfg(test)] mod tests { - use super::egress_enabled_line; + use super::format_egress_enabled; #[test] - fn egress_enabled_line_formats_yes_and_no() { - assert_eq!(egress_enabled_line(true), "Egress Enabled: yes"); - assert_eq!(egress_enabled_line(false), "Egress Enabled: no"); + fn format_egress_enabled_formats_yes_and_no() { + assert_eq!(format_egress_enabled(true), "Egress Enabled: yes"); + assert_eq!(format_egress_enabled(false), "Egress Enabled: no"); } } From ef26d0442cc0164edd393b512081e4154483e95a Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Mon, 15 Jun 2026 16:28:26 -0400 Subject: [PATCH 3/4] Address egress display review feedback --- tvc/src/commands/app/list.rs | 6 ++++-- tvc/src/commands/app/status.rs | 7 +++---- tvc/src/commands/deploy/get_status.rs | 7 +++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tvc/src/commands/app/list.rs b/tvc/src/commands/app/list.rs index 85557b0c..6d03e4bd 100644 --- a/tvc/src/commands/app/list.rs +++ b/tvc/src/commands/app/list.rs @@ -5,6 +5,8 @@ use clap::Args as ClapArgs; use turnkey_client::generated::external::data::v1::TvcApp; use turnkey_client::generated::GetTvcAppsRequest; +use crate::commands::display::format_egress_enabled; + const SEPARATOR_WIDTH: usize = 40; /// List apps. @@ -57,7 +59,7 @@ fn render_app(app: &TvcApp) { format!("ID: {}", app.id), format!("Quorum Public Key: {}", app.quorum_public_key), format!("Live Deployment: {live}"), - crate::commands::display::format_egress_enabled(app.enable_egress), + format_egress_enabled(app.enable_egress), ]; if !app.public_domain.is_empty() { @@ -161,7 +163,7 @@ mod tests { app.enable_egress = true; assert_eq!( - crate::commands::display::format_egress_enabled(app.enable_egress), + format_egress_enabled(app.enable_egress), "Egress Enabled: yes" ); } diff --git a/tvc/src/commands/app/status.rs b/tvc/src/commands/app/status.rs index 5be851c8..5a99681a 100644 --- a/tvc/src/commands/app/status.rs +++ b/tvc/src/commands/app/status.rs @@ -4,6 +4,8 @@ use anyhow::{anyhow, Context}; use clap::Args as ClapArgs; use turnkey_client::generated::GetAppStatusRequest; +use crate::commands::display::format_egress_enabled; + /// Get the live status of an app from the cluster. #[derive(Debug, ClapArgs)] #[command(about, long_about = None)] @@ -37,10 +39,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { println!("App ID: {}", app_status.app_id); println!("Targeted Deployment: {}", app_status.targeted_deployment_id); - println!( - "{}", - crate::commands::display::format_egress_enabled(app.enable_egress) - ); + println!("{}", format_egress_enabled(app.enable_egress)); if app_status.deployments.is_empty() { println!(); diff --git a/tvc/src/commands/deploy/get_status.rs b/tvc/src/commands/deploy/get_status.rs index 355cb3a9..91e77010 100644 --- a/tvc/src/commands/deploy/get_status.rs +++ b/tvc/src/commands/deploy/get_status.rs @@ -5,6 +5,8 @@ use clap::Args as ClapArgs; use turnkey_client::generated::external::data::v1::{AppStatus, DeploymentStatus}; use turnkey_client::generated::{GetAppStatusRequest, GetTvcDeploymentRequest}; +use crate::commands::display::format_egress_enabled; + /// Get the live status of a deployment from the app status API. #[derive(Debug, ClapArgs)] #[command(about, long_about = None)] @@ -53,10 +55,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { println!("Deployment: {}", deployment.id); println!("App ID: {}", app_status.app_id); - println!( - "{}", - crate::commands::display::format_egress_enabled(app.enable_egress) - ); + println!("{}", format_egress_enabled(app.enable_egress)); println!( "Is Targeted Deployment: {}", if app_status.targeted_deployment_id == args.deploy_id { From 1f978d39556736e4a20e826f0b7ab0293d18b4a1 Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Mon, 15 Jun 2026 17:18:45 -0400 Subject: [PATCH 4/4] tvc: short import for fetch_tvc_app --- tvc/src/commands/app/status.rs | 3 ++- tvc/src/commands/deploy/get_status.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tvc/src/commands/app/status.rs b/tvc/src/commands/app/status.rs index 5a99681a..4cba54c6 100644 --- a/tvc/src/commands/app/status.rs +++ b/tvc/src/commands/app/status.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Context}; use clap::Args as ClapArgs; use turnkey_client::generated::GetAppStatusRequest; +use crate::client::fetch_tvc_app; use crate::commands::display::format_egress_enabled; /// Get the live status of an app from the cluster. @@ -35,7 +36,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .app_status .ok_or_else(|| anyhow!("no status returned for app: {}", args.app_id))?, ); - let app = crate::client::fetch_tvc_app(&auth, &args.app_id).await?; + let app = fetch_tvc_app(&auth, &args.app_id).await?; println!("App ID: {}", app_status.app_id); println!("Targeted Deployment: {}", app_status.targeted_deployment_id); diff --git a/tvc/src/commands/deploy/get_status.rs b/tvc/src/commands/deploy/get_status.rs index 91e77010..9e897b97 100644 --- a/tvc/src/commands/deploy/get_status.rs +++ b/tvc/src/commands/deploy/get_status.rs @@ -5,6 +5,7 @@ use clap::Args as ClapArgs; use turnkey_client::generated::external::data::v1::{AppStatus, DeploymentStatus}; use turnkey_client::generated::{GetAppStatusRequest, GetTvcDeploymentRequest}; +use crate::client::fetch_tvc_app; use crate::commands::display::format_egress_enabled; /// Get the live status of a deployment from the app status API. @@ -51,7 +52,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { .app_status .ok_or_else(|| anyhow!("no status returned for app: {}", deployment.app_id))?, ); - let app = crate::client::fetch_tvc_app(&auth, &deployment.app_id).await?; + let app = fetch_tvc_app(&auth, &deployment.app_id).await?; println!("Deployment: {}", deployment.id); println!("App ID: {}", app_status.app_id);