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
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "0.3.62"
version = "0.3.63"
description = "Tower is the best way to host Python data apps in production"
rust-version = "1.81"
authors = ["Brad Heller <brad@tower.dev>", "Ben Lovell <ben@tower.dev>"]
Expand Down
59 changes: 45 additions & 14 deletions crates/tower-cmd/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::Item> { self.apps }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
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::Item> { self.teams }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
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::Item> { self.secrets }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
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::Item> { self.catalogs }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
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::Item> { self.environments }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
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::Item> { self.schedules }
fn pagination(&self) -> &Pagination {
&self.pages
}
fn into_items(self) -> Vec<Self::Item> {
self.schedules
}
}

/// Fetches pages from a paginated API endpoint, honoring the caller's
Expand Down Expand Up @@ -375,8 +399,7 @@ pub async fn list_secrets(
config: &Config,
env: &str,
all: bool,
) -> Result<Vec<tower_api::models::Secret>, Error<tower_api::apis::default_api::ListSecretsError>>
{
) -> Result<Vec<tower_api::models::Secret>, Error<tower_api::apis::default_api::ListSecretsError>> {
let api_config: configuration::Configuration = config.into();
let env = env.to_string();

Expand Down Expand Up @@ -650,8 +673,16 @@ pub async fn stream_run_logs(
builder = builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}

// Mirrors the generated tower-api client: prefer a bearer token (interactive session),
// otherwise fall back to the API key header set when TOWER_API_KEY is configured.
if let Some(ref token) = api_config.bearer_access_token {
builder = builder.bearer_auth(token.to_owned());
} else if let Some(ref apikey) = api_config.api_key {
let value = match &apikey.prefix {
Some(prefix) => format!("{} {}", prefix, apikey.key),
None => apikey.key.clone(),
};
builder = builder.header("X-API-Key", value);
};

// Now let's try to open the event source with the server.
Expand Down
8 changes: 4 additions & 4 deletions crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<String>("environment"), None);
Expand All @@ -849,7 +847,9 @@ mod tests {
let (_, list_args) = matches.subcommand().unwrap();

assert_eq!(
list_args.get_one::<String>("environment").map(|s| s.as_str()),
list_args
.get_one::<String>("environment")
.map(|s| s.as_str()),
Some("production")
);
}
Expand Down
3 changes: 2 additions & 1 deletion crates/tower-cmd/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
5 changes: 1 addition & 4 deletions crates/tower-cmd/src/teams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@ async fn do_list_via_api(config: &Config) {

let headers = vec!["Name".to_string()];

let teams_data: Vec<Vec<String>> = teams
.iter()
.map(|team| vec![team.name.clone()])
.collect();
let teams_data: Vec<Vec<String>> = teams.iter().map(|team| vec![team.name.clone()]).collect();

output::newline();
output::table(headers, teams_data, None::<&Vec<config::Team>>);
Expand Down
10 changes: 9 additions & 1 deletion crates/tower-cmd/src/util/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,17 @@ pub async fn upload_file_with_progress(
.header("Content-Encoding", "gzip")
.body(Body::wrap_stream(progress_stream));

// Add authorization if available
// Add authorization if available. Mirrors the generated tower-api client: prefer a
// bearer token (interactive session), otherwise fall back to the API key header set
// when TOWER_API_KEY is configured.
if let Some(token) = &api_config.bearer_access_token {
req = req.header("Authorization", format!("Bearer {}", token));
} else if let Some(apikey) = &api_config.api_key {
let value = match &apikey.prefix {
Some(prefix) => format!("{} {}", prefix, apikey.key),
None => apikey.key.clone(),
};
req = req.header("X-API-Key", value);
}

// Send the request
Expand Down
23 changes: 21 additions & 2 deletions crates/tower-package/src/towerfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,28 @@ impl Towerfile {
return Err(Error::MissingRequiredAppField {
field: "name".to_string(),
});
} else {
Ok(towerfile)
}

for import_path in &towerfile.app.import_paths {
let as_str = import_path.to_string_lossy();
if as_str.is_empty() {
return Err(Error::InvalidTowerfile {
message: "import_paths entries must not be empty".to_string(),
});
}
// PATH-style separators in a single entry would break PYTHONPATH construction at
// runtime, since each entry is joined with the platform path separator.
if as_str.contains(|c: char| c == ':' || c == ';') {
return Err(Error::InvalidTowerfile {
message: format!(
"import_paths entry {:?} contains an illegal character (':' or ';')",
as_str
),
});
}
}

Ok(towerfile)
}

/// set_parameter upserts a parameter by lookup name. If a parameter with the given name
Expand Down
28 changes: 26 additions & 2 deletions crates/tower-runtime/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<DateTime<Utc>>,
pub ended_at: Option<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
}

impl From<Status> 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<Status, Error>;
/// Get current execution status with optional timing metadata
async fn status(&self) -> Result<ExecutionStatus, Error>;

/// Subscribe to log stream
async fn logs(&self) -> Result<OutputReceiver, Error>;
Expand Down
5 changes: 1 addition & 4 deletions crates/tower-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)
)
}
}
Expand Down
Loading
Loading