Skip to content

Commit

Permalink
support module-config default / value
Browse files Browse the repository at this point in the history
- also adding support for project conditional prompts
  • Loading branch information
davidcheung committed Jun 9, 2020
1 parent 9d6372d commit 544ab60
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 164 deletions.
2 changes: 1 addition & 1 deletion internal/config/projectconfig/project_config.go
Expand Up @@ -11,7 +11,7 @@ import (
type ZeroProjectConfig struct {
Name string
Infrastructure Infrastructure // TODO simplify and flatten / rename?
Context map[string]string
Parameters map[string]string
Modules []string
}

Expand Down
126 changes: 53 additions & 73 deletions internal/context/init.go
Expand Up @@ -24,7 +24,8 @@ type Registry map[string][]string
// Create cloud provider context
func Init(outDir string) *projectconfig.ZeroProjectConfig {
projectConfig := defaultProjConfig()
projectConfig.Name = promptProjectName()

projectConfig.Name = getProjectNamePrompt().GetParam(projectConfig.Parameters)

rootDir := path.Join(outDir, projectConfig.Name)
flog.Infof(":tada: Creating project")
Expand All @@ -36,15 +37,17 @@ func Init(outDir string) *projectconfig.ZeroProjectConfig {
exit.Fatal("Error creating root: %v ", err)
}

projectConfig.Context["ShouldPushRepoUpstream"] = promptPushRepoUpstream()
projectConfig.Context["GithubRootOrg"] = promptGithubRootOrg()
projectConfig.Context["githubPersonalToken"] = promptGithubPersonalToken(projectConfig.Name)

// chooseCloudProvider(&projectConfig)
// fmt.Println(&projectConfig)
// s := project.GetSecrets(rootDir)
// fillProviderDetails(&projectConfig, s)
// fmt.Println(&projectConfig)
prompts := getProjectPrompts(projectConfig.Name)
projectConfig.Parameters["ShouldPushRepoUpstream"] = prompts["ShouldPushRepoUpstream"].GetParam(projectConfig.Parameters)
// Prompting for push-up stream, then conditionally prompting for github
projectConfig.Parameters["GithubRootOrg"] = prompts["GithubRootOrg"].GetParam(projectConfig.Parameters)
personalToken := prompts["githubPersonalToken"].GetParam(projectConfig.Parameters)
if personalToken != "" && personalToken != globalconfig.GetUserCredentials(projectConfig.Name).AccessToken {
projectConfig.Parameters["githubPersonalToken"] = personalToken
projectCredential := globalconfig.GetUserCredentials(projectConfig.Name)
projectCredential.GithubResourceConfig.AccessToken = personalToken
globalconfig.Save(projectCredential)
}
moduleSources := chooseStack(getRegistry())
moduleConfigs := loadAllModules(moduleSources)
for _ = range moduleConfigs {
Expand All @@ -53,7 +56,7 @@ func Init(outDir string) *projectconfig.ZeroProjectConfig {

projectParameters := promptAllModules(moduleConfigs)
for k, v := range projectParameters {
projectConfig.Context[k] = v
projectConfig.Parameters[k] = v
// TODO: Add parameters to module structs inside project
}

Expand Down Expand Up @@ -82,77 +85,54 @@ func promptAllModules(modules map[string]moduleconfig.ModuleConfig) map[string]s
parameterValues := make(map[string]string)
for _, config := range modules {
var err error
parameterValues, err = module.PromptParams(config, parameterValues)
parameterValues, err = PromptModuleParams(config, parameterValues)
if err != nil {
exit.Fatal("Exiting prompt: %v\n", err)
}
}
return parameterValues
}

// global configs
func promptPushRepoUpstream() string {
providerPrompt := promptui.Prompt{
Label: "Should the created projects be checked into github automatically? (y/n)",
Default: "y",
AllowEdit: false,
}
providerResult, err := providerPrompt.Run()
if err != nil {
exit.Fatal("Exiting prompt: %v\n", err)
}
return providerResult
}

func promptGithubRootOrg() string {
providerPrompt := promptui.Prompt{
Label: "What's the root of the github org to create repositories in?",
Default: "github.com/",
AllowEdit: true,
}
result, err := providerPrompt.Run()
if err != nil {
exit.Fatal("Exiting prompt: %v\n", err)
}
return result
}

func promptGithubPersonalToken(projectName string) string {
defaultToken := ""

project := globalconfig.GetUserCredentials(projectName)
if project.GithubResourceConfig.AccessToken != "" {
defaultToken = project.GithubResourceConfig.AccessToken
}

providerPrompt := promptui.Prompt{
Label: "Github Personal Access Token with access to the above organization",
Default: defaultToken,
}
result, err := providerPrompt.Run()
if err != nil {
exit.Fatal("Prompt failed %v\n", err)
}

// If its different from saved token, update it
if project.GithubResourceConfig.AccessToken != result {
project.GithubResourceConfig.AccessToken = result
globalconfig.Save(project)
// Project name is prompt individually because the rest of the prompts
// requires the projectName to populate defaults
func getProjectNamePrompt() PromptHandler {
return PromptHandler{
moduleconfig.Parameter{
Field: "projectName",
Label: "Project Name",
Default: "",
},
NoCondition,
}
return result
}

func promptProjectName() string {
providerPrompt := promptui.Prompt{
Label: "Project Name",
Default: "",
AllowEdit: false,
}
result, err := providerPrompt.Run()
if err != nil {
exit.Fatal("Prompt failed %v\n", err)
func getProjectPrompts(projectName string) map[string]PromptHandler {
return map[string]PromptHandler{
"ShouldPushRepoUpstream": {
moduleconfig.Parameter{
Field: "ShouldPushRepoUpstream",
Label: "Should the created projects be checked into github automatically? (y/n)",
Default: "y",
},
NoCondition,
},
"GithubRootOrg": {
moduleconfig.Parameter{
Field: "GithubRootOrg",
Label: "What's the root of the github org to create repositories in?",
Default: "github.com/",
},
KeyMatchCondition("ShouldPushRepoUpstream", "y"),
},
"githubPersonalToken": {
moduleconfig.Parameter{
Field: "githubPersonalToken",
Label: "Github Personal Access Token with access to the above organization",
Default: globalconfig.GetUserCredentials(projectName).AccessToken,
},
KeyMatchCondition("ShouldPushRepoUpstream", "y"),
},
}
return result
}

func chooseCloudProvider(projectConfig *projectconfig.ZeroProjectConfig) {
Expand Down Expand Up @@ -241,7 +221,7 @@ func defaultProjConfig() projectconfig.ZeroProjectConfig {
Infrastructure: projectconfig.Infrastructure{
AWS: nil,
},
Context: map[string]string{},
Modules: []string{},
Parameters: map[string]string{},
Modules: []string{},
}
}
128 changes: 128 additions & 0 deletions internal/context/prompts.go
@@ -0,0 +1,128 @@
package context

import (
"fmt"
"log"
"os"
"os/exec"
"regexp"

"github.com/commitdev/zero/internal/config/moduleconfig"
"github.com/commitdev/zero/pkg/util/exit"
"github.com/manifoldco/promptui"
)

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

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

// TODO: validation / allow prompt retry ...etc
func (p PromptHandler) GetParam(projectParams map[string]string) string {
var err error
var result string
if p.Condition(projectParams) {
// TODO: figure out scope of projectParams per project
// potentially dangerous to have cross module env leaking
// so if community module has an `execute: twitter tweet $ENV`
// it wouldnt leak things the module shouldnt have access to
if p.Parameter.Execute != "" {
result = executeCmd(p.Parameter.Execute, projectParams)
} else if p.Parameter.Value != "" {
result = p.Parameter.Value
} else {
err, result = promptParameter(p.Parameter)
}
if err != nil {
exit.Fatal("Exiting prompt: %v\n", err)
}

return sanitizeParameterValue(result)
}
return ""
}

func promptParameter(param moduleconfig.Parameter) (error, string) {
label := param.Label
if param.Label == "" {
label = param.Field
}
defaultValue := param.Default

var err error
var result string
if len(param.Options) > 0 {
prompt := promptui.Select{
Label: label,
Items: param.Options,
}
_, result, err = prompt.Run()

} else {
prompt := promptui.Prompt{
Label: label,
Default: defaultValue,
AllowEdit: true,
}
result, err = prompt.Run()
}
if err != nil {
exit.Fatal("Exiting prompt: %v\n", err)
}

return nil, result
}

func executeCmd(command string, envVars map[string]string) string {
cmd := exec.Command("bash", "-c", command)
cmd.Env = appendProjectEnvToCmdEnv(envVars, os.Environ())
out, err := cmd.Output()

if err != nil {
log.Fatalf("Failed to execute %v\n", err)
}
return string(out)
}

// aws cli prints output with linebreak in them
func sanitizeParameterValue(str string) string {
re := regexp.MustCompile("\\n")
return re.ReplaceAllString(str, "")
}

func appendProjectEnvToCmdEnv(envMap map[string]string, envList []string) []string {
for key, val := range envMap {
if val != "" {
envList = append(envList, fmt.Sprintf("%s=%s", key, val))
}
}
return envList
}

// PromptParams renders series of prompt UI based on the config
func PromptModuleParams(moduleConfig moduleconfig.ModuleConfig, parameters map[string]string) (map[string]string, error) {

for _, promptConfig := range moduleConfig.Parameters {
// deduplicate fields already prompted and received
if _, isAlreadySet := parameters[promptConfig.Field]; isAlreadySet {
continue
}
promptHandler := PromptHandler{
promptConfig,
NoCondition,
}
result := promptHandler.GetParam(parameters)

parameters[promptConfig.Field] = result
}
return parameters, nil
}
61 changes: 61 additions & 0 deletions internal/context/prompts_test.go
@@ -0,0 +1,61 @@
package context_test

import (
"testing"

"github.com/commitdev/zero/internal/config/moduleconfig"
"github.com/commitdev/zero/internal/context"

"github.com/stretchr/testify/assert"
)

func TestGetParam(t *testing.T) {
projectParams := map[string]string{}
t.Run("Should execute params without prompt", func(t *testing.T) {
param := moduleconfig.Parameter{
Field: "account-id",
Execute: "echo \"my-acconut-id\"",
}

prompt := context.PromptHandler{
param,
context.NoCondition,
}

result := prompt.GetParam(projectParams)
assert.Equal(t, "my-acconut-id", result)
})

t.Run("executes with project context", func(t *testing.T) {
param := moduleconfig.Parameter{
Field: "myEnv",
Execute: "echo $INJECTEDENV",
}

prompt := context.PromptHandler{
param,
context.NoCondition,
}

result := prompt.GetParam(map[string]string{
"INJECTEDENV": "SOME_ENV_VAR_VALUE",
})
assert.Equal(t, "SOME_ENV_VAR_VALUE", result)
})

t.Run("Should return static value", func(t *testing.T) {
param := moduleconfig.Parameter{
Field: "placeholder",
Value: "lorem-ipsum",
}

prompt := context.PromptHandler{
param,
context.NoCondition,
}

result := prompt.GetParam(projectParams)
assert.Equal(t, "lorem-ipsum", result)
})

}
4 changes: 2 additions & 2 deletions internal/generate/generate_infrastructure.go
Expand Up @@ -74,7 +74,7 @@ func Execute(cfg *projectconfig.ZeroProjectConfig, pathPrefix string) {
"cognito_client_id",
}
outputValues := GetOutputs(cfg, pathPrefix, outputs)
cfg.Context["cognito_pool_id"] = outputValues["cognito_pool_id"]
cfg.Context["cognito_client_id"] = outputValues["cognito_client_id"]
cfg.Parameters["cognito_pool_id"] = outputValues["cognito_pool_id"]
cfg.Parameters["cognito_client_id"] = outputValues["cognito_client_id"]
}
}
9 changes: 2 additions & 7 deletions internal/generate/generate_modules.go
Expand Up @@ -37,14 +37,9 @@ func GenerateModules(cfg *config.GeneratorConfig) {

// Prompt for module params and execute each of the generator modules
for _, mod := range templateModules {
// FIXME:(david) generate flow probably wont need prompts anymore
// added an empty map to fix test temporarily
err := mod.PromptParams(map[string]string{})
if err != nil {
flog.Warnf("module %s: params prompt failed", mod.Source)
}
// TODO: read zero-project.yml instead

err = Generate(mod, cfg)
err := Generate(mod, cfg)
if err != nil {
exit.Error("module %s: %s", mod.Source, err)
}
Expand Down

0 comments on commit 544ab60

Please sign in to comment.