// Copyright 2016-2018, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmd import ( "fmt" "io/ioutil" "os" "path/filepath" "runtime" "sort" "strings" "unicode" "github.com/pkg/errors" "github.com/spf13/cobra" survey "gopkg.in/AlecAivazis/survey.v1" surveycore "gopkg.in/AlecAivazis/survey.v1/core" "github.com/pulumi/pulumi/pkg/apitype" "github.com/pulumi/pulumi/pkg/backend" "github.com/pulumi/pulumi/pkg/backend/display" "github.com/pulumi/pulumi/pkg/backend/httpstate" "github.com/pulumi/pulumi/pkg/backend/state" "github.com/pulumi/pulumi/pkg/diag/colors" "github.com/pulumi/pulumi/pkg/engine" "github.com/pulumi/pulumi/pkg/npm" "github.com/pulumi/pulumi/pkg/resource/config" "github.com/pulumi/pulumi/pkg/tokens" "github.com/pulumi/pulumi/pkg/util/cmdutil" "github.com/pulumi/pulumi/pkg/util/contract" "github.com/pulumi/pulumi/pkg/util/logging" "github.com/pulumi/pulumi/pkg/workspace" ) type promptForValueFunc func(yes bool, valueType string, defaultValue string, secret bool, isValidFn func(value string) error, opts display.Options) (string, error) type newArgs struct { configArray []string configPath bool description string dir string force bool generateOnly bool interactive bool name string offline bool prompt promptForValueFunc secretsProvider string stack string templateNameOrURL string yes bool } func runNew(args newArgs) error { if !args.interactive { args.yes = true // auto-approve changes, since we cannot prompt. } // Prepare options. opts := display.Options{ Color: cmdutil.GetGlobalColorization(), IsInteractive: args.interactive, } // Validate name (if specified) before further prompts/operations. if args.name != "" && workspace.ValidateProjectName(args.name) != nil { return errors.Errorf("'%s' is not a valid project name. %s.", args.name, workspace.ValidateProjectName(args.name)) } // Validate secrets provider type if err := validateSecretsProvider(args.secretsProvider); err != nil { return err } // Get the current working directory. cwd, err := os.Getwd() if err != nil { return errors.Wrap(err, "getting the working directory") } originalCwd := cwd // If dir was specified, ensure it exists and use it as the // current working directory. if args.dir != "" { cwd, err = useSpecifiedDir(args.dir) if err != nil { return err } } // Return an error if the directory isn't empty. if !args.force { if err = errorIfNotEmptyDirectory(cwd); err != nil { return err } } // If we're going to be creating a stack, get the current backend, which // will kick off the login flow (if not already logged-in). if !args.generateOnly { if _, err = currentBackend(opts); err != nil { return err } } // Ensure the project doesn't already exist. if args.name != "" { if err := validateProjectName(args.name, args.generateOnly, opts); err != nil { return err } } // Retrieve the template repo. repo, err := workspace.RetrieveTemplates(args.templateNameOrURL, args.offline, workspace.TemplateKindPulumiProject) if err != nil { return err } defer func() { contract.IgnoreError(repo.Delete()) }() // List the templates from the repo. templates, err := repo.Templates() if err != nil { return err } var template workspace.Template if len(templates) == 0 { return errors.New("no templates") } else if len(templates) == 1 { template = templates[0] } else { if template, err = chooseTemplate(templates, opts); err != nil { return err } } // Do a dry run, if we're not forcing files to be overwritten. if !args.force { if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd, args.name); err != nil { if os.IsNotExist(err) { return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL) } return err } } // If a stack was specified via --stack, see if it already exists. // Only do the lookup for fully-qualified stack names `org/project/stack` because // otherwise `getStack` will fail to detect the project folder and fail. // The main purpose of this lookup is getting a proper start with a project // created via the web app. var s backend.Stack if args.stack != "" && strings.Count(args.stack, "/") == 2 { existingStack, existingName, existingDesc, err := getStack(args.stack, opts) if err != nil { return err } s = existingStack if args.name == "" { args.name = existingName } if args.description == "" { args.description = existingDesc } } // Show instructions, if we're going to show at least one prompt. hasAtLeastOnePrompt := (args.name == "") || (args.description == "") || (!args.generateOnly && args.stack == "") if !args.yes && hasAtLeastOnePrompt { fmt.Println("This command will walk you through creating a new Pulumi project.") fmt.Println() fmt.Println( opts.Color.Colorize( colors.Highlight("Enter a value or leave blank to accept the (default), and press .", "", colors.BrightCyan+colors.Bold))) fmt.Println( opts.Color.Colorize( colors.Highlight("Press ^C at any time to quit.", "^C", colors.BrightCyan+colors.Bold))) fmt.Println() } // Prompt for the project name, if it wasn't already specified. if args.name == "" { defaultValue := workspace.ValueOrSanitizedDefaultProjectName(args.name, template.ProjectName, filepath.Base(cwd)) if err := validateProjectName(defaultValue, args.generateOnly, opts); err != nil { // Do not suggest an invalid or existing name as the default project name. defaultValue = "" } validate := func(s string) error { return validateProjectName(s, args.generateOnly, opts) } args.name, err = args.prompt(args.yes, "project name", defaultValue, false, validate, opts) if err != nil { return err } } // Prompt for the project description, if it wasn't already specified. if args.description == "" { defaultValue := workspace.ValueOrDefaultProjectDescription( args.description, template.ProjectDescription, template.Description) args.description, err = args.prompt( args.yes, "project description", defaultValue, false, workspace.ValidateProjectDescription, opts) if err != nil { return err } } // Actually copy the files. if err = workspace.CopyTemplateFiles(template.Dir, cwd, args.force, args.name, args.description); err != nil { if os.IsNotExist(err) { return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL) } return err } fmt.Printf("Created project '%s'\n", args.name) fmt.Println() // Load the project, update the name & description, remove the template section, and save it. proj, _, err := readProject() if err != nil { return err } proj.Name = tokens.PackageName(args.name) proj.Description = &args.description proj.Template = nil if err = workspace.SaveProject(proj); err != nil { return errors.Wrap(err, "saving project") } // Create the stack, if needed. if !args.generateOnly && s == nil { if s, err = promptAndCreateStack(args.prompt, args.stack, args.name, true /*setCurrent*/, args.yes, opts, args.secretsProvider); err != nil { return err } // The backend will print "Created stack ''" on success. fmt.Println() } // Prompt for config values (if needed) and save. if !args.generateOnly { err = handleConfig(s, args.templateNameOrURL, template, args.configArray, args.yes, args.configPath, opts) if err != nil { return err } } // Ensure the stack is selected. if !args.generateOnly && s != nil { contract.IgnoreError(state.SetCurrentStack(s.Ref().String())) } // Install dependencies. if !args.generateOnly { if err := installDependencies(); err != nil { return err } } fmt.Println( opts.Color.Colorize( colors.BrightGreen+colors.Bold+"Your new project is ready to go!"+colors.Reset) + " " + cmdutil.EmojiOr("✨", "")) fmt.Println() // Print out next steps. printNextSteps(proj, originalCwd, cwd, args.generateOnly, opts) if template.Quickstart != "" { fmt.Println(template.Quickstart) } return nil } // Ensure the directory exists and uses it as the current working // directory. func useSpecifiedDir(dir string) (string, error) { // Ensure the directory exists. if err := os.MkdirAll(dir, os.ModePerm); err != nil { return "", errors.Wrap(err, "creating the directory") } // Change the working directory to the specified directory. if err := os.Chdir(dir); err != nil { return "", errors.Wrap(err, "changing the working directory") } // Get the new working directory. var cwd string var err error if cwd, err = os.Getwd(); err != nil { return "", errors.Wrap(err, "getting the working directory") } return cwd, nil } // newNewCmd creates a New command with default dependencies. // Intentionally disabling here for cleaner err declaration/assignment. // nolint: vetshadow func newNewCmd() *cobra.Command { var args = newArgs{ interactive: cmdutil.Interactive(), prompt: promptForValue, } cmd := &cobra.Command{ Use: "new [template|url]", SuggestFor: []string{"init", "create"}, Short: "Create a new Pulumi project", Long: "Create a new Pulumi project and stack from a template.\n" + "\n" + "To create a project from a specific template, pass the template name (such as `aws-typescript`\n" + "or `azure-python`). If no template name is provided, a list of suggested templates will be presented\n" + "which can be selected interactively.\n" + "\n" + "By default, a stack created using the pulumi.com backend will use the pulumi.com secrets\n" + "provider and a stack created using the local or cloud object storage backend will use the\n" + "`passphrase` secrets provider. A different secrets provider can be selected by passing the\n" + "`--secrets-provider` flag.\n" + "\n" + "To use the `passphrase` secrets provider with the pulumi.com backend, use:\n" + "* `pulumi new --secrets-provider=passphrase`\n" + "\n" + "To use a cloud secrets provider with any backend, use one of the following:\n" + "* `pulumi new --secrets-provider=\"awskms://alias/ExampleAlias?region=us-east-1\"`\n" + "* `pulumi new --secrets-provider=\"awskms://1234abcd-12ab-34cd-56ef-1234567890ab?region=us-east-1\"`\n" + "* `pulumi new --secrets-provider=\"azurekeyvault://mykeyvaultname.vault.azure.net/keys/mykeyname\"`\n" + "* `pulumi new --secrets-provider=\"gcpkms://projects/p/locations/l/keyRings/r/cryptoKeys/k\"`\n" + "* `pulumi new --secrets-provider=\"hashivault://mykey\"`", Args: cmdutil.MaximumNArgs(1), Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error { if len(cliArgs) > 0 { args.templateNameOrURL = cliArgs[0] } return runNew(args) }), } // Add additional help that includes a list of available templates. defaultHelp := cmd.HelpFunc() cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { // Show default help. defaultHelp(cmd, args) // Attempt to retrieve available templates. repo, err := workspace.RetrieveTemplates("", false /*offline*/, workspace.TemplateKindPulumiProject) if err != nil { logging.Warningf("could not retrieve templates: %v", err) return } // Get the list of templates. templates, err := repo.Templates() if err != nil { logging.Warningf("could not list templates: %v", err) return } // If we have any templates, show them. if len(templates) > 0 { available, _ := templatesToOptionArrayAndMap(templates, true) fmt.Println("") fmt.Println("Available Templates:") for _, t := range available { fmt.Printf(" %s\n", t) } } }) cmd.PersistentFlags().StringArrayVarP( &args.configArray, "config", "c", []string{}, "Config to save") cmd.PersistentFlags().BoolVar( &args.configPath, "config-path", false, "Config keys contain a path to a property in a map or list to set") cmd.PersistentFlags().StringVarP( &args.description, "description", "d", "", "The project description; if not specified, a prompt will request it") cmd.PersistentFlags().StringVar( &args.dir, "dir", "", "The location to place the generated project; if not specified, the current directory is used") cmd.PersistentFlags().BoolVarP( &args.force, "force", "f", false, "Forces content to be generated even if it would change existing files") cmd.PersistentFlags().BoolVarP( &args.generateOnly, "generate-only", "g", false, "Generate the project only; do not create a stack, save config, or install dependencies") cmd.PersistentFlags().StringVarP( &args.name, "name", "n", "", "The project name; if not specified, a prompt will request it") cmd.PersistentFlags().BoolVarP( &args.offline, "offline", "o", false, "Use locally cached templates without making any network requests") cmd.PersistentFlags().StringVarP( &args.stack, "stack", "s", "", "The stack name; either an existing stack or stack to create; if not specified, a prompt will request it") cmd.PersistentFlags().BoolVarP( &args.yes, "yes", "y", false, "Skip prompts and proceed with default values") cmd.PersistentFlags().StringVar( &args.secretsProvider, "secrets-provider", "default", "The type of the provider that should be used to encrypt and "+ "decrypt secrets (possible choices: default, passphrase, awskms, azurekeyvault, gcpkms, hashivault)") return cmd } // errorIfNotEmptyDirectory returns an error if path is not empty. func errorIfNotEmptyDirectory(path string) error { infos, err := ioutil.ReadDir(path) if err != nil { return err } if len(infos) > 0 { return errors.Errorf("%s is not empty; "+ "rerun in an empty directory, pass the path to an empty directory to --dir, or use --force", path) } return nil } func validateProjectName(projectName string, generateOnly bool, opts display.Options) error { err := workspace.ValidateProjectName(projectName) if err != nil { return err } if !generateOnly { b, err := currentBackend(opts) if err != nil { return err } exists, err := b.DoesProjectExist(commandContext(), projectName) if err != nil { return err } if exists { return errors.New("A project with this name already exists") } } return nil } // getStack gets a stack and the project name & description, or returns nil if the stack doesn't exist. func getStack(stack string, opts display.Options) (backend.Stack, string, string, error) { b, err := currentBackend(opts) if err != nil { return nil, "", "", err } stackRef, err := b.ParseStackReference(stack) if err != nil { return nil, "", "", err } s, err := b.GetStack(commandContext(), stackRef) if err != nil { return nil, "", "", err } name := "" description := "" if s != nil { if cs, ok := s.(httpstate.Stack); ok { tags := cs.Tags() name = tags[apitype.ProjectNameTag] description = tags[apitype.ProjectDescriptionTag] } } return s, name, description, nil } // promptAndCreateStack creates and returns a new stack (prompting for the name as needed). func promptAndCreateStack(prompt promptForValueFunc, stack string, projectName string, setCurrent bool, yes bool, opts display.Options, secretsProvider string) (backend.Stack, error) { b, err := currentBackend(opts) if err != nil { return nil, err } if stack != "" { s, err := stackInit(b, stack, setCurrent, secretsProvider) if err != nil { return nil, err } return s, nil } if b.SupportsOrganizations() { fmt.Print("Please enter your desired stack name.\n" + "To create a stack in an organization, " + "use the format / (e.g. `acmecorp/dev`).\n") } for { stackName, err := prompt(yes, "stack name", "dev", false, workspace.ValidateStackName, opts) if err != nil { return nil, err } s, err := stackInit(b, stackName, setCurrent, secretsProvider) if err != nil { if !yes { // Let the user know about the error and loop around to try again. fmt.Printf("Sorry, could not create stack '%s': %v\n", stackName, err) continue } return nil, err } return s, nil } } // stackInit creates the stack. func stackInit(b backend.Backend, stackName string, setCurrent bool, secretsProvider string) (backend.Stack, error) { stackRef, err := b.ParseStackReference(stackName) if err != nil { return nil, err } return createStack(b, stackRef, nil, setCurrent, secretsProvider) } // saveConfig saves the config for the stack. func saveConfig(stack backend.Stack, c config.Map) error { ps, err := loadProjectStack(stack) if err != nil { return err } for k, v := range c { ps.Config[k] = v } return saveProjectStack(stack, ps) } // installDependencies will install dependencies for the project, e.g. by running `npm install` for nodejs projects. func installDependencies() error { proj, root, err := readProject() if err != nil { return err } // TODO[pulumi/pulumi#1307]: move to the language plugins so we don't have to hard code here. if strings.EqualFold(proj.Runtime.Name(), "nodejs") { if bin, err := nodeInstallDependencies(); err != nil { return errors.Wrapf(err, "%s install failed; rerun manually to try again, "+ "then run 'pulumi up' to perform an initial deployment", bin) } } else if strings.EqualFold(proj.Runtime.Name(), "dotnet") { return dotnetInstallDependenciesAndBuild(proj, root) } return nil } // nodeInstallDependencies will install dependencies for the project or Policy Pack by running `npm install` or // `yarn install`. func nodeInstallDependencies() (string, error) { fmt.Println("Installing dependencies...") fmt.Println() bin, err := npm.Install("", os.Stdout, os.Stderr) if err != nil { return bin, err } fmt.Println("Finished installing dependencies") fmt.Println() return bin, nil } // dotnetInstallDependenciesAndBuild will install dependencies and build the project. func dotnetInstallDependenciesAndBuild(proj *workspace.Project, root string) error { contract.Assert(proj != nil) fmt.Println("Installing dependencies...") fmt.Println() projinfo := &engine.Projinfo{Proj: proj, Root: root} pwd, main, plugctx, err := engine.ProjectInfoContext(projinfo, nil, nil, cmdutil.Diag(), cmdutil.Diag(), nil) if err != nil { return err } defer plugctx.Close() // Call RunInstallPlugins, which will run `dotnet build`. This will automatically // restore dependencies, install any plugins, and build the project so it will be // prepped and ready to go for a faster initial `pulumi up`. if err = engine.RunInstallPlugins(proj, pwd, main, nil, plugctx); err != nil { return err } fmt.Println("Finished installing dependencies") fmt.Println() return nil } // printNextSteps prints out a series of commands that the user needs to run before their stack is able to be updated. func printNextSteps(proj *workspace.Project, originalCwd, cwd string, generateOnly bool, opts display.Options) { var commands []string // If the target working directory is not the same as our current WD, tell the user to // CD to the target directory. if originalCwd != cwd { // If we can determine a relative path, use that, otherwise use the full path. var cd string if rel, err := filepath.Rel(originalCwd, cwd); err == nil { cd = rel } else { cd = cwd } // Surround the path with double quotes if it contains whitespace. if containsWhiteSpace(cd) { cd = fmt.Sprintf("\"%s\"", cd) } cd = fmt.Sprintf("cd %s", cd) commands = append(commands, cd) } if strings.EqualFold(proj.Runtime.Name(), "nodejs") && generateOnly { // If we're generating a NodeJS project, and we didn't install dependencies (generateOnly), // instruct the user to do so. commands = append(commands, "npm install") } else if strings.EqualFold(proj.Runtime.Name(), "python") { // If we're generating a Python project, instruct the user to set up and activate a virtual // environment. // Create the virtual environment. commands = append(commands, "virtualenv -p python3 venv") // Activate the virtual environment. Only active in the user's current shell, so we can't // just run it for the user here. switch runtime.GOOS { case "windows": commands = append(commands, "venv\\Scripts\\activate") default: commands = append(commands, "source venv/bin/activate") } // Install dependencies within the virtualenv commands = append(commands, "pip3 install -r requirements.txt") } // If we didn't create a stack, show that as a command to run before `pulumi up`. if generateOnly { commands = append(commands, "pulumi stack init") } if len(commands) == 0 { // No additional commands need to be run. deployMsg := "To perform an initial deployment, run 'pulumi up'" deployMsg = colors.Highlight(deployMsg, "pulumi up", colors.BrightBlue+colors.Bold) fmt.Println(opts.Color.Colorize(deployMsg)) fmt.Println() return } if len(commands) == 1 { // Only one additional command need to be run. deployMsg := fmt.Sprintf("To perform an initial deployment, run '%s', then, run 'pulumi up'", commands[0]) deployMsg = colors.Highlight(deployMsg, commands[0], colors.BrightBlue+colors.Bold) deployMsg = colors.Highlight(deployMsg, "pulumi up", colors.BrightBlue+colors.Bold) fmt.Println(opts.Color.Colorize(deployMsg)) fmt.Println() return } // One or more additional commands needs to be run. fmt.Println("To perform an initial deployment, run the following commands:") fmt.Println() for i, cmd := range commands { cmdColors := colors.BrightBlue + colors.Bold + cmd + colors.Reset fmt.Printf(" %d. %s\n", i+1, opts.Color.Colorize(cmdColors)) } fmt.Println() upMsg := colors.Highlight("Then, run 'pulumi up'", "pulumi up", colors.BrightBlue+colors.Bold) fmt.Println(opts.Color.Colorize(upMsg)) fmt.Println() } // chooseTemplate will prompt the user to choose amongst the available templates. func chooseTemplate(templates []workspace.Template, opts display.Options) (workspace.Template, error) { const chooseTemplateErr = "no template selected; please use `pulumi new` to choose one" if !opts.IsInteractive { return workspace.Template{}, errors.New(chooseTemplateErr) } // Customize the prompt a little bit (and disable color since it doesn't match our scheme). surveycore.DisableColor = true surveycore.QuestionIcon = "" surveycore.SelectFocusIcon = opts.Color.Colorize(colors.BrightGreen + ">" + colors.Reset) message := "\rPlease choose a template:" message = opts.Color.Colorize(colors.SpecPrompt + message + colors.Reset) showAll := false var selectedOption workspace.Template for { options, optionToTemplateMap := templatesToOptionArrayAndMap(templates, showAll) // If showAll was false and we got only a single result, force showAll to be true and try // again. if !showAll && len(options) <= 1 { showAll = true continue } var option string if err := survey.AskOne(&survey.Select{ Message: message, Options: options, PageSize: len(options), }, &option, nil); err != nil { return workspace.Template{}, errors.New(chooseTemplateErr) } var has bool selectedOption, has = optionToTemplateMap[option] if has { break } else { showAll = true } } return selectedOption, nil } // parseConfig parses the config values passed via command line flags. // These are passed as `-c aws:region=us-east-1 -c foo:bar=blah` and end up // in configArray as ["aws:region=us-east-1", "foo:bar=blah"]. // This function converts the array into a config.Map. func parseConfig(configArray []string, path bool) (config.Map, error) { configMap := make(config.Map) for _, c := range configArray { kvp := strings.SplitN(c, "=", 2) key, err := parseConfigKey(kvp[0]) if err != nil { return nil, err } value := config.NewValue("") if len(kvp) == 2 { value = config.NewValue(kvp[1]) } if err = configMap.Set(key, value, path); err != nil { return nil, err } } return configMap, nil } // promptForConfig will go through each config key needed by the template and prompt for a value. // If a config value exists in commandLineConfig, it will be used without prompting. // If stackConfig is non-nil and a config value exists in stackConfig, it will be used as the default // value when prompting instead of the default value specified in templateConfig. func promptForConfig( stack backend.Stack, templateConfig map[string]workspace.ProjectTemplateConfigValue, commandLineConfig config.Map, stackConfig config.Map, yes bool, opts display.Options) (config.Map, error) { // Convert `string` keys to `config.Key`. If a string key is missing a delimiter, // the project name will be prepended. parsedTemplateConfig := make(map[config.Key]workspace.ProjectTemplateConfigValue) for k, v := range templateConfig { parsedKey, parseErr := parseConfigKey(k) if parseErr != nil { return nil, parseErr } parsedTemplateConfig[parsedKey] = v } // Sort keys. Note that we use the fully qualified module member here instead of a `prettyKey` so that // all config values for the current program are prompted one after another. var keys config.KeyArray for k := range parsedTemplateConfig { keys = append(keys, k) } sort.Sort(keys) sm, err := getStackSecretsManager(stack) if err != nil { return nil, err } encrypter, err := sm.Encrypter() if err != nil { return nil, err } decrypter, err := sm.Decrypter() if err != nil { return nil, err } c := make(config.Map) for _, k := range keys { // If it was passed as a command line flag, use it without prompting. if val, ok := commandLineConfig[k]; ok { c[k] = val continue } templateConfigValue := parsedTemplateConfig[k] // Prepare a default value. var defaultValue string var secret bool if stackConfig != nil { // Use the stack's existing value as the default. if val, ok := stackConfig[k]; ok { // It's OK to pass a nil or non-nil crypter for non-secret values. value, err := val.Value(decrypter) if err != nil { return nil, err } defaultValue = value } } if defaultValue == "" { defaultValue = templateConfigValue.Default } if !secret { secret = templateConfigValue.Secret } // Prepare the prompt. prompt := prettyKey(k) if templateConfigValue.Description != "" { prompt = prompt + ": " + templateConfigValue.Description } // Prompt. value, err := promptForValue(yes, prompt, defaultValue, secret, nil, opts) if err != nil { return nil, err } // Encrypt the value if needed. var v config.Value if secret { enc, err := encrypter.EncryptValue(value) if err != nil { return nil, err } v = config.NewSecureValue(enc) } else { v = config.NewValue(value) } // Save it. c[k] = v } // Add any other config values from the command line. for k, v := range commandLineConfig { if _, ok := c[k]; !ok { c[k] = v } } return c, nil } // promptForValue prompts the user for a value with a defaultValue preselected. Hitting enter accepts the // default. If yes is true, defaultValue is returned without prompting. isValidFn is an optional parameter; // when specified, it will be run to validate that value entered. When this function returns a non nil error // validation is assumed to have failed and an error is printed. The error returned by isValidFn is also displayed // to provide information about why the validation failed. A period is appended to this message. `promptForValue` then // prompts again. func promptForValue( yes bool, valueType string, defaultValue string, secret bool, isValidFn func(value string) error, opts display.Options) (string, error) { if yes { return defaultValue, nil } for { var prompt string if defaultValue == "" { prompt = opts.Color.Colorize( fmt.Sprintf("%s%s:%s ", colors.SpecPrompt, valueType, colors.Reset)) } else { defaultValuePrompt := defaultValue if secret { defaultValuePrompt = "[secret]" } prompt = opts.Color.Colorize( fmt.Sprintf("%s%s:%s (%s) ", colors.SpecPrompt, valueType, colors.Reset, defaultValuePrompt)) } fmt.Print(prompt) // Read the value. var err error var value string if secret { value, err = cmdutil.ReadConsoleNoEcho("") if err != nil { return "", err } } else { value, err = cmdutil.ReadConsole("") if err != nil { return "", err } } value = strings.TrimSpace(value) if value != "" { var validationError error if isValidFn != nil { validationError = isValidFn(value) } if validationError == nil { return value, nil } // The value is invalid, let the user know and try again fmt.Printf("Sorry, '%s' is not a valid %s. %s.\n", value, valueType, validationError) continue } return defaultValue, nil } } // templatesToOptionArrayAndMap returns an array of option strings and a map of option strings to templates. // Each option string is made up of the template name and description with some padding in between. func templatesToOptionArrayAndMap(templates []workspace.Template, showAll bool) ([]string, map[string]workspace.Template) { // Find the longest name length. Used to add padding between the name and description. maxNameLength := 0 for _, template := range templates { if len(template.Name) > maxNameLength { maxNameLength = len(template.Name) } } // Build the array and map. var options []string nameToTemplateMap := make(map[string]workspace.Template) for _, template := range templates { // If showAll is false, then only include templates marked Important if !showAll && !template.Important { continue } // Create the option string that combines the name, padding, and description. desc := workspace.ValueOrDefaultProjectDescription("", template.ProjectDescription, template.Description) option := fmt.Sprintf(fmt.Sprintf("%%%ds %%s", -maxNameLength), template.Name, desc) // Add it to the array and map. options = append(options, option) nameToTemplateMap[option] = template } sort.Strings(options) if !showAll { // If showAll is false, include an option to show all option := "Show additional templates" options = append(options, option) } return options, nameToTemplateMap } // containsWhiteSpace returns true if the string contains whitespace. func containsWhiteSpace(value string) bool { for _, c := range value { if unicode.IsSpace(c) { return true } } return false }