Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/config/projectconfig/project_config.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do this as part of this PR but shouldn't these be part of the projectConfig struct rather than parameters? Parameters are passed into project templates, but these values we will be using for zero itself during the create phase.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do this as part of my ticket.

// 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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice implementation

},
"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
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this would be a good one to have a test for, exercising the different types.

Copy link
Contributor Author

@davidcheung davidcheung Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did, i aliased my git a to git add . -u and forgot to index the untracked file 😅 fixed now

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading