These changes refactor direct interactions with the Pulumi API out of the cloud backend and into a subpackage, `pkg/backend/cloud/client`. This package exposes a slightly higher-level API that takes care of calculating paths, performing HTTP calls, and occasionally wrapping multiple physical calls into a single logical call (notably the creation of an update and the upload of its program). This is primarily intended as preparation for some of the changes suggested in the feedback for #1067.
218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/pulumi/pulumi/pkg/backend/cloud"
|
|
"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 offline bool
|
|
|
|
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)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// 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(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)
|
|
}
|
|
|
|
// Get the values to fill in.
|
|
name = workspace.ValueOrSanitizedDefaultProjectName(name, filepath.Base(cwd))
|
|
description = workspace.ValueOrDefaultProjectDescription(description, template.Description)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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.Println("Your project was created successfully.")
|
|
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, the name of the current working directory is used")
|
|
cmd.PersistentFlags().StringVarP(
|
|
&description, "description", "d", "",
|
|
"The project description; if not specified, a default description is used")
|
|
cmd.PersistentFlags().BoolVarP(
|
|
&force, "force", "f", false,
|
|
"Forces content to be generated even if it would change existing files")
|
|
cmd.PersistentFlags().BoolVarP(
|
|
&offline, "offline", "o", false,
|
|
"Allows offline use of cached templates without making any network requests")
|
|
|
|
return cmd
|
|
}
|
|
|
|
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(); 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
|
|
}
|
|
|
|
// 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
|
|
}
|