From 767af71e7e9c594b8df71b2b28031ec6b020c2b9 Mon Sep 17 00:00:00 2001 From: David Cheung Date: Wed, 24 Jun 2020 15:15:25 -0400 Subject: [PATCH] vendor credentials is env-vars for execute --- internal/apply/apply.go | 11 ++++- internal/config/globalconfig/global_config.go | 42 +++++++++++++---- .../config/globalconfig/global_config_test.go | 47 +++++++++++++++++++ internal/init/init.go | 10 ++-- internal/init/prompts.go | 23 +++++---- internal/util/util.go | 9 ++++ .../test_data/apply/project1/zero-module.yml | 19 ++++++++ .../test_data/apply/project2/zero-module.yml | 19 ++++++++ 8 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 tests/test_data/apply/project1/zero-module.yml create mode 100644 tests/test_data/apply/project2/zero-module.yml diff --git a/internal/apply/apply.go b/internal/apply/apply.go index a5f61f788..7b1fc5a69 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -77,11 +77,18 @@ func applyAll(dir string, projectConfig projectconfig.ZeroProjectConfig, applyEn modulePath = filepath.Join(dir, modulePath) } + // TODO: in the case user lost the `/tmp` (module source dir), this will fail + // and we should redownload the module for the user + modConfig, err := module.ParseModuleConfig(modulePath) + if err != nil { + exit.Fatal("Failed to load module config, credentials cannot be injected properly") + } + // Get project credentials for the makefile credentials := globalconfig.GetProjectCredentials(projectConfig.Name) - + credentialEnvs := credentials.SelectedVendorsCredentialsAsEnv(modConfig.RequiredCredentials) envList = util.AppendProjectEnvToCmdEnv(mod.Parameters, envList) - envList = util.AppendProjectEnvToCmdEnv(credentials.AsEnvVars(), envList) + envList = util.AppendProjectEnvToCmdEnv(credentialEnvs, envList) util.ExecuteCommand(exec.Command("make"), modulePath, envList) return nil }) diff --git a/internal/config/globalconfig/global_config.go b/internal/config/globalconfig/global_config.go index 9feb5ddf5..9f1ed2809 100644 --- a/internal/config/globalconfig/global_config.go +++ b/internal/config/globalconfig/global_config.go @@ -8,8 +8,10 @@ import ( "os/user" "path" "reflect" + "strings" "github.com/commitdev/zero/internal/constants" + "github.com/commitdev/zero/internal/util" "github.com/commitdev/zero/pkg/util/exit" yaml "gopkg.in/yaml.v2" ) @@ -20,20 +22,20 @@ type ProjectCredentials map[string]ProjectCredential type ProjectCredential struct { ProjectName string `yaml:"-"` - AWSResourceConfig `yaml:"aws,omitempty"` - GithubResourceConfig `yaml:"github,omitempty"` - CircleCiResourceConfig `yaml:"circleci,omitempty"` + AWSResourceConfig `yaml:"aws,omitempty" vendor:"aws"` + GithubResourceConfig `yaml:"github,omitempty" vendor:"github"` + CircleCiResourceConfig `yaml:"circleci,omitempty" vendor:"circleci"` } type AWSResourceConfig struct { - AccessKeyID string `yaml:"accessKeyId,omitempty" env:"AWS_ACCESS_KEY_ID"` - SecretAccessKey string `yaml:"secretAccessKey,omitempty" env:"AWS_SECRET_ACCESS_KEY"` + AccessKeyID string `yaml:"accessKeyId,omitempty" env:"AWS_ACCESS_KEY_ID,omitempty"` + SecretAccessKey string `yaml:"secretAccessKey,omitempty" env:"AWS_SECRET_ACCESS_KEY,omitempty"` } type GithubResourceConfig struct { - AccessToken string `yaml:"accessToken,omitempty" env:"GITHUB_ACCESS_TOKEN"` + AccessToken string `yaml:"accessToken,omitempty" env:"GITHUB_ACCESS_TOKEN,omitempty"` } type CircleCiResourceConfig struct { - ApiKey string `yaml:"apiKey,omitempty" env:"CIRCLECI_API_KEY"` + ApiKey string `yaml:"apiKey,omitempty" env:"CIRCLECI_API_KEY,omitempty"` } func (p ProjectCredentials) Unmarshal(data []byte) error { @@ -74,12 +76,36 @@ func gatherFieldTags(t reflect.Value, list map[string]string) map[string]string } if env := fieldType.Tag.Get("env"); env != "" { - list[env] = fieldValue.String() + name, opts := parseTag(env) + if idx := strings.Index(opts, "omitempty"); idx != -1 && fieldValue.String() == "" { + continue + } + list[name] = fieldValue.String() } } return list } +func (p ProjectCredential) SelectedVendorsCredentialsAsEnv(vendors []string) map[string]string { + t := reflect.ValueOf(p) + envs := map[string]string{} + for i := 0; i < t.NumField(); i++ { + childStruct := t.Type().Field(i) + childValue := t.Field(i) + if tag := childStruct.Tag.Get("vendor"); tag != "" && util.ItemInSlice(vendors, tag) { + envs = gatherFieldTags(childValue, envs) + } + } + return envs +} + +func parseTag(tag string) (string, string) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tag[idx+1:] + } + return tag, "" +} + func LoadUserCredentials() ProjectCredentials { data := readOrCreateUserCredentialsFile() diff --git a/internal/config/globalconfig/global_config_test.go b/internal/config/globalconfig/global_config_test.go index 9f9278220..eddfc8291 100644 --- a/internal/config/globalconfig/global_config_test.go +++ b/internal/config/globalconfig/global_config_test.go @@ -119,4 +119,51 @@ func TestMarshalProjectCredentialAsEnvVars(t *testing.T) { assert.Equal(t, "SAK", envVars["AWS_SECRET_ACCESS_KEY"]) assert.Equal(t, "APIKEY", envVars["CIRCLECI_API_KEY"]) }) + + t.Run("should honor omitempty and left out empty values", func(t *testing.T) { + pc := globalconfig.ProjectCredential{} + + envVars := pc.AsEnvVars() + assert.Equal(t, 0, len(envVars)) + }) +} + +func TestMarshalSelectedVendorsCredentialsAsEnv(t *testing.T) { + pc := globalconfig.ProjectCredential{ + AWSResourceConfig: globalconfig.AWSResourceConfig{ + AccessKeyID: "AKID", + SecretAccessKey: "SAK", + }, + GithubResourceConfig: globalconfig.GithubResourceConfig{ + AccessToken: "FOOBAR", + }, + CircleCiResourceConfig: globalconfig.CircleCiResourceConfig{ + ApiKey: "APIKEY", + }, + } + + t.Run("cherry pick credentials by vendor", func(t *testing.T) { + envs := pc.SelectedVendorsCredentialsAsEnv([]string{"aws", "github"}) + assert.Equal(t, "AKID", envs["AWS_ACCESS_KEY_ID"]) + assert.Equal(t, "SAK", envs["AWS_SECRET_ACCESS_KEY"]) + assert.Equal(t, "FOOBAR", envs["GITHUB_ACCESS_TOKEN"]) + }) + + t.Run("omits vendors not selected", func(t *testing.T) { + envs := pc.SelectedVendorsCredentialsAsEnv([]string{"github"}) + assert.Equal(t, "FOOBAR", envs["GITHUB_ACCESS_TOKEN"]) + + _, hasAWSKeyID := envs["AWS_ACCESS_KEY_ID"] + assert.Equal(t, false, hasAWSKeyID) + _, hasAWSSecretAccessKey := envs["AWS_SECRET_ACCESS_KEY"] + assert.Equal(t, false, hasAWSSecretAccessKey) + _, hasCircleCIKey := envs["CIRCLECI_API_KEY"] + assert.Equal(t, false, hasCircleCIKey) + }) + + t.Run("omits vendors not selected", func(t *testing.T) { + envs := pc.SelectedVendorsCredentialsAsEnv([]string{}) + assert.Equal(t, 0, len(envs)) + }) + } diff --git a/internal/init/init.go b/internal/init/init.go index a80edfaa6..0d4fa5f31 100644 --- a/internal/init/init.go +++ b/internal/init/init.go @@ -11,6 +11,7 @@ import ( "github.com/commitdev/zero/internal/config/projectconfig" "github.com/commitdev/zero/internal/module" "github.com/commitdev/zero/internal/registry" + "github.com/commitdev/zero/internal/util" project "github.com/commitdev/zero/pkg/credentials" "github.com/commitdev/zero/pkg/util/exit" "github.com/commitdev/zero/pkg/util/flog" @@ -51,7 +52,7 @@ func Init(outDir string) *projectconfig.ZeroProjectConfig { credentialPrompts := getCredentialPrompts(projectCredentials, moduleConfigs) projectCredentials = promptCredentialsAndFillProjectCreds(credentialPrompts, projectCredentials) globalconfig.Save(projectCredentials) - projectParameters := promptAllModules(moduleConfigs) + projectParameters := promptAllModules(moduleConfigs, projectCredentials) // Map parameter values back to specific modules for moduleName, module := range moduleConfigs { @@ -101,11 +102,12 @@ func loadAllModules(moduleSources []string) (map[string]moduleconfig.ModuleConfi } // promptAllModules takes a map of all the modules and prompts the user for values for all the parameters -func promptAllModules(modules map[string]moduleconfig.ModuleConfig) map[string]string { +func promptAllModules(modules map[string]moduleconfig.ModuleConfig, projectCredentials globalconfig.ProjectCredential) map[string]string { parameterValues := make(map[string]string) for _, config := range modules { var err error - parameterValues, err = PromptModuleParams(config, parameterValues) + + parameterValues, err = PromptModuleParams(config, parameterValues, projectCredentials) if err != nil { exit.Fatal("Exiting prompt: %v\n", err) } @@ -175,7 +177,7 @@ func getCredentialPrompts(projectCredentials globalconfig.ProjectCredential, mod // map is to keep track of which vendor they belong to, to fill them back into the projectConfig prompts := []CredentialPrompts{} for _, vendor := range AvailableVendorOrders { - if itemInSlice(uniqueVendors, vendor) { + if util.ItemInSlice(uniqueVendors, vendor) { vendorPrompts := CredentialPrompts{vendor, mapVendorToPrompts(projectCredentials, vendor)} prompts = append(prompts, vendorPrompts) } diff --git a/internal/init/prompts.go b/internal/init/prompts.go index d976a4c4c..534421342 100644 --- a/internal/init/prompts.go +++ b/internal/init/prompts.go @@ -163,19 +163,27 @@ func sanitizeParameterValue(str string) string { } // PromptParams renders series of prompt UI based on the config -func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[string]string) (map[string]string, error) { +func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[string]string, projectCredentials globalconfig.ProjectCredential) (map[string]string, error) { + credentialEnvs := projectCredentials.SelectedVendorsCredentialsAsEnv(moduleConfig.RequiredCredentials) for _, promptConfig := range moduleConfig.Parameters { // deduplicate fields already prompted and received if _, isAlreadySet := parameters[promptConfig.Field]; isAlreadySet { continue } + promptHandler := PromptHandler{ promptConfig, NoCondition, NoValidation, } - result := promptHandler.GetParam(parameters) + // merging the context of param and credentals + // this treats credentialEnvs as throwaway, parameters is shared between modules + // so credentials should not be in parameters as it gets returned to parent + for k, v := range parameters { + credentialEnvs[k] = v + } + result := promptHandler.GetParam(credentialEnvs) parameters[promptConfig.Field] = result } @@ -212,18 +220,9 @@ func promptCredentialsAndFillProjectCreds(credentialPrompts []CredentialPrompts, func appendToSet(set []string, toAppend []string) []string { for _, appendee := range toAppend { - if !itemInSlice(set, appendee) { + if !util.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/util/util.go b/internal/util/util.go index 7e0116c28..3c22e996a 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -133,3 +133,12 @@ func IndentString(content string, spaces int) string { } return result } + +func ItemInSlice(slice []string, target string) bool { + for _, item := range slice { + if item == target { + return true + } + } + return false +} diff --git a/tests/test_data/apply/project1/zero-module.yml b/tests/test_data/apply/project1/zero-module.yml new file mode 100644 index 000000000..5d6914f9b --- /dev/null +++ b/tests/test_data/apply/project1/zero-module.yml @@ -0,0 +1,19 @@ +name: project1 +description: 'project1' +author: 'Commit' + +template: + strictMode: true + delimiters: + - "<%" + - "%>" + inputDir: '.' + outputDir: 'test' + +requiredCredentials: + - aws + - github + +parameters: + - field: foo + label: foo diff --git a/tests/test_data/apply/project2/zero-module.yml b/tests/test_data/apply/project2/zero-module.yml new file mode 100644 index 000000000..c50d5df76 --- /dev/null +++ b/tests/test_data/apply/project2/zero-module.yml @@ -0,0 +1,19 @@ +name: project2 +description: 'project2' +author: 'Commit' + +template: + strictMode: true + delimiters: + - "<%" + - "%>" + inputDir: '.' + outputDir: 'test' + +requiredCredentials: + - aws + - github + +parameters: + - field: baz + label: baz