From 23f834786ac080254663c7ec8253c2a913621de3 Mon Sep 17 00:00:00 2001 From: Hironori Yamamoto Date: Sat, 30 Jul 2022 23:24:47 +0900 Subject: [PATCH] :sparkles: implement `__default` option to auth commands --- Cargo.toml | 2 +- README.md | 3 ++ src/aws.rs | 17 +++++++--- src/configs.rs | 85 ++++++++++++++++++++++++++++++------------------- tests/aws.rs | 25 +++++++++++---- tests/common.rs | 34 +++++++++++++++----- 6 files changed, 113 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2a9f25..c32b82a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ config = {version = "0.13.1", features = ["ini", "yaml"]} dirs = "4.0.0" handlebars = "4.3.3" log = "0.4.17" +maplit = "1.0.2" once_cell = "1.13.0" regex = "1.6.0" rust-ini = "0.18.0" @@ -28,6 +29,5 @@ skim = "0.9.4" thiserror = "1.0.31" [dev-dependencies] -maplit = "1.0.2" rstest = "0.15.0" tempfile = "3.3.0" diff --git a/README.md b/README.md index ba8f69d..1a73807 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ auth_commands: bar: | # In this case, name of one-login configuration is same as `profile` onelogin-aws-login -C {{profile}} --profile {{profile}} -u user@example.com + # default configuration for profiles without auth configuration + __default: | + aws configure --profile {{profile}} ``` ### Configure Completion diff --git a/src/aws.rs b/src/aws.rs index bf44c51..16c8698 100644 --- a/src/aws.rs +++ b/src/aws.rs @@ -40,13 +40,20 @@ impl> AWS<'_, P> { impl> ctx::CTX for AWS<'_, P> { fn auth(&self, profile: &str) -> Result { - let script_template = self.configs.auth_commands.get(profile).ok_or_else(|| { - ctx::CTXError::NoAuthConfiguration { + let script_template = self + .configs + .auth_commands + .get(profile) + // fallback to default configuration if a commend for the profile is not found + .or_else(|| { + self.configs + .auth_commands + .get(Configs::DEFAULT_AUTH_COMMAND_KEY) + }) + .ok_or_else(|| ctx::CTXError::NoAuthConfiguration { profile: profile.to_string(), source: None, - } - })?; - + })?; let script = self .reg .render_template(script_template, &json!({ "profile": profile })) diff --git a/src/configs.rs b/src/configs.rs index 3152b89..5ba5c82 100644 --- a/src/configs.rs +++ b/src/configs.rs @@ -1,10 +1,11 @@ use dirs::home_dir; +use maplit::hashmap; use std::fs; use std::io::Write; use std::path::PathBuf; use std::{collections::HashMap, path::Path}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use config::{Config, File, FileFormat}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -19,7 +20,27 @@ pub static CONFIGS_PATH: Lazy = Lazy::new(|| { path.push(".awsctx/configs.yaml"); path }); -const CONFIGS_DESCRIPTIONS: &str = r#"# # Configurations for awsctx + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Configs { + pub auth_commands: HashMap, +} + +impl Default for Configs { + fn default() -> Self { + Self { + auth_commands: hashmap! { + Self::DEFAULT_AUTH_COMMAND_KEY.to_string() => r#"echo "This is default configuration for auth commands." +echo "You can edit this configuration on ~/.awsctx/configs.yaml according to your needs." +aws configure --profile {{profile}} +"#.to_string(), + }, + } + } +} + +impl Configs { + const CONFIGS_DESCRIPTIONS: &'static str = r#"# # Configurations for awsctx # # You can manually edit configurations according to the following usage # # To use subcommand `auth` or `refresh`, fill the below configs for each profile. @@ -33,14 +54,13 @@ const CONFIGS_DESCRIPTIONS: &str = r#"# # Configurations for awsctx # bar: | # # In this case, name of one-login configuration is same as `profile` # onelogin-aws-login -C {{profile}} --profile {{profile}} -u user@example.com +# # default configuration for profiles without auth configuration +# __default: | +# aws configure --profile {{profile}} "#; -#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct Configs { - pub auth_commands: HashMap, -} + pub const DEFAULT_AUTH_COMMAND_KEY: &'static str = "__default"; -impl Configs { pub fn load_configs>(path: Option

) -> Result { let path = path .map(|p| p.as_ref().to_path_buf()) @@ -80,22 +100,29 @@ impl Configs { return Self::load_configs(Some(path)); } // if the config directory does not exist, create the directory recursively - match path.parent() { - Some(parent) => fs::create_dir_all(parent) - .context("failed to create a directory of a configuration file") - .map_err(|e| ctx::CTXError::UnexpectedError { source: Some(e) })?, - None => (), - } + path.parent() + .map_or_else( + || { + Err(anyhow!( + "no parent directory found for config path: {}", + path.to_str().unwrap() + )) + }, + |parent| fs::create_dir_all(parent).context("failed to create config directory"), + ) + .map_err(|e| ctx::CTXError::UnexpectedError { source: Some(e) })?; + let c = Configs::default(); let mut file = fs::File::create(&path) .context("failed to create a configuration file") .map_err(|e| ctx::CTXError::UnexpectedError { source: Some(e) })?; - file.write_all(CONFIGS_DESCRIPTIONS.as_bytes()) + file.write_all(Self::CONFIGS_DESCRIPTIONS.as_bytes()) .context("failed to write a configuration file") .map_err(|e| ctx::CTXError::UnexpectedError { source: Some(e) })?; + let mut ser = serde_yaml::Serializer::new(&mut file); c.serialize(&mut ser) - .context("failed to write a configuration file") + .context("failed to serialize configuration") .map_err(|e| ctx::CTXError::UnexpectedError { source: Some(e) })?; file.flush() .context("failed to flush a configuration file") @@ -116,25 +143,10 @@ mod tests { #[fixture] pub fn configs_text() -> String { - r#"# # Configurations for awsctx -# # You can manually edit configurations according to the following usage - -# # To use subcommand `auth` or `refresh`, fill the below configs for each profile. -# auth_commands: -# # configuration for `foo` profile with aws configure -# foo: | -# # you can use pre-defined parameter `{{profile}}` which is replaced by key of this block -# # In this case, `{{profile}}` is replaced by `foo` -# aws configure --profile {{profile}} -# # configuration for `bar` profile with [onelogin-aws-cli](https://github.com/physera/onelogin-aws-cli) -# bar: | -# # In this case, name of one-login configuration is same as `profile` -# onelogin-aws-login -C {{profile}} --profile {{profile}} -u user@example.com ---- -auth_commands: + r#"auth_commands: foo: | echo 1"# - .to_string() + .to_string() } #[fixture] @@ -217,7 +229,14 @@ auth_commands: # bar: | # # In this case, name of one-login configuration is same as `profile` # onelogin-aws-login -C {{profile}} --profile {{profile}} -u user@example.com -auth_commands: {} +# # default configuration for profiles without auth configuration +# __default: | +# aws configure --profile {{profile}} +auth_commands: + __default: | + echo "This is default configuration for auth commands." + echo "You can edit this configuration on ~/.awsctx/configs.yaml according to your needs." + aws configure --profile {{profile}} "#; let actual = fs::read_to_string(tmpfile).unwrap(); assert_eq!(expect, actual); diff --git a/tests/aws.rs b/tests/aws.rs index d813f25..ee4f087 100644 --- a/tests/aws.rs +++ b/tests/aws.rs @@ -7,21 +7,31 @@ use tempfile::NamedTempFile; mod common; use common::*; -#[rstest(input, expect)] +#[rstest(configs, input, expect)] #[case( + configs(), "foo", Ok(ctx::Context {name: "foo".to_string(), active: true}), )] #[case( + configs(), "bar", Err(ctx::CTXError::InvalidConfigurations { message: "failed to execute an auth script of profile (bar), check configurations".to_string(), source: None }), )] +// baz is not defined in configs.auth_commands and default is set #[case( - "unknown", - Err(ctx::CTXError::NoAuthConfiguration{ profile: "unknown".to_string(), source: None }), + configs(), + "baz", + Ok(ctx::Context {name: "baz".to_string(), active: true}), +)] +// baz is not defined in configs.auth_commands and default is not set +#[case( + configs_without_default(), + "baz", + Err(ctx::CTXError::NoAuthConfiguration{ profile: "baz".to_string(), source: None }), )] fn test_aws_auth( configs: Rc, @@ -31,10 +41,10 @@ fn test_aws_auth( ) { let aws: &dyn ctx::CTX = &AWS::new(configs, aws_credentials.path()).unwrap(); let actual = aws.auth(input); - match (expect, actual) { + match (&expect, &actual) { (Ok(expect), Ok(actual)) => { assert_eq!(expect, actual); - assert_eq!(expect, aws.get_active_context().unwrap()) + assert_eq!(expect, &aws.get_active_context().unwrap()) } (Err(expect), Err(actual)) => match (&expect, &actual) { ( @@ -63,7 +73,10 @@ fn test_aws_auth( } _ => panic!("unexpected error: {}", actual), }, - _ => panic!("expect and actual are not match"), + _ => panic!( + "expect and actual are not match: expect: {:?}, actual: {:?}", + &expect, &actual + ), } } diff --git a/tests/common.rs b/tests/common.rs index fa97566..23fc39f 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,9 +1,9 @@ use std::{ - collections::HashMap, io::{Seek, SeekFrom, Write}, rc::Rc, }; +use maplit::hashmap; use rstest::*; use tempfile::NamedTempFile; @@ -11,7 +11,12 @@ use awsctx::{configs::Configs, creds::Credentials, ctx}; #[fixture] pub fn aws_credentials_text() -> String { - r#"[bar] + r#"[baz] +aws_access_key_id=ZZZZZZZZZZZ +aws_secret_access_key=ZZZZZZZZZZZ +aws_session_token=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ + +[bar] aws_access_key_id=YYYYYYYYYYY aws_secret_access_key=YYYYYYYYYYY aws_session_token=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY @@ -70,6 +75,10 @@ pub fn contexts() -> Vec { name: "bar".to_string(), active: false, }, + ctx::Context { + name: "baz".to_string(), + active: false, + }, ctx::Context { name: "foo".to_string(), active: true, @@ -94,11 +103,20 @@ pub fn contexts_without_default() -> Vec { #[fixture] pub fn configs() -> Rc { Rc::new(Configs { - auth_commands: vec![ - ("foo".to_string(), "echo auth".to_string()), - ("bar".to_string(), "exit 1".to_string()), - ] - .into_iter() - .collect::>(), + auth_commands: hashmap! { + "foo".to_string() => "echo auth".to_string(), + "bar".to_string() => "exit 1".to_string(), + Configs::DEFAULT_AUTH_COMMAND_KEY.to_string() => "echo default auth".to_string(), + }, + }) +} + +#[fixture] +pub fn configs_without_default() -> Rc { + Rc::new(Configs { + auth_commands: hashmap! { + "foo".to_string() => "echo auth".to_string(), + "bar".to_string() => "exit 1".to_string(), + }, }) }