diff --git a/crates/tower-cmd/src/api.rs b/crates/tower-cmd/src/api.rs index 07f0dca8..7accd413 100644 --- a/crates/tower-cmd/src/api.rs +++ b/crates/tower-cmd/src/api.rs @@ -30,38 +30,62 @@ trait PaginatedResponse { impl PaginatedResponse for tower_api::models::ListAppsResponse { type Item = tower_api::models::AppSummary; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.apps } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.apps + } } impl PaginatedResponse for tower_api::models::ListTeamsResponse { type Item = tower_api::models::Team; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.teams } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.teams + } } impl PaginatedResponse for tower_api::models::ListSecretsResponse { type Item = tower_api::models::Secret; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.secrets } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.secrets + } } impl PaginatedResponse for tower_api::models::ListCatalogsResponse { type Item = tower_api::models::Catalog; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.catalogs } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.catalogs + } } impl PaginatedResponse for tower_api::models::ListEnvironmentsResponse { type Item = tower_api::models::Environment; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.environments } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.environments + } } impl PaginatedResponse for tower_api::models::ListSchedulesResponse { type Item = tower_api::models::Schedule; - fn pagination(&self) -> &Pagination { &self.pages } - fn into_items(self) -> Vec { self.schedules } + fn pagination(&self) -> &Pagination { + &self.pages + } + fn into_items(self) -> Vec { + self.schedules + } } /// Fetches pages from a paginated API endpoint, honoring the caller's @@ -375,8 +399,7 @@ pub async fn list_secrets( config: &Config, env: &str, all: bool, -) -> Result, Error> -{ +) -> Result, Error> { let api_config: configuration::Configuration = config.into(); let env = env.to_string(); diff --git a/crates/tower-cmd/src/apps.rs b/crates/tower-cmd/src/apps.rs index e04c3dc9..5fc707c1 100644 --- a/crates/tower-cmd/src/apps.rs +++ b/crates/tower-cmd/src/apps.rs @@ -833,9 +833,7 @@ mod tests { #[test] fn list_defaults_to_no_environment_filter() { - let matches = apps_cmd() - .try_get_matches_from(["apps", "list"]) - .unwrap(); + let matches = apps_cmd().try_get_matches_from(["apps", "list"]).unwrap(); let (_, list_args) = matches.subcommand().unwrap(); assert_eq!(list_args.get_one::("environment"), None); @@ -849,7 +847,9 @@ mod tests { let (_, list_args) = matches.subcommand().unwrap(); assert_eq!( - list_args.get_one::("environment").map(|s| s.as_str()), + list_args + .get_one::("environment") + .map(|s| s.as_str()), Some("production") ); } diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index be8050e6..3a3cfa27 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -713,9 +713,10 @@ async fn monitor_cli_status( ); match handle.lock().await.status().await { - Ok(status) => { + Ok(exec_status) => { // We reset the error count to indicate that we can intermittently get statuses. err_count = 0; + let status = exec_status.status; match status { Status::Exited => { diff --git a/crates/tower-cmd/src/teams.rs b/crates/tower-cmd/src/teams.rs index 36783a9e..59ae9636 100644 --- a/crates/tower-cmd/src/teams.rs +++ b/crates/tower-cmd/src/teams.rs @@ -60,10 +60,7 @@ async fn do_list_via_api(config: &Config) { let headers = vec!["Name".to_string()]; - let teams_data: Vec> = teams - .iter() - .map(|team| vec![team.name.clone()]) - .collect(); + let teams_data: Vec> = teams.iter().map(|team| vec![team.name.clone()]).collect(); output::newline(); output::table(headers, teams_data, None::<&Vec>); diff --git a/crates/tower-runtime/src/execution.rs b/crates/tower-runtime/src/execution.rs index 47d6dd26..f69e51f9 100644 --- a/crates/tower-runtime/src/execution.rs +++ b/crates/tower-runtime/src/execution.rs @@ -5,6 +5,7 @@ //! Kubernetes pods, etc.) through a uniform interface. use async_trait::async_trait; +use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::path::PathBuf; use tokio::io::AsyncRead; @@ -198,14 +199,37 @@ pub struct BackendCapabilities { // Execution Handle Trait // ============================================================================ +/// Result of querying execution status, including optional timing and +/// backend-specific metadata. The metadata map allows backends to surface +/// arbitrary key-value data (e.g. node_type, scheduling_latency_ms) without +/// requiring trait changes. +#[derive(Clone, Debug)] +pub struct ExecutionStatus { + pub status: Status, + pub started_at: Option>, + pub ended_at: Option>, + pub metadata: HashMap, +} + +impl From for ExecutionStatus { + fn from(status: Status) -> Self { + Self { + status, + started_at: None, + ended_at: None, + metadata: HashMap::new(), + } + } +} + /// ExecutionHandle represents a running execution #[async_trait] pub trait ExecutionHandle: Send + Sync { /// Get a unique identifier for this execution fn id(&self) -> &str; - /// Get current execution status - async fn status(&self) -> Result; + /// Get current execution status with optional timing metadata + async fn status(&self) -> Result; /// Subscribe to log stream async fn logs(&self) -> Result; diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 446264fe..fffb9447 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -83,10 +83,7 @@ impl Status { pub fn is_terminal(&self) -> bool { matches!( self, - Status::Exited - | Status::Crashed { .. } - | Status::Cancelled - | Status::Failed(_) + Status::Exited | Status::Crashed { .. } | Status::Cancelled | Status::Failed(_) ) } } diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 07b368cf..5f209361 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -303,7 +303,8 @@ async fn inner_execute_local_app( } } Ok(child) => { - let mut res = run_setup_child(&ctx, &cancel_token, &opts.output_sender, child).await; + let mut res = + run_setup_child(&ctx, &cancel_token, &opts.output_sender, child).await; // If sync was cancelled, don't bother retrying — bail out // cleanly so the receiver sees `Status::Cancelled` instead of @@ -328,7 +329,8 @@ async fn inner_execute_local_app( let retry_child = uv .sync_with_legacy_setuptools_pin(&working_dir, &env_vars) .await?; - res = run_setup_child(&ctx, &cancel_token, &opts.output_sender, retry_child).await; + res = run_setup_child(&ctx, &cancel_token, &opts.output_sender, retry_child) + .await; if cancel_token.is_cancelled() { return Err(Error::Cancelled); } @@ -419,7 +421,9 @@ impl App for LocalApp { Ok(Ok(code)) => AppCompletion::Exit(code), Ok(Err(Error::Cancelled)) => AppCompletion::Cancelled, Ok(Err(e)) => AppCompletion::Failed(AppFailure::Runtime(e)), - Err(panic) => AppCompletion::Failed(AppFailure::Panic(panic_payload_message(&panic))), + Err(panic) => { + AppCompletion::Failed(AppFailure::Panic(panic_payload_message(&panic))) + } }; let _ = sx.send(completion); }); diff --git a/crates/tower-runtime/src/subprocess.rs b/crates/tower-runtime/src/subprocess.rs index decdb1b0..23c7d4ef 100644 --- a/crates/tower-runtime/src/subprocess.rs +++ b/crates/tower-runtime/src/subprocess.rs @@ -4,7 +4,7 @@ use crate::auto_cleanup; use crate::errors::Error; use crate::execution::{ BackendCapabilities, CacheBackend, ExecutionBackend, ExecutionHandle, ExecutionSpec, - ServiceEndpoint, + ExecutionStatus, ServiceEndpoint, }; use crate::local::LocalApp; use crate::{App, OutputReceiver, StartOptions, Status}; @@ -212,9 +212,9 @@ impl ExecutionHandle for SubprocessHandle { &self.id } - async fn status(&self) -> Result { + async fn status(&self) -> Result { let app = self.app.lock().await; - app.status().await + Ok(app.status().await?.into()) } async fn logs(&self) -> Result { @@ -247,7 +247,7 @@ impl ExecutionHandle for SubprocessHandle { async fn wait_for_completion(&self) -> Result { loop { - let status = self.status().await?; + let status = self.status().await?.status; match status { Status::None | Status::Running => { tokio::time::sleep(Duration::from_millis(100)).await;