Skip to content

Commit 18e085c

Browse files
authored
Merge pull request #150 from tower/develop
v0.3.39 release
2 parents 824fce1 + 959450c commit 18e085c

22 files changed

Lines changed: 498 additions & 371 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ resolver = "2"
44

55
[workspace.package]
66
edition = "2021"
7-
version = "0.3.38"
7+
version = "0.3.39"
88
description = "Tower is the best way to host Python data apps in production"
99
rust-version = "1.81"
1010
authors = ["Brad Heller <brad@tower.dev>"]

crates/config/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ rust-version = { workspace = true }
77
license = { workspace = true }
88

99
[dependencies]
10+
base64 = { workspace = true }
1011
chrono = { workspace = true }
1112
clap = { workspace = true }
12-
dirs = { workspace = true }
13+
dirs = { workspace = true }
1314
futures = { workspace = true }
1415
serde = { workspace = true }
1516
serde_json = { workspace = true }

crates/config/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,15 @@ impl Config {
181181

182182
configuration.base_path = base_path.to_string();
183183

184-
if let Some(session) = &self.session {
184+
// Always read from disk to pick up team switches
185+
if let Ok(session) = Session::from_config_dir() {
185186
if let Some(active_team) = &session.active_team {
186-
// Use the active team's JWT token
187187
configuration.bearer_access_token = Some(active_team.token.jwt.clone());
188188
} else {
189-
// Fall back to session token if no active team
190189
configuration.bearer_access_token = Some(session.token.jwt.clone());
191190
}
192191
}
193192

194-
// Store the configuration in self
195193
configuration
196194
}
197195
}

crates/config/src/session.rs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
12
use chrono::{DateTime, TimeZone, Utc};
23
use serde::{Deserialize, Serialize};
34
use std::fs;
@@ -9,6 +10,20 @@ use crate::error::Error;
910
use tower_api::apis::default_api::describe_session;
1011
use tower_telemetry::debug;
1112

13+
/// Extracts the account ID (aid) from a Tower JWT token.
14+
/// Returns None if the JWT is malformed or doesn't contain an aid.
15+
fn extract_aid_from_jwt(jwt: &str) -> Option<String> {
16+
let parts: Vec<&str> = jwt.split('.').collect();
17+
if parts.len() != 3 {
18+
return None;
19+
}
20+
21+
let payload = parts[1];
22+
let decoded = URL_SAFE_NO_PAD.decode(payload).ok()?;
23+
let json: serde_json::Value = serde_json::from_slice(&decoded).ok()?;
24+
json.get("https://tower.dev/aid")?.as_str().map(String::from)
25+
}
26+
1227
const DEFAULT_TOWER_URL: &str = "https://api.tower.dev";
1328

1429
pub fn default_tower_url() -> Url {
@@ -163,6 +178,22 @@ impl Session {
163178
}
164179
}
165180

181+
/// Sets the active team based on an account ID (aid) extracted from a JWT.
182+
/// Returns true if a matching team was found and set as active, false otherwise.
183+
pub fn set_active_team_by_aid(&mut self, aid: &str) -> bool {
184+
// Find the team whose JWT contains the matching aid
185+
if let Some(team) = self
186+
.teams
187+
.iter()
188+
.find(|team| extract_aid_from_jwt(&team.token.jwt).as_deref() == Some(aid))
189+
{
190+
self.active_team = Some(team.clone());
191+
true
192+
} else {
193+
false
194+
}
195+
}
196+
166197
/// Updates the session with data from the API response
167198
pub fn update_from_api_response(
168199
&mut self,
@@ -263,36 +294,30 @@ impl Session {
263294
}
264295

265296
pub fn from_jwt(jwt: &str) -> Result<Self, Error> {
266-
// We need to instantiate our own configuration object here, instead of the typical thing
267-
// that we do which is turn a Config into a Configuration.
297+
let jwt_aid = extract_aid_from_jwt(jwt);
298+
268299
let mut config = tower_api::apis::configuration::Configuration::new();
269300
config.bearer_access_token = Some(jwt.to_string());
270301

271-
// We only pull TOWER_URL out of the environment here because we only ever use the JWT and
272-
// all that in programmatic contexts (when TOWER_URL is set).
273-
let tower_url = if let Ok(val) = std::env::var("TOWER_URL") {
274-
val
275-
} else {
276-
DEFAULT_TOWER_URL.to_string()
277-
};
278-
279-
// Setup the base path to point to the /v1 API endpoint as expected.
302+
let tower_url = std::env::var("TOWER_URL").unwrap_or(DEFAULT_TOWER_URL.to_string());
280303
let mut base_path = Url::parse(&tower_url).unwrap();
281304
base_path.set_path("/v1");
282-
283305
config.base_path = base_path.to_string();
284306

285-
// This is a bit of a hairy thing: I didn't want to pull in too much from the Tower API
286-
// client, so we're using the raw bindings here.
287307
match run_future_sync(describe_session(&config)) {
288308
Ok(resp) => {
289-
// Now we need to extract the session from the response.
290309
let entity = resp.entity.unwrap();
291310

292311
match entity {
293312
tower_api::apis::default_api::DescribeSessionSuccess::Status200(resp) => {
294313
let mut session = Session::from_api_session(&resp.session);
295314
session.tower_url = base_path;
315+
316+
if let Some(aid) = jwt_aid {
317+
session.set_active_team_by_aid(&aid);
318+
}
319+
320+
session.save()?;
296321
Ok(session)
297322
}
298323
tower_api::apis::default_api::DescribeSessionSuccess::UnknownValue(val) => {

crates/tower-cmd/src/apps.rs

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -133,36 +133,27 @@ pub async fn do_show(config: Config, cmd: &ArgMatches) {
133133

134134
output::table(headers, rows, Some(&app_response));
135135
}
136-
Err(err) => {
137-
output::tower_error(err);
138-
}
136+
Err(err) => output::tower_error_and_die(err, "Fetching app details failed"),
139137
}
140138
}
141139

142140
pub async fn do_list_apps(config: Config) {
143-
let resp = api::list_apps(&config).await;
144-
145-
match resp {
146-
Ok(resp) => {
147-
let items = resp
148-
.apps
149-
.iter()
150-
.map(|app_summary| {
151-
let app = &app_summary.app;
152-
let desc = if app.short_description.is_empty() {
153-
output::placeholder("No description")
154-
} else {
155-
app.short_description.to_string()
156-
};
157-
format!("{}\n{}", output::title(&app.name), desc)
158-
})
159-
.collect();
160-
output::list(items, Some(&resp.apps));
161-
}
162-
Err(err) => {
163-
output::tower_error(err);
164-
}
165-
}
141+
let resp = output::with_spinner("Listing apps", api::list_apps(&config)).await;
142+
143+
let items = resp
144+
.apps
145+
.iter()
146+
.map(|app_summary| {
147+
let app = &app_summary.app;
148+
let desc = if app.short_description.is_empty() {
149+
output::placeholder("No description")
150+
} else {
151+
app.short_description.to_string()
152+
};
153+
format!("{}\n{}", output::title(&app.name), desc)
154+
})
155+
.collect();
156+
output::list(items, Some(&resp.apps));
166157
}
167158

168159
pub async fn do_create(config: Config, args: &ArgMatches) {
@@ -172,30 +163,16 @@ pub async fn do_create(config: Config, args: &ArgMatches) {
172163

173164
let description = args.get_one::<String>("description").unwrap();
174165

175-
let mut spinner = output::spinner("Creating app");
166+
let app =
167+
output::with_spinner("Creating app", api::create_app(&config, name, description)).await;
176168

177-
match api::create_app(&config, name, description).await {
178-
Ok(app) => {
179-
spinner.success();
180-
output::success_with_data(&format!("App '{}' created", name), Some(app));
181-
}
182-
Err(err) => {
183-
spinner.failure();
184-
output::tower_error(err);
185-
}
186-
}
169+
output::success_with_data(&format!("App '{}' created", name), Some(app));
187170
}
188171

189172
pub async fn do_delete(config: Config, cmd: &ArgMatches) {
190173
let name = extract_app_name("delete", cmd.subcommand());
191-
let mut spinner = output::spinner("Deleting app");
192174

193-
if let Err(err) = api::delete_app(&config, &name).await {
194-
spinner.failure();
195-
output::tower_error(err);
196-
} else {
197-
spinner.success();
198-
}
175+
output::with_spinner("Deleting app", api::delete_app(&config, &name)).await;
199176
}
200177

201178
/// Extract app name and run number from command

crates/tower-cmd/src/deploy.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ pub async fn do_deploy(config: Config, args: &ArgMatches) {
4040
let create_app = args.get_flag("create");
4141
if let Err(err) = deploy_from_dir(config, dir, create_app).await {
4242
match err {
43-
crate::Error::ApiDeployError { source } => output::tower_error(source),
44-
crate::Error::ApiDescribeAppError { source } => output::tower_error(source),
45-
crate::Error::PackageError { source } => output::package_error(source),
46-
crate::Error::TowerfileLoadFailed { source, .. } => output::config_error(source),
47-
_ => output::error(&err.to_string()),
43+
crate::Error::ApiDeployError { source } => {
44+
output::tower_error_and_die(source, "Deploying app failed")
45+
}
46+
crate::Error::ApiDescribeAppError { source } => {
47+
output::tower_error_and_die(source, "Fetching app details failed")
48+
}
49+
crate::Error::PackageError { source } => {
50+
output::package_error(source);
51+
std::process::exit(1);
52+
}
53+
crate::Error::TowerfileLoadFailed { source, .. } => {
54+
output::config_error(source);
55+
std::process::exit(1);
56+
}
57+
_ => output::die(&err.to_string()),
4858
}
4959
}
5060
}
@@ -62,13 +72,16 @@ pub async fn deploy_from_dir(
6272
let api_config = config.into();
6373

6474
// Add app existence check before proceeding
65-
util::apps::ensure_app_exists(
75+
if let Err(err) = util::apps::ensure_app_exists(
6676
&api_config,
6777
&towerfile.app.name,
6878
&towerfile.app.description,
6979
create_app,
7080
)
71-
.await?;
81+
.await
82+
{
83+
return Err(crate::Error::ApiDescribeAppError { source: err });
84+
}
7285

7386
let spec = PackageSpec::from_towerfile(&towerfile);
7487
let mut spinner = output::spinner("Building package...");

0 commit comments

Comments
 (0)