pulumi/cmd/new.go
Justin Van Patten 6a11d049b7
Add optional --dir flag to pulumi new (#1459)
Usage:

```
pulumi new <template> --dir folderName
```

Used to specify the directory where to place the generated project.
If the directory does not exist, it will be created.
2018-06-04 13:33:58 -07:00

439 lines
14 KiB
Go

// 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 (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/backend/cloud"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/workspace"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/spf13/cobra"
survey "gopkg.in/AlecAivazis/survey.v1"
surveycore "gopkg.in/AlecAivazis/survey.v1/core"
)
const defaultURLEnvVar = "PULUMI_TEMPLATE_API"
func newNewCmd() *cobra.Command {
var cloudURL string
var name string
var description string
var force bool
var yes bool
var offline bool
var generateOnly bool
var dir string
cmd := &cobra.Command{
Use: "new [template]",
Short: "Create a new Pulumi project",
Args: cmdutil.MaximumNArgs(1),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
var err error
// Validate name (if specified) before further prompts/operations.
if name != "" && !workspace.IsValidProjectName(name) {
return errors.Errorf("'%s' is not a valid project name", name)
}
// If dir was specified, ensure it exists and use it as the
// current working directory.
if dir != "" {
// 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 current working directory.
var cwd string
if cwd, err = os.Getwd(); err != nil {
return errors.Wrap(err, "getting the working directory")
}
releases, err := cloud.New(cmdutil.Diag(), getCloudURL(cloudURL))
if err != nil {
return errors.Wrap(err, "creating API client")
}
// 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).
var b backend.Backend
if !generateOnly {
b, err = currentBackend()
if err != nil {
return err
}
}
// Get the selected template.
var templateName string
if len(args) > 0 {
templateName = strings.ToLower(args[0])
} else {
if templateName, err = chooseTemplate(releases, offline); err != nil {
return err
}
}
// Download and install the template to the local template cache.
if !offline {
var tarball io.ReadCloser
source := releases.CloudURL()
if tarball, err = releases.DownloadTemplate(commandContext(), templateName, false); err != nil {
message := ""
// If the local template is available locally, provide a nicer error message.
if localTemplates, localErr := workspace.ListLocalTemplates(); localErr == nil && len(localTemplates) > 0 {
_, m := templateArrayToStringArrayAndMap(localTemplates)
if _, ok := m[templateName]; ok {
message = fmt.Sprintf(
"; rerun the command and pass --offline to use locally cached template '%s'",
templateName)
}
}
return errors.Wrapf(err, "downloading template '%s' from %s%s", templateName, source, message)
}
if err = workspace.InstallTemplate(templateName, tarball); err != nil {
return errors.Wrapf(err, "installing template '%s' from %s", templateName, source)
}
}
// Load the local template.
var template workspace.Template
if template, err = workspace.LoadLocalTemplate(templateName); err != nil {
return errors.Wrapf(err, "template '%s' not found", templateName)
}
// Do a dry run, if we're not forcing files to be overwritten.
if !force {
if err = template.CopyTemplateFilesDryRun(cwd); err != nil {
if os.IsNotExist(err) {
return errors.Wrapf(err, "template '%s' not found", templateName)
}
return err
}
}
// Show instructions, if we're going to show at least one prompt.
hasAtLeastOnePrompt := (name == "") || (description == "") || !generateOnly
if !yes && hasAtLeastOnePrompt {
fmt.Println("This command will walk you through creating a new Pulumi project.")
fmt.Println()
fmt.Println("Enter a value or leave blank to accept the default, and press <ENTER>.")
fmt.Println("Press ^C at any time to quit.")
}
// Prompt for the project name, if it wasn't already specified.
if name == "" {
defaultValue := workspace.ValueOrSanitizedDefaultProjectName(name, filepath.Base(cwd))
name = promptForValue(yes, "project name", defaultValue, workspace.IsValidProjectName)
}
// Prompt for the project description, if it wasn't already specified.
if description == "" {
defaultValue := workspace.ValueOrDefaultProjectDescription(description, template.Description)
description = promptForValue(yes, "project description", defaultValue, nil)
}
// Actually copy the files.
if err = template.CopyTemplateFiles(cwd, force, name, description); err != nil {
if os.IsNotExist(err) {
return errors.Wrapf(err, "template '%s' not found", templateName)
}
return err
}
fmt.Printf("Created project '%s'.\n", name)
// Prompt for the stack name and create the stack.
var stack backend.Stack
if !generateOnly {
defaultValue := getDevStackName(name)
for {
stackName := promptForValue(yes, "stack name", defaultValue, nil)
stack, err = stackInit(b, stackName)
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 err
}
break
}
// The backend will print "Created stack '<stack>'." on success.
}
// Prompt for config values and save.
if !generateOnly {
var keys config.KeyArray
for k := range template.Config {
keys = append(keys, k)
}
if len(keys) > 0 {
sort.Sort(keys)
c := make(config.Map)
for _, k := range keys {
value := promptForValue(yes, k.String(), template.Config[k], nil)
c[k] = config.NewValue(value)
}
if err = saveConfig(stack.Name().StackName(), c); err != nil {
return errors.Wrap(err, "saving config")
}
fmt.Println("Saved config.")
}
}
// Install dependencies.
if !generateOnly && template.InstallDependencies {
fmt.Println("Installing dependencies...")
err = installDependencies()
if err != nil {
return errors.Wrap(err, "installing dependencies")
}
fmt.Println("Finished installing dependencies.")
// Write a summary with next steps.
fmt.Println("New project is configured and ready to deploy with 'pulumi update'.")
}
return nil
}),
}
cmd.PersistentFlags().StringVarP(&cloudURL,
"cloud-url", "c", "", "A cloud URL to download templates from")
cmd.PersistentFlags().StringVarP(
&name, "name", "n", "",
"The project name; if not specified, a prompt will request it")
cmd.PersistentFlags().StringVarP(
&description, "description", "d", "",
"The project description; if not specified, a prompt will request it")
cmd.PersistentFlags().BoolVarP(
&force, "force", "f", false,
"Forces content to be generated even if it would change existing files")
cmd.PersistentFlags().BoolVarP(
&yes, "yes", "y", false,
"Skip prompts and proceed with default values")
cmd.PersistentFlags().BoolVarP(
&offline, "offline", "o", false,
"Use locally cached templates without making any network requests")
cmd.PersistentFlags().BoolVar(
&generateOnly, "generate-only", false,
"Generate the project only; do not create a stack, save config, or install dependencies")
cmd.PersistentFlags().StringVar(&dir, "dir", "",
"The location to place the generated project; if not specified, the current directory is used")
return cmd
}
// getDevStackName returns the stack name suffixed with -dev.
func getDevStackName(name string) string {
const suffix = "-dev"
// Strip the suffix so we don't include two -dev suffixes
// if the name already has it.
return strings.TrimSuffix(name, suffix) + suffix
}
// stackInit creates the stack.
func stackInit(b backend.Backend, stackName string) (backend.Stack, error) {
stackRef, err := b.ParseStackReference(stackName)
if err != nil {
return nil, err
}
return createStack(b, stackRef, nil)
}
// saveConfig saves the config for the stack.
func saveConfig(stackName tokens.QName, c config.Map) error {
ps, err := workspace.DetectProjectStack(stackName)
if err != nil {
return err
}
for k, v := range c {
ps.Config[k] = v
}
return workspace.SaveProjectStack(stackName, ps)
}
// installDependencies will install dependencies for the project, e.g. by running
// `npm install` for nodejs projects or `pip install` for python projects.
func installDependencies() error {
proj, _, 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.
var c *exec.Cmd
if strings.EqualFold(proj.Runtime, "nodejs") {
c = exec.Command("npm", "install") // nolint: gas, intentionally launching with partial path
} else if strings.EqualFold(proj.Runtime, "python") {
c = exec.Command("pip", "install", "-r", "requirements.txt") // nolint: gas, intentionally launching with partial path
} else {
return nil
}
// Run the command.
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}
// getCloudURL returns the URL used to download the template.
func getCloudURL(cloudURL string) string {
// If we have a cloud URL, just return it.
if cloudURL != "" {
return cloudURL
}
// Otherwise, respect the PULUMI_TEMPLATE_API override.
if fromEnv := os.Getenv(defaultURLEnvVar); fromEnv != "" {
return fromEnv
}
// Otherwise, use the default.
return cloud.DefaultURL()
}
// chooseTemplate will prompt the user to choose amongst the available templates.
func chooseTemplate(backend cloud.Backend, offline bool) (string, error) {
const chooseTemplateErr = "no template selected; please use `pulumi new` to choose one"
if !cmdutil.Interactive() {
return "", errors.New(chooseTemplateErr)
}
var templates []workspace.Template
var err error
if !offline {
if templates, err = backend.ListTemplates(commandContext()); err != nil {
message := "could not fetch list of remote templates"
// If we couldn't fetch the list, see if there are any local templates
if localTemplates, localErr := workspace.ListLocalTemplates(); localErr == nil && len(localTemplates) > 0 {
options, _ := templateArrayToStringArrayAndMap(localTemplates)
message = message + "\nrerun the command and pass --offline to use locally cached templates: " +
strings.Join(options, ", ")
}
return "", errors.Wrap(err, message)
}
} else {
if templates, err = workspace.ListLocalTemplates(); err != nil || len(templates) == 0 {
return "", errors.Wrap(err, chooseTemplateErr)
}
}
// Customize the prompt a little bit (and disable color since it doesn't match our scheme).
surveycore.DisableColor = true
surveycore.QuestionIcon = ""
surveycore.SelectFocusIcon = colors.ColorizeText(colors.BrightGreen + ">" + colors.Reset)
message := "\rPlease choose a template:"
message = colors.ColorizeText(colors.BrightWhite + message + colors.Reset)
options, _ := templateArrayToStringArrayAndMap(templates)
var option string
if err := survey.AskOne(&survey.Select{
Message: message,
Options: options,
}, &option, nil); err != nil {
return "", errors.New(chooseTemplateErr)
}
return option, 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. An invalid value will result in an error
// message followed by another prompt for the value.
func promptForValue(yes bool, prompt string, defaultValue string, isValidFn func(value string) bool) string {
if yes {
return defaultValue
}
for {
if defaultValue == "" {
prompt = colors.ColorizeText(
fmt.Sprintf("%s%s:%s ", colors.BrightCyan, prompt, colors.Reset))
} else {
prompt = colors.ColorizeText(
fmt.Sprintf("%s%s: (%s)%s ", colors.BrightCyan, prompt, defaultValue, colors.Reset))
}
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
value := strings.TrimSpace(line)
if value != "" {
if isValidFn == nil || isValidFn(value) {
return value
}
// The value is invalid, let the user know and try again
fmt.Printf("Sorry, '%s' is not a valid %s.\n", value, prompt)
continue
}
return defaultValue
}
}
// templateArrayToStringArrayAndMap returns an array of template names and map of names to templates
// from an array of templates.
func templateArrayToStringArrayAndMap(templates []workspace.Template) ([]string, map[string]workspace.Template) {
var options []string
nameToTemplateMap := make(map[string]workspace.Template)
for _, template := range templates {
options = append(options, template.Name)
nameToTemplateMap[template.Name] = template
}
sort.Strings(options)
return options, nameToTemplateMap
}