-
Notifications
You must be signed in to change notification settings - Fork 3
feat: multi-environment support for tower deploy #248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
665e5b9
0ebf7e4
75bd0e7
d9caa86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,20 @@ pub fn deploy_cmd() -> Command { | |
| .help("Automatically force creation of the app if it doesn't already exist") | ||
| .action(clap::ArgAction::SetTrue), | ||
| ) | ||
| .arg( | ||
| Arg::new("environment") | ||
| .long("environment") | ||
| .short('e') | ||
| .help("The environment to deploy to") | ||
| .conflicts_with("all"), | ||
| ) | ||
| .arg( | ||
| Arg::new("all") | ||
| .long("all") | ||
| .help("Deploy to all environments") | ||
| .action(clap::ArgAction::SetTrue) | ||
| .conflicts_with("environment"), | ||
| ) | ||
| .about("Deploy your latest code to Tower") | ||
| } | ||
|
|
||
|
|
@@ -35,10 +49,30 @@ fn resolve_path(args: &ArgMatches) -> PathBuf { | |
| } | ||
| } | ||
|
|
||
| /// Resolves the target environment from CLI args. | ||
| /// | ||
| /// - `--all` → `DeployTarget::All` | ||
| /// - `--environment <name>` → `DeployTarget::Environment(name)` | ||
| /// - neither → `DeployTarget::Default` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a |
||
| #[derive(Debug, Clone)] | ||
| pub enum DeployTarget { | ||
| Environment(String), | ||
| All, | ||
| } | ||
|
|
||
| pub async fn do_deploy(config: Config, args: &ArgMatches) { | ||
| let dir = resolve_path(args); | ||
| let create_app = args.get_flag("create"); | ||
| if let Err(err) = deploy_from_dir(config, dir, create_app).await { | ||
|
|
||
| let target = if args.get_flag("all") { | ||
| DeployTarget::All | ||
| } else if let Some(env) = args.get_one::<String>("environment") { | ||
| DeployTarget::Environment(env.clone()) | ||
| } else { | ||
| DeployTarget::Environment("default".to_string()) | ||
| }; | ||
|
|
||
| if let Err(err) = deploy_from_dir(config, dir, create_app, target).await { | ||
| match err { | ||
| crate::Error::ApiDeployError { source } => { | ||
| output::tower_error_and_die(source, "Deploying app failed") | ||
|
|
@@ -66,6 +100,7 @@ pub async fn deploy_from_dir( | |
| config: Config, | ||
| dir: PathBuf, | ||
| create_app: bool, | ||
| target: DeployTarget, | ||
| ) -> Result<(), crate::Error> { | ||
| debug!("Building package from directory: {:?}", dir); | ||
|
|
||
|
|
@@ -96,23 +131,145 @@ pub async fn deploy_from_dir( | |
| }; | ||
|
|
||
| spinner.success(); | ||
| do_deploy_package(api_config, package, &towerfile).await | ||
| do_deploy_package(api_config, package, &towerfile, target).await | ||
| } | ||
|
|
||
| async fn do_deploy_package( | ||
| api_config: Configuration, | ||
| package: Package, | ||
| towerfile: &Towerfile, | ||
| target: DeployTarget, | ||
| ) -> Result<(), crate::Error> { | ||
| let res = util::deploy::deploy_app_package(&api_config, &towerfile.app.name, package).await; | ||
| let (environment, all_environments) = match &target { | ||
| DeployTarget::All => (None, true), | ||
| DeployTarget::Environment(env) => (Some(env.as_str()), false), | ||
| }; | ||
|
|
||
| let res = util::deploy::deploy_app_package( | ||
| &api_config, | ||
| &towerfile.app.name, | ||
| package, | ||
| environment, | ||
| all_environments, | ||
| ) | ||
| .await; | ||
|
|
||
| match res { | ||
| Ok(resp) => { | ||
| let version = resp.version; | ||
| let line = format!("Version `{}` has been deployed to Tower!", version.version); | ||
| let line = match &target { | ||
| DeployTarget::All => format!( | ||
| "Version `{}` has been deployed to all environments!", | ||
| version.version | ||
| ), | ||
| DeployTarget::Environment(env) => format!( | ||
| "Version `{}` has been deployed to environment '{}'!", | ||
| version.version, env | ||
| ), | ||
| }; | ||
| output::success(&line); | ||
| Ok(()) | ||
| } | ||
| Err(err) => Err(crate::Error::ApiDeployError { source: err }), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::deploy_cmd; | ||
|
|
||
| fn parse(args: &[&str]) -> Result<clap::ArgMatches, clap::Error> { | ||
| let mut full = vec!["deploy"]; | ||
| full.extend_from_slice(args); | ||
| deploy_cmd().try_get_matches_from(full) | ||
| } | ||
|
|
||
| #[test] | ||
| fn no_args_uses_defaults() { | ||
| let m = parse(&[]).unwrap(); | ||
| assert_eq!(m.get_one::<String>("environment"), None); | ||
| assert!(!m.get_flag("all")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn environment_flag_long() { | ||
| let m = parse(&["--environment", "production"]).unwrap(); | ||
| assert_eq!( | ||
| m.get_one::<String>("environment").map(|s| s.as_str()), | ||
| Some("production") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn environment_flag_short() { | ||
| let m = parse(&["-e", "staging"]).unwrap(); | ||
| assert_eq!( | ||
| m.get_one::<String>("environment").map(|s| s.as_str()), | ||
| Some("staging") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn environment_flag_equals_syntax() { | ||
| let m = parse(&["--environment=production"]).unwrap(); | ||
| assert_eq!( | ||
| m.get_one::<String>("environment").map(|s| s.as_str()), | ||
| Some("production") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn all_flag() { | ||
| let m = parse(&["--all"]).unwrap(); | ||
| assert!(m.get_flag("all")); | ||
| assert_eq!(m.get_one::<String>("environment"), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn environment_and_all_conflict() { | ||
| let err = parse(&["--environment", "production", "--all"]).unwrap_err(); | ||
| assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); | ||
| } | ||
|
|
||
| #[test] | ||
| fn all_and_environment_conflict() { | ||
| let err = parse(&["--all", "--environment", "staging"]).unwrap_err(); | ||
| assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); | ||
| } | ||
|
|
||
| #[test] | ||
| fn create_flag_with_environment() { | ||
| let m = parse(&["--create", "--environment", "production"]).unwrap(); | ||
| assert!(m.get_flag("create")); | ||
| assert_eq!( | ||
| m.get_one::<String>("environment").map(|s| s.as_str()), | ||
| Some("production") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn create_flag_with_all() { | ||
| let m = parse(&["--create", "--all"]).unwrap(); | ||
| assert!(m.get_flag("create")); | ||
| assert!(m.get_flag("all")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn dir_with_environment() { | ||
| let m = parse(&["-d", "/tmp/myapp", "-e", "production"]).unwrap(); | ||
| assert_eq!( | ||
| m.get_one::<String>("dir").map(|s| s.as_str()), | ||
| Some("/tmp/myapp") | ||
| ); | ||
| assert_eq!( | ||
| m.get_one::<String>("environment").map(|s| s.as_str()), | ||
| Some("production") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn help_flag_shows_help() { | ||
| let err = parse(&["--help"]).unwrap_err(); | ||
| assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ use tower_package::{compute_sha256_file, Package}; | |
| use tower_telemetry::debug; | ||
|
|
||
| use tower_api::apis::configuration::Configuration; | ||
| use tower_api::apis::urlencode; | ||
| use tower_api::apis::default_api::DeployAppError; | ||
| use tower_api::apis::Error; | ||
| use tower_api::apis::ResponseContent; | ||
|
|
@@ -96,6 +97,8 @@ pub async fn deploy_app_package( | |
| api_config: &tower_api::apis::configuration::Configuration, | ||
| app_name: &str, | ||
| package: Package, | ||
| environment: Option<&str>, | ||
| all_environments: bool, | ||
| ) -> Result<DeployAppResponse, Error<DeployAppError>> { | ||
| let progress_bar = Arc::new(Mutex::new(output::progress_bar("Deploying to Tower..."))); | ||
|
|
||
|
|
@@ -116,7 +119,14 @@ pub async fn deploy_app_package( | |
|
|
||
| // Create the URL for the API endpoint | ||
| let base_url = &api_config.base_path; | ||
| let url = format!("{}/apps/{}/deploy", base_url, app_name); | ||
| let url = if all_environments { | ||
| format!("{}/apps/{}/deploy?all_environments=true", base_url, app_name) | ||
| } else if let Some(env) = environment { | ||
| let encoded_environment = urlencode(env); | ||
| format!("{}/apps/{}/deploy?environment={}", base_url, app_name, encoded_environment) | ||
|
Comment on lines
+125
to
+126
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this lead to some environments that are impossible to use via the CLI as we don't url encode them on the other side? (or do we url_decode the env name?) |
||
| } else { | ||
| format!("{}/apps/{}/deploy", base_url, app_name) | ||
| }; | ||
|
bradhe marked this conversation as resolved.
|
||
|
|
||
| // Upload the package | ||
| let response = upload_file_with_progress( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.