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.49"
version = "0.3.50"
description = "Tower is the best way to host Python data apps in production"
rust-version = "1.81"
authors = ["Brad Heller <brad@tower.dev>"]
Expand Down
24 changes: 24 additions & 0 deletions crates/config/src/towerfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub struct Parameter {

#[serde(default)]
pub default: String,

#[serde(default)]
pub hidden: bool,
}

#[derive(Deserialize, Serialize, Debug)]
Expand Down Expand Up @@ -122,6 +125,7 @@ impl Towerfile {
name,
description,
default,
hidden: false,
});
}
}
Expand Down Expand Up @@ -272,6 +276,26 @@ mod test {
assert_eq!(towerfile.parameters.len(), 2);
assert_eq!(towerfile.parameters[0].name, "my_first_param");
assert_eq!(towerfile.parameters[1].name, "my_second_param");
assert!(!towerfile.parameters[0].hidden);
}

#[test]
fn test_parses_secret_parameters() {
let toml = r#"
[app]
name = "my-app"
script = "./script.py"
source = ["*.py"]

[[parameters]]
name = "MY_PARAMETER"
hidden = true
"#;

let towerfile = crate::Towerfile::from_toml(toml).unwrap();
assert_eq!(towerfile.parameters.len(), 1);
assert_eq!(towerfile.parameters[0].name, "MY_PARAMETER");
assert!(towerfile.parameters[0].hidden);
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/tower-api/src/models/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub struct Parameter {
#[serde_as(as = "DefaultOnNull")]
#[serde(rename = "name")]
pub name: String,
#[serde(default)]
#[serde(rename = "hidden")]
pub hidden: bool,
}

impl Parameter {
Expand All @@ -31,6 +34,7 @@ impl Parameter {
default,
description,
name,
hidden: false,
}
}
}
9 changes: 8 additions & 1 deletion crates/tower-api/src/models/run_parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ pub struct RunParameter {
#[serde_as(as = "DefaultOnNull")]
#[serde(rename = "value")]
pub value: String,
#[serde(default)]
#[serde(rename = "hidden")]
pub hidden: bool,
}

impl RunParameter {
pub fn new(name: String, value: String) -> RunParameter {
RunParameter { name, value }
RunParameter {
name,
value,
hidden: false,
}
}
}
17 changes: 14 additions & 3 deletions crates/tower-cmd/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,11 @@ pub async fn create_schedule(
let run_parameters = parameters.map(|params| {
params
.into_iter()
.map(|(key, value)| RunParameter { name: key, value })
.map(|(key, value)| RunParameter {
name: key,
value,
hidden: false,
})
.collect()
});

Expand Down Expand Up @@ -874,14 +878,21 @@ pub async fn update_schedule(
let run_parameters = parameters.map(|params| {
params
.into_iter()
.map(|(key, value)| RunParameter { name: key, value })
.map(|(key, value)| RunParameter {
name: key,
value,
hidden: false,
})
.collect()
});

let params = tower_api::apis::default_api::UpdateScheduleParams {
id_or_name: schedule_id.to_string(),
update_schedule_params: tower_api::models::UpdateScheduleParams {
cron: cron.map(|s| s.clone()),
schema: None,
cron: cron.cloned(),
environment: None,
app_version: None,
parameters: run_parameters,
..Default::default()
},
Expand Down
112 changes: 64 additions & 48 deletions crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,36 @@ pub fn apps_cmd() -> Command {
.subcommand(Command::new("list").about("List all of your apps"))
.subcommand(
Command::new("show")
.allow_external_subcommands(true)
.override_usage("tower apps show [OPTIONS] <APP_NAME>")
.after_help("Example: tower apps show hello-world")
.arg(
Arg::new("app_name")
.value_parser(value_parser!(String))
.index(1)
.required(true)
.help("Name of the app"),
)
.about("Show the details about an app in Tower"),
)
.subcommand(
Command::new("logs")
.arg(
Arg::new("app_name")
.value_parser(value_parser!(String))
.index(1)
.required(true)
.help("app_name#run_number"),
)
.arg(
Arg::new("run_number")
.value_parser(value_parser!(i64))
.index(2),
)
.arg(
Arg::new("follow")
.short('f')
.long("follow")
.help("Follow logs in real time")
.action(clap::ArgAction::SetTrue),
)
.allow_external_subcommands(true)
.override_usage("tower apps logs [OPTIONS] <APP_NAME>#<RUN_NUMBER>")
.after_help("Example: tower apps logs hello-world#11")
.about("Get the logs from a previous Tower app run"),
)
.subcommand(
Expand All @@ -54,15 +67,29 @@ pub fn apps_cmd() -> Command {
)
.subcommand(
Command::new("delete")
.allow_external_subcommands(true)
.override_usage("tower apps delete [OPTIONS] <APP_NAME>")
.after_help("Example: tower apps delete hello-world")
.arg(
Arg::new("app_name")
.value_parser(value_parser!(String))
.index(1)
.required(true)
.help("Name of the app"),
)
.about("Delete an app in Tower"),
)
}

pub async fn do_logs(config: Config, cmd: &ArgMatches) {
let (name, seq) = extract_app_name_and_run("logs", cmd.subcommand());
let app_name_raw = cmd.get_one::<String>("app_name").expect("app_name is required");
let (name, seq) = if let Some((name, num_str)) = app_name_raw.split_once('#') {
let num = num_str.parse::<i64>().unwrap_or_else(|_| output::die("Run number must be a number"));
(name.to_string(), num)
} else {
let num = match cmd.get_one::<i64>("run_number").copied() {
Some(n) => n,
None => latest_run_number(&config, app_name_raw).await,
};
(app_name_raw.clone(), num)
};
let follow = cmd.get_one::<bool>("follow").copied().unwrap_or(false);

if follow {
Expand All @@ -78,7 +105,7 @@ pub async fn do_logs(config: Config, cmd: &ArgMatches) {
}

pub async fn do_show(config: Config, cmd: &ArgMatches) {
let name = extract_app_name("show", cmd.subcommand());
let name = cmd.get_one::<String>("app_name").expect("app_name is required");

match api::describe_app(&config, &name).await {
Ok(app_response) => {
Expand Down Expand Up @@ -191,46 +218,20 @@ pub async fn do_create(config: Config, args: &ArgMatches) {
}

pub async fn do_delete(config: Config, cmd: &ArgMatches) {
let name = extract_app_name("delete", cmd.subcommand());
let name = cmd.get_one::<String>("app_name").expect("app_name is required");

output::with_spinner("Deleting app", api::delete_app(&config, &name)).await;
output::with_spinner("Deleting app", api::delete_app(&config, name)).await;
}

/// Extract app name and run number from command
fn extract_app_name_and_run(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> (String, i64) {
if let Some((name, _)) = cmd {
if let Some((name, num)) = name.split_once('#') {
return (
name.to_string(),
num.parse::<i64>().unwrap_or_else(|_| {
output::die("Run number must be an actual number");
}),
);
}

let line = format!(
"Run number is required. Example: tower apps {} <app name>#<run number>",
subcmd
);
output::die(&line);
}
let line = format!(
"App name is required. Example: tower apps {} <app name>#<run number>",
subcmd
);
output::die(&line)
}

fn extract_app_name(subcmd: &str, cmd: Option<(&str, &ArgMatches)>) -> String {
if let Some((name, _)) = cmd {
return name.to_string();
async fn latest_run_number(config: &Config, name: &str) -> i64 {
match api::describe_app(config, name).await {
Ok(resp) => resp.runs
.iter()
.map(|r| r.number)
.max()
.unwrap_or_else(|| output::die(&format!("No runs found for app '{}'", name))),
Err(err) => output::tower_error_and_die(err, "Fetching app details failed"),
}

let line = format!(
"App name is required. Example: tower apps {} <app name>",
subcmd
);
output::die(&line);
}

const FOLLOW_BACKOFF_INITIAL: Duration = Duration::from_millis(500);
Expand Down Expand Up @@ -539,9 +540,24 @@ mod tests {
assert_eq!(cmd, "logs");
assert_eq!(sub_matches.get_one::<bool>("follow"), Some(&true));
assert_eq!(
sub_matches.subcommand().map(|(name, _)| name),
sub_matches.get_one::<String>("app_name").map(|s| s.as_str()),
Some("hello-world#11")
);
assert_eq!(sub_matches.get_one::<i64>("run_number"), None);
}

#[test]
fn test_separate_run_number_parsing() {
let matches = apps_cmd()
.try_get_matches_from(["apps", "logs", "hello-world", "11"])
.unwrap();
let (_, sub_matches) = matches.subcommand().unwrap();

assert_eq!(
sub_matches.get_one::<String>("app_name").map(|s| s.as_str()),
Some("hello-world")
);
assert_eq!(sub_matches.get_one::<i64>("run_number"), Some(&11));
}

#[test]
Expand Down
Loading