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:
Justin Van Patten 2018-03-13 16:09:25 -07:00 committed by GitHub
parent 134941dd22
commit c7985ed296
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 34 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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
}