forked from pulumi/pulumi
/
new.go
438 lines (374 loc) · 13.7 KB
/
new.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
// 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
}