Skip to content

Commit

Permalink
prompt aws profile and fallback to input
Browse files Browse the repository at this point in the history
  • Loading branch information
davidcheung committed Jun 23, 2020
1 parent 74528df commit b1e8ec6
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 34 deletions.
61 changes: 53 additions & 8 deletions internal/context/init.go
Expand Up @@ -171,42 +171,87 @@ func getProjectPrompts(projectName string, modules map[string]moduleconfig.Modul
return handlers
}

func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, moduleConfigs map[string]moduleconfig.ModuleConfig) map[string][]PromptHandler {
func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, moduleConfigs map[string]moduleconfig.ModuleConfig) []CredentialPrompts {
var uniqueVendors []string
for _, module := range moduleConfigs {
uniqueVendors = appendToSet(uniqueVendors, module.RequiredCredentials)
}

// map is to keep track of which vendor they belong to, to fill them back into the projectConfig
prompts := map[string][]PromptHandler{}
for _, vendor := range uniqueVendors {
prompts[vendor] = mapVendorToPrompts(projectCredentials, vendor)
prompts := []CredentialPrompts{}
for _, vendor := range AvailableVendorOrders {
if itemInSlice(uniqueVendors, vendor) {
vendorPrompts := CredentialPrompts{vendor, mapVendorToPrompts(projectCredentials, vendor)}
prompts = append(prompts, vendorPrompts)
}
}
return prompts
}

func mapVendorToPrompts(projectCred globalconfig.ProjectCredential, vendor string) []PromptHandler {
var prompts []PromptHandler
profiles, err := project.GetAWSProfiles()
if err != nil {
profiles = []string{}
}

// if no profiles available, dont prompt use to pick profile
customAwsPickProfileCondition := func(param map[string]string) bool {
if len(profiles) == 0 {
flog.Infof(":warning: No AWS profiles found, please manually input AWS credentials")
return false
} else {
return true
}
}

// condition for prompting manual AWS credentials input
customAwsMustInputCondition := func(param map[string]string) bool {
toPickProfile := awsPickProfile
if val, ok := param["use_aws_profile"]; ok && val != toPickProfile {
return true
}
return false
}

switch vendor {
case "aws":
awsPrompts := []PromptHandler{
{
moduleconfig.Parameter{
Field: "use_aws_profile",
Label: "Use credentials from existing AWS profiles?",
Options: []string{awsPickProfile, awsManualInputCredentials},
},
customAwsPickProfileCondition,
NoValidation,
},
{
moduleconfig.Parameter{
Field: "aws_profile",
Label: "Select AWS Profile",
Options: profiles,
},
KeyMatchCondition("use_aws_profile", awsPickProfile),
NoValidation,
},
{
moduleconfig.Parameter{
Field: "accessKeyId",
Label: "AWS Access Key ID",
Default: projectCred.AWSResourceConfig.AccessKeyId,
},
NoCondition,
NoValidation,
CustomCondition(customAwsMustInputCondition),
project.ValidateAKID,
},
{
moduleconfig.Parameter{
Field: "secretAccessKey",
Label: "AWS Secret access key",
Default: projectCred.AWSResourceConfig.SecretAccessKey,
},
NoCondition,
NoValidation,
CustomCondition(customAwsMustInputCondition),
project.ValidateSAK,
},
}
prompts = append(prompts, awsPrompts...)
Expand Down
45 changes: 37 additions & 8 deletions internal/context/prompts.go
Expand Up @@ -10,27 +10,48 @@ import (

"github.com/commitdev/zero/internal/config/globalconfig"
"github.com/commitdev/zero/internal/config/moduleconfig"
"github.com/commitdev/zero/pkg/credentials"
"github.com/commitdev/zero/pkg/util/exit"
"github.com/manifoldco/promptui"
"gopkg.in/yaml.v2"
)

// Constant to maintain prompt orders so users can have the same flow,
// modules get downloaded asynchronously therefore its easier to just hardcode an order
var AvailableVendorOrders = []string{"aws", "github", "circleci"}

const awsPickProfile = "Existing AWS Profiles"
const awsManualInputCredentials = "Enter my own AWS credentials"

type PromptHandler struct {
moduleconfig.Parameter
Condition func(map[string]string) bool
Condition CustomConditionSignature
Validate func(string) error
}

type CredentialPrompts struct {
Vendor string
Prompts []PromptHandler
}

type CustomConditionSignature func(map[string]string) bool

func NoCondition(map[string]string) bool {
return true
}

func KeyMatchCondition(key string, value string) func(map[string]string) bool {
func KeyMatchCondition(key string, value string) CustomConditionSignature {
return func(param map[string]string) bool {
return param[key] == value
}
}

func CustomCondition(fn CustomConditionSignature) CustomConditionSignature {
return func(param map[string]string) bool {
return fn(param)
}
}

func NoValidation(string) error {
return nil
}
Expand Down Expand Up @@ -150,24 +171,32 @@ func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[s
return parameters, nil
}

func promptCredentialsAndFillProjectCreds(credentialPrompts map[string][]PromptHandler, credentials globalconfig.ProjectCredential) globalconfig.ProjectCredential {
func promptCredentialsAndFillProjectCreds(credentialPrompts []CredentialPrompts, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
promptsValues := map[string]map[string]string{}

for vendor, prompts := range credentialPrompts {
for _, prompts := range credentialPrompts {
vendor := prompts.Vendor
vendorPromptValues := map[string]string{}

// vendors like AWS have multiple prompts (accessKeyId and secretAccessKey)
for _, prompt := range prompts {
vendorPromptValues[prompt.Field] = prompt.GetParam(map[string]string{})
for _, prompt := range prompts.Prompts {
vendorPromptValues[prompt.Field] = prompt.GetParam(vendorPromptValues)
}
promptsValues[vendor] = vendorPromptValues
}

// FIXME: what is a good way to dynamically modify partial data of a struct
// current just marashing to yaml, then unmarshaling into the base struct
yamlContent, _ := yaml.Marshal(promptsValues)
yaml.Unmarshal(yamlContent, &credentials)
return credentials
yaml.Unmarshal(yamlContent, &creds)

// Fill AWS credentials based on profile from ~/.aws/credentials
if val, ok := promptsValues["aws"]; ok {
if val["use_aws_profile"] == awsPickProfile {
creds = credentials.GetAWSProfileProjectCredentials(val["aws_profile"], creds)
}
}
return creds
}

func appendToSet(set []string, toAppend []string) []string {
Expand Down
59 changes: 41 additions & 18 deletions pkg/credentials/credentials.go
Expand Up @@ -11,6 +11,7 @@ import (
"regexp"

"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/commitdev/zero/internal/config/globalconfig"
"github.com/commitdev/zero/internal/config/projectconfig"
"github.com/manifoldco/promptui"
"gopkg.in/yaml.v2"
Expand All @@ -37,6 +38,29 @@ func MakeAwsEnvars(cfg *projectconfig.ZeroProjectConfig, awsSecrets Secrets) []s
return env
}

func AwsCredsPath() string {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
return filepath.Join(usr.HomeDir, ".aws/credentials")
}

func GetAWSProfileProjectCredentials(profileName string, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
awsPath := AwsCredsPath()
return GetAWSProfileCredentials(awsPath, profileName, creds)
}

func GetAWSProfileCredentials(credsPath string, profileName string, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
awsCreds, err := credentials.NewSharedCredentials(credsPath, profileName).Get()
if err != nil {
log.Fatal(err)
}
creds.AWSResourceConfig.AccessKeyId = awsCreds.AccessKeyID
creds.AWSResourceConfig.SecretAccessKey = awsCreds.SecretAccessKey
return creds
}

func GetSecrets(baseDir string) Secrets {

secretsFile := filepath.Join(baseDir, "secrets.yaml")
Expand Down Expand Up @@ -153,29 +177,28 @@ func writeSecrets(secretsFile string, s Secrets) {
}
}

func promptAWSCredentials(secrets *Secrets) {

validateAKID := func(input string) error {
// 20 uppercase alphanumeric characters
var awsAccessKeyIDPat = regexp.MustCompile(`^[A-Z0-9]{20}$`)
if !awsAccessKeyIDPat.MatchString(input) {
return errors.New("Invalid aws_access_key_id")
}
return nil
func ValidateAKID(input string) error {
// 20 uppercase alphanumeric characters
var awsAccessKeyIDPat = regexp.MustCompile(`^[A-Z0-9]{20}$`)
if !awsAccessKeyIDPat.MatchString(input) {
return errors.New("Invalid aws_access_key_id")
}
return nil
}

validateSAK := func(input string) error {
// 40 base64 characters
var awsSecretAccessKeyPat = regexp.MustCompile(`^[A-Za-z0-9/+=]{40}$`)
if !awsSecretAccessKeyPat.MatchString(input) {
return errors.New("Invalid aws_secret_access_key")
}
return nil
func ValidateSAK(input string) error {
// 40 base64 characters
var awsSecretAccessKeyPat = regexp.MustCompile(`^[A-Za-z0-9/+=]{40}$`)
if !awsSecretAccessKeyPat.MatchString(input) {
return errors.New("Invalid aws_secret_access_key")
}
return nil
}

func promptAWSCredentials(secrets *Secrets) {
accessKeyIDPrompt := promptui.Prompt{
Label: "Aws Access Key ID ",
Validate: validateAKID,
Validate: ValidateAKID,
}

accessKeyIDResult, err := accessKeyIDPrompt.Run()
Expand All @@ -187,7 +210,7 @@ func promptAWSCredentials(secrets *Secrets) {

secretAccessKeyPrompt := promptui.Prompt{
Label: "Aws Secret Access Key ",
Validate: validateSAK,
Validate: ValidateSAK,
Mask: '*',
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/credentials/credentials_test.go
@@ -0,0 +1,26 @@
package credentials_test

import (
"testing"

"github.com/commitdev/zero/internal/config/globalconfig"
"github.com/commitdev/zero/pkg/credentials"
"github.com/stretchr/testify/assert"
)

func TestFillAWSProfileCredentials(t *testing.T) {
mockAwsCredentialFilePath := "../../tests/test_data/aws/mock_credentials.yml"
t.Run("fills project credentials", func(t *testing.T) {
projectCreds := globalconfig.ProjectCredential{}
projectCreds = credentials.GetAWSProfileCredentials(mockAwsCredentialFilePath, "default", projectCreds)
assert.Equal(t, "MOCK1_ACCESS_KEY", projectCreds.AWSResourceConfig.AccessKeyId)
assert.Equal(t, "MOCK1_SECRET_ACCESS_KEY", projectCreds.AWSResourceConfig.SecretAccessKey)
})

t.Run("supports non-default profiles", func(t *testing.T) {
projectCreds := globalconfig.ProjectCredential{}
projectCreds = credentials.GetAWSProfileCredentials(mockAwsCredentialFilePath, "foobar", projectCreds)
assert.Equal(t, "MOCK2_ACCESS_KEY", projectCreds.AWSResourceConfig.AccessKeyId)
assert.Equal(t, "MOCK2_SECRET_ACCESS_KEY", projectCreds.AWSResourceConfig.SecretAccessKey)
})
}
7 changes: 7 additions & 0 deletions tests/test_data/aws/mock_credentials.yml
@@ -0,0 +1,7 @@
[default]
aws_access_key_id=MOCK1_ACCESS_KEY
aws_secret_access_key=MOCK1_SECRET_ACCESS_KEY

[foobar]
aws_access_key_id=MOCK2_ACCESS_KEY
aws_secret_access_key=MOCK2_SECRET_ACCESS_KEY

0 comments on commit b1e8ec6

Please sign in to comment.