From c838d646457cc486504a551575946d0ce4cd6c62 Mon Sep 17 00:00:00 2001 From: Adam Rodger Date: Fri, 24 Jul 2020 21:32:43 +0100 Subject: [PATCH] Interactive create command. Closes #26 --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++- gctx/Cargo.toml | 3 +- gctx/src/arguments.rs | 13 ++++++--- gctx/src/commands.rs | 58 +++++++++++++++++++++++++++++++++++++- gctx/src/main.rs | 11 +++++--- gctx/tests/cli.rs | 65 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e59c3c..1d298cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "console" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "termios", + "unicode-width", + "winapi", + "winapi-util", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -169,6 +186,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "dialoguer" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aa86af7b19b40ef9cbef761ed411a49f0afa06b7b6dcd3dfe2f96a3c546138" +dependencies = [ + "console", + "lazy_static", + "tempfile", +] + [[package]] name = "difference" version = "2.0.0" @@ -202,6 +230,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "float-cmp" version = "0.6.0" @@ -232,13 +266,14 @@ dependencies = [ [[package]] name = "gctx" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "assert_cmd", "assert_fs", "clap", "colored", + "dialoguer", "gcloud-ctx", "predicates", "which", @@ -644,6 +679,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a14cd9f8c72704232f0bfc8455c0e861f0ad4eb60cc9ec8a170e231414c1e13" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" +dependencies = [ + "libc", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/gctx/Cargo.toml b/gctx/Cargo.toml index 2f5bfb0..deef91e 100644 --- a/gctx/Cargo.toml +++ b/gctx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gctx" -version = "0.4.0" +version = "0.5.0" authors = ["Adam Rodger "] edition = "2018" description = "A gcloud configuration management utility" @@ -17,6 +17,7 @@ categories = ["command-line-utilities", "config"] anyhow = "1" clap = "3.0.0-beta.1" colored = "2" +dialoguer = "0.6" gcloud-ctx = { path = "../gcloud-ctx", version = "0.3" } which = "4" diff --git a/gctx/src/arguments.rs b/gctx/src/arguments.rs index 837d602..6b2cfe7 100644 --- a/gctx/src/arguments.rs +++ b/gctx/src/arguments.rs @@ -38,20 +38,25 @@ pub enum SubCommand { /// Create a new configuration Create { + /// Create a configuration interactively + #[clap(short, long, conflicts_with_all(&["name", "project", "account", "zone", "region", "activate", "force"]))] + interactive: bool, + // Name of the new configuration - name: String, + #[clap(required_unless("interactive"), requires_all(&["project", "account", "zone"]))] + name: Option, /// Setting for core/project #[clap(short, long)] - project: String, + project: Option, /// Setting for core/account #[clap(short, long)] - account: String, + account: Option, /// Setting for compute/zone #[clap(short, long)] - zone: String, + zone: Option, /// Setting for compute/region #[clap(short, long)] diff --git a/gctx/src/commands.rs b/gctx/src/commands.rs index 1ca7f68..d8f37a3 100644 --- a/gctx/src/commands.rs +++ b/gctx/src/commands.rs @@ -1,5 +1,6 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use colored::*; +use dialoguer::{Confirm, Input}; use gcloud_ctx::{ConfigurationStore, ConflictAction, PropertiesBuilder}; /// Used to control whether to activate a configuration after creation @@ -66,6 +67,61 @@ pub fn copy(src_name: &str, dest_name: &str, conflict: ConflictAction, activate: Ok(()) } +/// Create a new configuration interactively +pub fn create_interactive() -> Result<()> { + let store = ConfigurationStore::with_default_location()?; + + let name = Input::::new() + .with_prompt("Name".blue().to_string()) + .interact()?; + + if store.find_by_name(&name).is_some() { + let prompt = "A configuration with the same name already exists. Overwrite?" + .yellow() + .to_string(); + let confirm = Confirm::new().with_prompt(prompt).default(false).interact()?; + + if !confirm { + bail!("Operation cancelled".yellow()); + } + } + + let project = Input::::new() + .with_prompt("Project".blue().to_string()) + .interact()?; + + let account = Input::::new() + .with_prompt("Account".blue().to_string()) + .interact()?; + + let zone = Input::::new() + .with_prompt("Zone".blue().to_string()) + .interact()?; + + let region = Input::::new() + .with_prompt("Region (optional)".blue().to_string()) + .allow_empty(true) + .interact()?; + let region = if region.is_empty() { None } else { Some(region) }; + + let activate = Confirm::new() + .with_prompt("Activate".blue().to_string()) + .default(false) + .interact()?; + + create( + &name, + &project, + &account, + &zone, + region.as_deref(), + ConflictAction::Overwrite, + activate.into(), + )?; + + Ok(()) +} + /// Create a new configuration pub fn create( name: &str, diff --git a/gctx/src/main.rs b/gctx/src/main.rs index 72437fc..a65d4de 100644 --- a/gctx/src/main.rs +++ b/gctx/src/main.rs @@ -33,7 +33,9 @@ pub fn run(opts: Opts) -> Result<()> { } => { commands::copy(&src_name, &dest_name, force.into(), activate.into())?; } + SubCommand::Create { interactive: true, .. } => commands::create_interactive()?, SubCommand::Create { + interactive: false, name, project, account, @@ -43,10 +45,11 @@ pub fn run(opts: Opts) -> Result<()> { force, } => { commands::create( - &name, - &project, - &account, - &zone, + // safe to unwrap these because they are set as required in clap + &name.unwrap(), + &project.unwrap(), + &account.unwrap(), + &zone.unwrap(), region.as_deref(), force.into(), activate.into(), diff --git a/gctx/tests/cli.rs b/gctx/tests/cli.rs index ebfe772..b34dd2b 100644 --- a/gctx/tests/cli.rs +++ b/gctx/tests/cli.rs @@ -424,6 +424,71 @@ fn create_without_force_fails() { tmp.close().unwrap(); } +#[test] +#[ignore] // TODO: this doesn't work because assert_cmd doesn't support interactive programs +fn create_interactive_with_activate() { + let (mut cli, tmp) = TempConfigurationStore::new() + .unwrap() + .with_config_activated("foo") + .build() + .unwrap(); + + #[rustfmt::skip] + cli.arg("create").arg("--interactive") + .write_stdin("bar") // name + .write_stdin("my-project") // project + .write_stdin("a.user@example.org") // account + .write_stdin("europe-west1-d") // zone + .write_stdin("us-east1") // region + .write_stdin("y"); // activate + + cli.assert() + .success() + .stdout("Successfully created configuration 'bar'\n"); + + tmp.child("active_config").assert("bar"); + + #[rustfmt::skip] + tmp.child("configurations/config_bar").assert([ + "[core]", + "project=my-project", + "account=a.user@example.org", + "[compute]", + "zone=europe-west1-d", + "region=us-east1", + "" + ].join("\n")); + + tmp.close().unwrap(); +} + +#[test] +#[ignore] // TODO: this doesn't work because assert_cmd doesn't support interactive programs +fn create_interactive_without_activate() { + let (mut cli, tmp) = TempConfigurationStore::new() + .unwrap() + .with_config_activated("foo") + .build() + .unwrap(); + + #[rustfmt::skip] + cli.arg("create").arg("--interactive") + .write_stdin("bar") // name + .write_stdin("my-project") // project + .write_stdin("a.user@example.org") // account + .write_stdin("europe-west1-d") // zone + .write_stdin("us-east1") // region + .write_stdin("n"); // activate + + cli.assert() + .success() + .stdout("Successfully created configuration 'bar'\n"); + + tmp.child("active_config").assert("foo"); + + tmp.close().unwrap(); +} + #[test] fn describe_with_name_shows_supported_properties() { let (mut cli, tmp) = TempConfigurationStore::new()