Support offline template descriptions (#1044)
Note: This is a minor issue that I didn't get to for M11 that isn't required for M11 and would be fine merging for post-M11. When you specify a template name explicitly (e.g. `pulumi new typescript`), we'll try to download the template tarball without first downloading the JSON list of available templates. The JSON includes a description used when replacing the `${DESCRIPTION}` string in template files. Since we didn't download the JSON, we won't have a description, so we fallback to a default value (`"A Pulumi project."`). This also happens when specifying `--offline` to use an existing template under `~/.pulumi/templates`; we won't have a description for the template, so we fallback to a default description. The fallback value happens to be the same as the description for each of our current templates, so noone will currently notice an issue. For M11, I included initial support for a template manifest file where the description (and any future metadata) could be stored, but didn't go as far as actually reading the file. This change makes it so the CLI actually reads the description from the manifest file (if it exists), otherwise falling back to the default value as is done currently. Some minor related cleanup is included in this change.
This commit is contained in:
parent
134941dd22
commit
c7985ed296
46
cmd/new.go
46
cmd/new.go
|
@ -53,11 +53,11 @@ func newNewCmd() *cobra.Command {
|
|||
releases := cloud.New(cmdutil.Diag(), getCloudURL(cloudURL))
|
||||
|
||||
// Get the selected template.
|
||||
var template workspace.Template
|
||||
var templateName string
|
||||
if len(args) > 0 {
|
||||
template = workspace.Template{Name: strings.ToLower(args[0])}
|
||||
templateName = strings.ToLower(args[0])
|
||||
} else {
|
||||
if template, err = chooseTemplate(releases, offline); err != nil {
|
||||
if templateName, err = chooseTemplate(releases, offline); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -66,43 +66,49 @@ func newNewCmd() *cobra.Command {
|
|||
if !offline {
|
||||
var tarball io.ReadCloser
|
||||
source := releases.CloudURL()
|
||||
if tarball, err = releases.DownloadTemplate(template.Name, false); err != nil {
|
||||
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[template.Name]; ok {
|
||||
if _, ok := m[templateName]; ok {
|
||||
message = fmt.Sprintf(
|
||||
"; rerun the command and pass --offline to use locally cached template '%s'",
|
||||
template.Name)
|
||||
templateName)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrapf(err, "downloading template '%s' from %s%s", template.Name, source, message)
|
||||
return errors.Wrapf(err, "downloading template '%s' from %s%s", templateName, source, message)
|
||||
}
|
||||
if err = workspace.InstallTemplate(template.Name, tarball); err != nil {
|
||||
return errors.Wrapf(err, "installing template '%s' from %s", template.Name, source)
|
||||
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 = workspace.CopyTemplateFilesDryRun(template.Name, cwd); err != nil {
|
||||
if err = template.CopyTemplateFilesDryRun(cwd); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "template not found")
|
||||
return errors.Wrapf(err, "template '%s' not found", templateName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Actually copy the files.
|
||||
if err = workspace.CopyTemplateFiles(template.Name, cwd, force, name, description); err != nil {
|
||||
if err = template.CopyTemplateFiles(cwd, force, name, description); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "template not found")
|
||||
return errors.Wrapf(err, "template '%s' not found", templateName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -146,10 +152,10 @@ func getCloudURL(cloudURL string) string {
|
|||
}
|
||||
|
||||
// chooseTemplate will prompt the user to choose amongst the available templates.
|
||||
func chooseTemplate(backend cloud.Backend, offline bool) (workspace.Template, error) {
|
||||
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 workspace.Template{}, errors.New(chooseTemplateErr)
|
||||
return "", errors.New(chooseTemplateErr)
|
||||
}
|
||||
|
||||
var templates []workspace.Template
|
||||
|
@ -166,11 +172,11 @@ func chooseTemplate(backend cloud.Backend, offline bool) (workspace.Template, er
|
|||
strings.Join(options, ", ")
|
||||
}
|
||||
|
||||
return workspace.Template{}, errors.Wrap(err, message)
|
||||
return "", errors.Wrap(err, message)
|
||||
}
|
||||
} else {
|
||||
if templates, err = workspace.ListLocalTemplates(); err != nil || len(templates) == 0 {
|
||||
return workspace.Template{}, errors.Wrap(err, chooseTemplateErr)
|
||||
return "", errors.Wrap(err, chooseTemplateErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,17 +187,17 @@ func chooseTemplate(backend cloud.Backend, offline bool) (workspace.Template, er
|
|||
message := "\rPlease choose a template:"
|
||||
message = colors.ColorizeText(colors.BrightWhite + message + colors.Reset)
|
||||
|
||||
options, nameToTemplateMap := templateArrayToStringArrayAndMap(templates)
|
||||
options, _ := templateArrayToStringArrayAndMap(templates)
|
||||
|
||||
var option string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: message,
|
||||
Options: options,
|
||||
}, &option, nil); err != nil {
|
||||
return workspace.Template{}, errors.New(chooseTemplateErr)
|
||||
return "", errors.New(chooseTemplateErr)
|
||||
}
|
||||
|
||||
return nameToTemplateMap[option], nil
|
||||
return option, nil
|
||||
}
|
||||
|
||||
// templateArrayToStringArrayAndMap returns an array of template names and map of names to templates
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
|
@ -36,6 +38,39 @@ type Template struct {
|
|||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// templateManifest represents a template's manifest file.
|
||||
type templateManifest struct {
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// LoadLocalTemplate returns a local template.
|
||||
func LoadLocalTemplate(name string) (Template, error) {
|
||||
template := Template{Name: name}
|
||||
|
||||
templateDir, err := GetTemplateDir(name)
|
||||
if err != nil {
|
||||
return Template{}, err
|
||||
}
|
||||
|
||||
info, err := os.Stat(templateDir)
|
||||
if err != nil {
|
||||
return Template{}, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return Template{}, errors.Errorf("template '%s' in %s is not a directory", name, templateDir)
|
||||
}
|
||||
|
||||
// Read the description from the manifest (if it exists).
|
||||
manifest, err := readTemplateManifest(filepath.Join(templateDir, pulumiTemplateManifestFile))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return Template{}, err
|
||||
} else if err == nil && manifest.Description != "" {
|
||||
template.Description = manifest.Description
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// ListLocalTemplates returns a list of local templates.
|
||||
func ListLocalTemplates() ([]Template, error) {
|
||||
templateDir, err := GetTemplateDir("")
|
||||
|
@ -51,7 +86,11 @@ func ListLocalTemplates() ([]Template, error) {
|
|||
var templates []Template
|
||||
for _, info := range infos {
|
||||
if info.IsDir() {
|
||||
templates = append(templates, Template{Name: info.Name()})
|
||||
template, err := LoadLocalTemplate(info.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
}
|
||||
return templates, nil
|
||||
|
@ -97,10 +136,10 @@ func InstallTemplate(name string, tarball io.ReadCloser) error {
|
|||
|
||||
// CopyTemplateFilesDryRun does a dry run of copying a template to a destination directory,
|
||||
// to ensure it won't overwrite any files.
|
||||
func CopyTemplateFilesDryRun(name string, destDir string) error {
|
||||
func (template Template) CopyTemplateFilesDryRun(destDir string) error {
|
||||
var err error
|
||||
var sourceDir string
|
||||
if sourceDir, err = GetTemplateDir(name); err != nil {
|
||||
if sourceDir, err = GetTemplateDir(template.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -122,8 +161,10 @@ func CopyTemplateFilesDryRun(name string, destDir string) error {
|
|||
}
|
||||
|
||||
// CopyTemplateFiles does the actual copy operation to a destination directory.
|
||||
func CopyTemplateFiles(name string, destDir string, force bool, projectName string, projectDescription string) error {
|
||||
sourceDir, err := GetTemplateDir(name)
|
||||
func (template Template) CopyTemplateFiles(
|
||||
destDir string, force bool, projectName string, projectDescription string) error {
|
||||
|
||||
sourceDir, err := GetTemplateDir(template.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -302,6 +343,22 @@ func walkFiles(sourceDir string, destDir string,
|
|||
return nil
|
||||
}
|
||||
|
||||
// readTemplateManifest reads a template manifest file.
|
||||
func readTemplateManifest(filename string) (templateManifest, error) {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return templateManifest{}, err
|
||||
}
|
||||
|
||||
var manifest templateManifest
|
||||
err = yaml.Unmarshal(b, &manifest)
|
||||
if err != nil {
|
||||
return templateManifest{}, err
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// newExistingFilesError returns a new error from a list of existing file names
|
||||
// that would be overwritten.
|
||||
func newExistingFilesError(existing []string) error {
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory and CD into it.
|
||||
|
@ -38,6 +38,28 @@ func TestPulumiNew(t *testing.T) {
|
|||
assertSuccess(t, subdir, "foo", "A Pulumi project.")
|
||||
})
|
||||
|
||||
t.Run("SanityTestWithManifest", func(t *testing.T) {
|
||||
e := ptesting.NewEnvironment(t)
|
||||
defer deleteIfNotFailed(e)
|
||||
|
||||
const description = "My project description."
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t, description)
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory and CD into it.
|
||||
subdir := path.Join(e.RootPath, "foo")
|
||||
err := os.MkdirAll(subdir, os.ModePerm)
|
||||
assert.NoError(t, err, "error creating subdirectory")
|
||||
e.CWD = subdir
|
||||
|
||||
// Run pulumi new.
|
||||
e.RunCommand("pulumi", "new", template, "--offline")
|
||||
|
||||
assertSuccess(t, subdir, "foo", description)
|
||||
})
|
||||
|
||||
t.Run("NoTemplateSpecified", func(t *testing.T) {
|
||||
e := ptesting.NewEnvironment(t)
|
||||
defer deleteIfNotFailed(e)
|
||||
|
@ -101,7 +123,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Run pulumi new.
|
||||
|
@ -115,7 +137,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory that contains an invalid char
|
||||
|
@ -137,7 +159,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory and CD into it.
|
||||
|
@ -172,7 +194,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory and CD into it.
|
||||
|
@ -218,7 +240,7 @@ func TestPulumiNew(t *testing.T) {
|
|||
defer deleteIfNotFailed(e)
|
||||
|
||||
// Create a temporary local template.
|
||||
template := createTemporaryLocalTemplate(t)
|
||||
template := createTemporaryLocalTemplate(t, "")
|
||||
defer deleteTemporaryLocalTemplate(t, template)
|
||||
|
||||
// Create a subdirectory and CD into it.
|
||||
|
@ -263,6 +285,10 @@ func assertSuccess(t *testing.T, dir string, expectedProjectName string, expecte
|
|||
// Confirm the .pulumi.template.yaml file was skipped.
|
||||
_, err := os.Stat(filepath.Join(dir, ".pulumi.template.yaml"))
|
||||
assert.Error(t, err)
|
||||
|
||||
infos, err := ioutil.ReadDir(dir)
|
||||
assert.NoError(t, err, "reading dir")
|
||||
assert.Equal(t, 4, len(infos))
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, filename string) string {
|
||||
|
@ -271,7 +297,7 @@ func readFile(t *testing.T, filename string) string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
func createTemporaryLocalTemplate(t *testing.T) string {
|
||||
func createTemporaryLocalTemplate(t *testing.T, description string) string {
|
||||
name := fmt.Sprintf("%v", time.Now().UnixNano())
|
||||
dir := getTemplateDir(t, name)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
|
@ -295,8 +321,12 @@ func createTemporaryLocalTemplate(t *testing.T) string {
|
|||
err = ioutil.WriteFile(filepath.Join(dir, "sub", "blah.json"), []byte("{}"), 0600)
|
||||
assert.NoError(t, err, "creating sub/blah.json")
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(dir, ".pulumi.template.yaml"), []byte{}, 0600)
|
||||
assert.NoError(t, err, "creating .pulumi.template.yaml")
|
||||
// If description is not empty, write it out in a manifest file.
|
||||
if description != "" {
|
||||
descriptionBytes := []byte(fmt.Sprintf("description: %s", description))
|
||||
err = ioutil.WriteFile(filepath.Join(dir, ".pulumi.template.yaml"), descriptionBytes, 0600)
|
||||
assert.NoError(t, err, "creating .pulumi.template.yaml")
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue