Skip to content

Commit b1e8ec6

Browse files
committed
prompt aws profile and fallback to input
1 parent 74528df commit b1e8ec6

5 files changed

Lines changed: 164 additions & 34 deletions

File tree

internal/context/init.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,42 +171,87 @@ func getProjectPrompts(projectName string, modules map[string]moduleconfig.Modul
171171
return handlers
172172
}
173173

174-
func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, moduleConfigs map[string]moduleconfig.ModuleConfig) map[string][]PromptHandler {
174+
func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, moduleConfigs map[string]moduleconfig.ModuleConfig) []CredentialPrompts {
175175
var uniqueVendors []string
176176
for _, module := range moduleConfigs {
177177
uniqueVendors = appendToSet(uniqueVendors, module.RequiredCredentials)
178178
}
179+
179180
// map is to keep track of which vendor they belong to, to fill them back into the projectConfig
180-
prompts := map[string][]PromptHandler{}
181-
for _, vendor := range uniqueVendors {
182-
prompts[vendor] = mapVendorToPrompts(projectCredentials, vendor)
181+
prompts := []CredentialPrompts{}
182+
for _, vendor := range AvailableVendorOrders {
183+
if itemInSlice(uniqueVendors, vendor) {
184+
vendorPrompts := CredentialPrompts{vendor, mapVendorToPrompts(projectCredentials, vendor)}
185+
prompts = append(prompts, vendorPrompts)
186+
}
183187
}
184188
return prompts
185189
}
186190

187191
func mapVendorToPrompts(projectCred globalconfig.ProjectCredential, vendor string) []PromptHandler {
188192
var prompts []PromptHandler
193+
profiles, err := project.GetAWSProfiles()
194+
if err != nil {
195+
profiles = []string{}
196+
}
197+
198+
// if no profiles available, dont prompt use to pick profile
199+
customAwsPickProfileCondition := func(param map[string]string) bool {
200+
if len(profiles) == 0 {
201+
flog.Infof(":warning: No AWS profiles found, please manually input AWS credentials")
202+
return false
203+
} else {
204+
return true
205+
}
206+
}
207+
208+
// condition for prompting manual AWS credentials input
209+
customAwsMustInputCondition := func(param map[string]string) bool {
210+
toPickProfile := awsPickProfile
211+
if val, ok := param["use_aws_profile"]; ok && val != toPickProfile {
212+
return true
213+
}
214+
return false
215+
}
189216

190217
switch vendor {
191218
case "aws":
192219
awsPrompts := []PromptHandler{
220+
{
221+
moduleconfig.Parameter{
222+
Field: "use_aws_profile",
223+
Label: "Use credentials from existing AWS profiles?",
224+
Options: []string{awsPickProfile, awsManualInputCredentials},
225+
},
226+
customAwsPickProfileCondition,
227+
NoValidation,
228+
},
229+
{
230+
moduleconfig.Parameter{
231+
Field: "aws_profile",
232+
Label: "Select AWS Profile",
233+
Options: profiles,
234+
},
235+
KeyMatchCondition("use_aws_profile", awsPickProfile),
236+
NoValidation,
237+
},
193238
{
194239
moduleconfig.Parameter{
195240
Field: "accessKeyId",
196241
Label: "AWS Access Key ID",
197242
Default: projectCred.AWSResourceConfig.AccessKeyId,
198243
},
199-
NoCondition,
200-
NoValidation,
244+
CustomCondition(customAwsMustInputCondition),
245+
project.ValidateAKID,
201246
},
202247
{
203248
moduleconfig.Parameter{
204249
Field: "secretAccessKey",
205250
Label: "AWS Secret access key",
206251
Default: projectCred.AWSResourceConfig.SecretAccessKey,
207252
},
208-
NoCondition,
209-
NoValidation,
253+
CustomCondition(customAwsMustInputCondition),
254+
project.ValidateSAK,
210255
},
211256
}
212257
prompts = append(prompts, awsPrompts...)

internal/context/prompts.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,48 @@ import (
1010

1111
"github.com/commitdev/zero/internal/config/globalconfig"
1212
"github.com/commitdev/zero/internal/config/moduleconfig"
13+
"github.com/commitdev/zero/pkg/credentials"
1314
"github.com/commitdev/zero/pkg/util/exit"
1415
"github.com/manifoldco/promptui"
1516
"gopkg.in/yaml.v2"
1617
)
1718

19+
// Constant to maintain prompt orders so users can have the same flow,
20+
// modules get downloaded asynchronously therefore its easier to just hardcode an order
21+
var AvailableVendorOrders = []string{"aws", "github", "circleci"}
22+
23+
const awsPickProfile = "Existing AWS Profiles"
24+
const awsManualInputCredentials = "Enter my own AWS credentials"
25+
1826
type PromptHandler struct {
1927
moduleconfig.Parameter
20-
Condition func(map[string]string) bool
28+
Condition CustomConditionSignature
2129
Validate func(string) error
2230
}
2331

32+
type CredentialPrompts struct {
33+
Vendor string
34+
Prompts []PromptHandler
35+
}
36+
37+
type CustomConditionSignature func(map[string]string) bool
38+
2439
func NoCondition(map[string]string) bool {
2540
return true
2641
}
2742

28-
func KeyMatchCondition(key string, value string) func(map[string]string) bool {
43+
func KeyMatchCondition(key string, value string) CustomConditionSignature {
2944
return func(param map[string]string) bool {
3045
return param[key] == value
3146
}
3247
}
3348

49+
func CustomCondition(fn CustomConditionSignature) CustomConditionSignature {
50+
return func(param map[string]string) bool {
51+
return fn(param)
52+
}
53+
}
54+
3455
func NoValidation(string) error {
3556
return nil
3657
}
@@ -150,24 +171,32 @@ func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[s
150171
return parameters, nil
151172
}
152173

153-
func promptCredentialsAndFillProjectCreds(credentialPrompts map[string][]PromptHandler, credentials globalconfig.ProjectCredential) globalconfig.ProjectCredential {
174+
func promptCredentialsAndFillProjectCreds(credentialPrompts []CredentialPrompts, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
154175
promptsValues := map[string]map[string]string{}
155176

156-
for vendor, prompts := range credentialPrompts {
177+
for _, prompts := range credentialPrompts {
178+
vendor := prompts.Vendor
157179
vendorPromptValues := map[string]string{}
158180

159181
// vendors like AWS have multiple prompts (accessKeyId and secretAccessKey)
160-
for _, prompt := range prompts {
161-
vendorPromptValues[prompt.Field] = prompt.GetParam(map[string]string{})
182+
for _, prompt := range prompts.Prompts {
183+
vendorPromptValues[prompt.Field] = prompt.GetParam(vendorPromptValues)
162184
}
163185
promptsValues[vendor] = vendorPromptValues
164186
}
165187

166188
// FIXME: what is a good way to dynamically modify partial data of a struct
167189
// current just marashing to yaml, then unmarshaling into the base struct
168190
yamlContent, _ := yaml.Marshal(promptsValues)
169-
yaml.Unmarshal(yamlContent, &credentials)
170-
return credentials
191+
yaml.Unmarshal(yamlContent, &creds)
192+
193+
// Fill AWS credentials based on profile from ~/.aws/credentials
194+
if val, ok := promptsValues["aws"]; ok {
195+
if val["use_aws_profile"] == awsPickProfile {
196+
creds = credentials.GetAWSProfileProjectCredentials(val["aws_profile"], creds)
197+
}
198+
}
199+
return creds
171200
}
172201

173202
func appendToSet(set []string, toAppend []string) []string {

pkg/credentials/credentials.go

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"regexp"
1212

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

41+
func AwsCredsPath() string {
42+
usr, err := user.Current()
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
return filepath.Join(usr.HomeDir, ".aws/credentials")
47+
}
48+
49+
func GetAWSProfileProjectCredentials(profileName string, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
50+
awsPath := AwsCredsPath()
51+
return GetAWSProfileCredentials(awsPath, profileName, creds)
52+
}
53+
54+
func GetAWSProfileCredentials(credsPath string, profileName string, creds globalconfig.ProjectCredential) globalconfig.ProjectCredential {
55+
awsCreds, err := credentials.NewSharedCredentials(credsPath, profileName).Get()
56+
if err != nil {
57+
log.Fatal(err)
58+
}
59+
creds.AWSResourceConfig.AccessKeyId = awsCreds.AccessKeyID
60+
creds.AWSResourceConfig.SecretAccessKey = awsCreds.SecretAccessKey
61+
return creds
62+
}
63+
4064
func GetSecrets(baseDir string) Secrets {
4165

4266
secretsFile := filepath.Join(baseDir, "secrets.yaml")
@@ -153,29 +177,28 @@ func writeSecrets(secretsFile string, s Secrets) {
153177
}
154178
}
155179

156-
func promptAWSCredentials(secrets *Secrets) {
157-
158-
validateAKID := func(input string) error {
159-
// 20 uppercase alphanumeric characters
160-
var awsAccessKeyIDPat = regexp.MustCompile(`^[A-Z0-9]{20}$`)
161-
if !awsAccessKeyIDPat.MatchString(input) {
162-
return errors.New("Invalid aws_access_key_id")
163-
}
164-
return nil
180+
func ValidateAKID(input string) error {
181+
// 20 uppercase alphanumeric characters
182+
var awsAccessKeyIDPat = regexp.MustCompile(`^[A-Z0-9]{20}$`)
183+
if !awsAccessKeyIDPat.MatchString(input) {
184+
return errors.New("Invalid aws_access_key_id")
165185
}
186+
return nil
187+
}
166188

167-
validateSAK := func(input string) error {
168-
// 40 base64 characters
169-
var awsSecretAccessKeyPat = regexp.MustCompile(`^[A-Za-z0-9/+=]{40}$`)
170-
if !awsSecretAccessKeyPat.MatchString(input) {
171-
return errors.New("Invalid aws_secret_access_key")
172-
}
173-
return nil
189+
func ValidateSAK(input string) error {
190+
// 40 base64 characters
191+
var awsSecretAccessKeyPat = regexp.MustCompile(`^[A-Za-z0-9/+=]{40}$`)
192+
if !awsSecretAccessKeyPat.MatchString(input) {
193+
return errors.New("Invalid aws_secret_access_key")
174194
}
195+
return nil
196+
}
175197

198+
func promptAWSCredentials(secrets *Secrets) {
176199
accessKeyIDPrompt := promptui.Prompt{
177200
Label: "Aws Access Key ID ",
178-
Validate: validateAKID,
201+
Validate: ValidateAKID,
179202
}
180203

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

188211
secretAccessKeyPrompt := promptui.Prompt{
189212
Label: "Aws Secret Access Key ",
190-
Validate: validateSAK,
213+
Validate: ValidateSAK,
191214
Mask: '*',
192215
}
193216

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package credentials_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/commitdev/zero/internal/config/globalconfig"
7+
"github.com/commitdev/zero/pkg/credentials"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestFillAWSProfileCredentials(t *testing.T) {
12+
mockAwsCredentialFilePath := "../../tests/test_data/aws/mock_credentials.yml"
13+
t.Run("fills project credentials", func(t *testing.T) {
14+
projectCreds := globalconfig.ProjectCredential{}
15+
projectCreds = credentials.GetAWSProfileCredentials(mockAwsCredentialFilePath, "default", projectCreds)
16+
assert.Equal(t, "MOCK1_ACCESS_KEY", projectCreds.AWSResourceConfig.AccessKeyId)
17+
assert.Equal(t, "MOCK1_SECRET_ACCESS_KEY", projectCreds.AWSResourceConfig.SecretAccessKey)
18+
})
19+
20+
t.Run("supports non-default profiles", func(t *testing.T) {
21+
projectCreds := globalconfig.ProjectCredential{}
22+
projectCreds = credentials.GetAWSProfileCredentials(mockAwsCredentialFilePath, "foobar", projectCreds)
23+
assert.Equal(t, "MOCK2_ACCESS_KEY", projectCreds.AWSResourceConfig.AccessKeyId)
24+
assert.Equal(t, "MOCK2_SECRET_ACCESS_KEY", projectCreds.AWSResourceConfig.SecretAccessKey)
25+
})
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[default]
2+
aws_access_key_id=MOCK1_ACCESS_KEY
3+
aws_secret_access_key=MOCK1_SECRET_ACCESS_KEY
4+
5+
[foobar]
6+
aws_access_key_id=MOCK2_ACCESS_KEY
7+
aws_secret_access_key=MOCK2_SECRET_ACCESS_KEY

0 commit comments

Comments
 (0)