From dea7ac16ef78404b9e6db8dc2b0fac14b2792f9a Mon Sep 17 00:00:00 2001 From: David Cheung Date: Mon, 15 Jun 2020 17:32:44 -0400 Subject: [PATCH] prompt global credentials according to modules --- internal/config/globalconfig/global_config.go | 2 +- .../config/globalconfig/global_config_test.go | 14 +-- internal/config/moduleconfig/module_config.go | 2 +- internal/context/init.go | 86 +++++++++++++++---- internal/context/prompts.go | 40 +++++++++ internal/module/module_test.go | 4 + tests/test_data/modules/ci/zero-module.yml | 5 ++ 7 files changed, 128 insertions(+), 25 deletions(-) diff --git a/internal/config/globalconfig/global_config.go b/internal/config/globalconfig/global_config.go index 7371d81af..0f1cf91ff 100644 --- a/internal/config/globalconfig/global_config.go +++ b/internal/config/globalconfig/global_config.go @@ -93,7 +93,7 @@ func readOrCreateUserCredentialsFile() []byte { return data } -func GetUserCredentials(targetProjectName string) ProjectCredential { +func GetProjectCredentials(targetProjectName string) ProjectCredential { projects := LoadUserCredentials() if val, ok := projects[targetProjectName]; ok { diff --git a/internal/config/globalconfig/global_config_test.go b/internal/config/globalconfig/global_config_test.go index 0a4737fcd..12c47eb4a 100644 --- a/internal/config/globalconfig/global_config_test.go +++ b/internal/config/globalconfig/global_config_test.go @@ -49,7 +49,7 @@ func TestReadOrCreateUserCredentialsFile(t *testing.T) { _, fileStateErr := os.Stat(credPath) assert.True(t, os.IsNotExist(fileStateErr), "File should not exist") // attempting to read the file should create the file - globalconfig.GetUserCredentials("any-project") + globalconfig.GetProjectCredentials("any-project") stats, err := os.Stat(credPath) assert.False(t, os.IsNotExist(err), "File should be created") @@ -63,7 +63,7 @@ func TestGetUserCredentials(t *testing.T) { t.Run("Fixture file should have existing project with creds", func(t *testing.T) { projectName := "my-project" - project := globalconfig.GetUserCredentials(projectName) + project := globalconfig.GetProjectCredentials(projectName) // Reading from fixtures: tests/test_data/configs/credentials.yml assert.Equal(t, "AKIAABCD", project.AWSResourceConfig.AccessKeyId) @@ -74,7 +74,7 @@ func TestGetUserCredentials(t *testing.T) { t.Run("Fixture file should support multiple projects", func(t *testing.T) { projectName := "another-project" - project := globalconfig.GetUserCredentials(projectName) + project := globalconfig.GetProjectCredentials(projectName) assert.Equal(t, "654", project.GithubResourceConfig.AccessToken) }) } @@ -86,18 +86,18 @@ func TestEditUserCredentials(t *testing.T) { t.Run("Should create new project if not exist", func(t *testing.T) { projectName := "test-project3" - project := globalconfig.GetUserCredentials(projectName) + project := globalconfig.GetProjectCredentials(projectName) project.AWSResourceConfig.AccessKeyId = "TEST_KEY_ID_1" globalconfig.Save(project) - newKeyID := globalconfig.GetUserCredentials(projectName).AWSResourceConfig.AccessKeyId + newKeyID := globalconfig.GetProjectCredentials(projectName).AWSResourceConfig.AccessKeyId assert.Equal(t, "TEST_KEY_ID_1", newKeyID) }) t.Run("Should edit old project if already exist", func(t *testing.T) { projectName := "my-project" - project := globalconfig.GetUserCredentials(projectName) + project := globalconfig.GetProjectCredentials(projectName) project.AWSResourceConfig.AccessKeyId = "EDITED_ACCESS_KEY_ID" globalconfig.Save(project) - newKeyID := globalconfig.GetUserCredentials(projectName).AWSResourceConfig.AccessKeyId + newKeyID := globalconfig.GetProjectCredentials(projectName).AWSResourceConfig.AccessKeyId assert.Equal(t, "EDITED_ACCESS_KEY_ID", newKeyID) }) } diff --git a/internal/config/moduleconfig/module_config.go b/internal/config/moduleconfig/module_config.go index d1ff078c4..95084002a 100644 --- a/internal/config/moduleconfig/module_config.go +++ b/internal/config/moduleconfig/module_config.go @@ -12,7 +12,7 @@ type ModuleConfig struct { Description string Author string TemplateConfig `yaml:"template"` - RequiredCredentials []string + RequiredCredentials []string `yaml:"requiredCredentials"` Parameters []Parameter } diff --git a/internal/context/init.go b/internal/context/init.go index 3f0cdba58..aabfc6d45 100644 --- a/internal/context/init.go +++ b/internal/context/init.go @@ -54,13 +54,10 @@ func Init(outDir string) *projectconfig.ZeroProjectConfig { // Prompting for push-up stream, then conditionally prompting for github initParams["GithubRootOrg"] = prompts["GithubRootOrg"].GetParam(initParams) - initParams["GithubPersonalToken"] = prompts["GithubPersonalToken"].GetParam(initParams) - if initParams["GithubRootOrg"] != "" && initParams["GithubPersonalToken"] != globalconfig.GetUserCredentials(projectConfig.Name).AccessToken { - projectCredential := globalconfig.GetUserCredentials(projectConfig.Name) - projectCredential.GithubResourceConfig.AccessToken = initParams["GithubPersonalToken"] - globalconfig.Save(projectCredential) - } - + projectCredentials := globalconfig.GetProjectCredentials(projectConfig.Name) + credentialPrompts := getCredentialPrompts(projectCredentials, moduleConfigs) + projectCredentials = promptCredentialsAndFillProjectCreds(credentialPrompts, projectCredentials) + globalconfig.Save(projectCredentials) projectParameters := promptAllModules(moduleConfigs) // Map parameter values back to specific modules @@ -84,6 +81,7 @@ func Init(outDir string) *projectconfig.ZeroProjectConfig { // TODO : Write the project config file. For now, print. pp.Println(projectConfig) + pp.Print(projectCredentials) // TODO: load ~/.zero/config.yml (or credentials) // TODO: prompt global credentials @@ -159,15 +157,6 @@ func getProjectPrompts(projectName string, modules map[string]moduleconfig.Modul KeyMatchCondition("ShouldPushRepositories", "y"), NoValidation, }, - "GithubPersonalToken": { - moduleconfig.Parameter{ - Field: "GithubPersonalToken", - Label: "Github Personal Access Token with access to the above organization", - Default: globalconfig.GetUserCredentials(projectName).AccessToken, - }, - KeyMatchCondition("ShouldPushRepositories", "y"), - NoValidation, - }, } for moduleName, module := range modules { @@ -187,6 +176,71 @@ func getProjectPrompts(projectName string, modules map[string]moduleconfig.Modul return handlers } +func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, moduleConfigs map[string]moduleconfig.ModuleConfig) map[string][]PromptHandler { + 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) + } + return prompts +} + +func mapVendorToPrompts(projectCred globalconfig.ProjectCredential, vendor string) []PromptHandler { + var prompts []PromptHandler + + switch vendor { + case "aws": + awsPrompts := []PromptHandler{ + { + moduleconfig.Parameter{ + Field: "accessKeyId", + Label: "AWS Access Key ID", + Default: projectCred.AWSResourceConfig.AccessKeyId, + }, + NoCondition, + NoValidation, + }, + { + moduleconfig.Parameter{ + Field: "secretAccessKey", + Label: "AWS Secret access key", + Default: projectCred.AWSResourceConfig.SecretAccessKey, + }, + NoCondition, + NoValidation, + }, + } + prompts = append(prompts, awsPrompts...) + case "github": + githubPrompt := PromptHandler{ + moduleconfig.Parameter{ + Field: "accessToken", + Label: "Github Personal Access Token with access to the above organization", + Default: projectCred.GithubResourceConfig.AccessToken, + }, + NoCondition, + NoValidation, + } + prompts = append(prompts, githubPrompt) + case "circleci": + circleCiPrompt := PromptHandler{ + moduleconfig.Parameter{ + Field: "apiKey", + Label: "Circleci api key for CI/CD", + Default: projectCred.CircleCiResourceConfig.ApiKey, + }, + NoCondition, + NoValidation, + } + prompts = append(prompts, circleCiPrompt) + } + return prompts +} + func chooseCloudProvider(projectConfig *projectconfig.ZeroProjectConfig) { // @TODO move options into configs providerPrompt := promptui.Select{ diff --git a/internal/context/prompts.go b/internal/context/prompts.go index 467f0c309..02c890ae9 100644 --- a/internal/context/prompts.go +++ b/internal/context/prompts.go @@ -8,9 +8,11 @@ import ( "regexp" "strings" + "github.com/commitdev/zero/internal/config/globalconfig" "github.com/commitdev/zero/internal/config/moduleconfig" "github.com/commitdev/zero/pkg/util/exit" "github.com/manifoldco/promptui" + "gopkg.in/yaml.v2" ) type PromptHandler struct { @@ -147,3 +149,41 @@ func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[s } return parameters, nil } + +func promptCredentialsAndFillProjectCreds(credentialPrompts map[string][]PromptHandler, credentials globalconfig.ProjectCredential) globalconfig.ProjectCredential { + promptsValues := map[string]map[string]string{} + + for vendor, prompts := range credentialPrompts { + 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{}) + } + 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 +} + +func appendToSet(set []string, toAppend []string) []string { + for _, appendee := range toAppend { + if !itemInSlice(set, appendee) { + set = append(set, appendee) + } + } + return set +} + +func itemInSlice(slice []string, target string) bool { + for _, item := range slice { + if item == target { + return true + } + } + return false +} diff --git a/internal/module/module_test.go b/internal/module/module_test.go index 888c0586f..1bf553f58 100644 --- a/internal/module/module_test.go +++ b/internal/module/module_test.go @@ -46,6 +46,10 @@ func TestParseModuleConfig(t *testing.T) { assert.Equal(t, "CI Platform", param.Label) }) + t.Run("requiredCredentials are loaded", func(t *testing.T) { + assert.Equal(t, []string{"aws", "circleci", "github"}, mod.RequiredCredentials) + }) + t.Run("TemplateConfig is unmarshaled", func(t *testing.T) { mod, _ = module.ParseModuleConfig(testModuleSource) assert.Equal(t, ".circleci", mod.TemplateConfig.OutputDir) diff --git a/tests/test_data/modules/ci/zero-module.yml b/tests/test_data/modules/ci/zero-module.yml index d34d33fc0..ec6a69e4d 100644 --- a/tests/test_data/modules/ci/zero-module.yml +++ b/tests/test_data/modules/ci/zero-module.yml @@ -4,6 +4,11 @@ author: "" icon: "" thumbnail: "" +requiredCredentials: + - aws + - circleci + - github + # Template variables to populate, these could be overwritten by the file spefic frontmatter variables template: # strictMode: true # will only parse files that includes the .tmpl.* extension, otherwise it will copy file