forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 0
/
apply.go
511 lines (429 loc) · 14.5 KB
/
apply.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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
package command
import (
"bytes"
"fmt"
"os"
"sort"
"strings"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/terraform"
)
// ApplyCommand is a Command implementation that applies a Terraform
// configuration and actually builds or changes infrastructure.
type ApplyCommand struct {
Meta
// If true, then this apply command will become the "destroy"
// command. It is just like apply but only processes a destroy.
Destroy bool
// When this channel is closed, the apply will be cancelled.
ShutdownCh <-chan struct{}
}
func (c *ApplyCommand) Run(args []string) int {
var destroyForce, refresh bool
args = c.Meta.process(args, true)
cmdName := "apply"
if c.Destroy {
cmdName = "destroy"
}
cmdFlags := c.Meta.flagSet(cmdName)
if c.Destroy {
cmdFlags.BoolVar(&destroyForce, "force", false, "force")
}
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
cmdFlags.IntVar(
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
var configPath string
maybeInit := true
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The apply command expects at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
maybeInit = false
}
// Prepare the extra hooks to count resources
countHook := new(CountHook)
stateHook := new(StateHook)
c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
if !c.Destroy && maybeInit {
// Do a detect to determine if we need to do an init + apply.
if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
c.Ui.Error(fmt.Sprintf(
"Invalid path: %s", err))
return 1
} else if !strings.HasPrefix(detected, "file") {
// If this isn't a file URL then we're doing an init +
// apply.
var init InitCommand
init.Meta = c.Meta
if code := init.Run([]string{detected}); code != 0 {
return code
}
// Change the config path to be the cwd
configPath = pwd
}
}
terraform.SetDebugInfo(DefaultDataDir)
// Check for the new apply
if experiment.Enabled(experiment.X_newApply) && !experiment.Force() {
desc := "Experimental new apply graph has been enabled. This may still\n" +
"have bugs, and should be used with care. If you'd like to continue,\n" +
"you must enter exactly 'yes' as a response."
v, err := c.UIInput().Input(&terraform.InputOpts{
Id: "Xnew-apply",
Query: "Experimental feature enabled: new apply graph. Continue?",
Description: desc,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
return 1
}
if v != "yes" {
c.Ui.Output("Apply cancelled.")
return 1
}
}
// Check for the new destroy
if experiment.Enabled(experiment.X_newDestroy) && !experiment.Force() {
desc := "Experimental new destroy graph has been enabled. This may still\n" +
"have bugs, and should be used with care. If you'd like to continue,\n" +
"you must enter exactly 'yes' as a response."
v, err := c.UIInput().Input(&terraform.InputOpts{
Id: "Xnew-destroy",
Query: "Experimental feature enabled: new destroy graph. Continue?",
Description: desc,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
return 1
}
if v != "yes" {
c.Ui.Output("Apply cancelled.")
return 1
}
}
// This is going to keep track of shadow errors
var shadowErr error
// Build the context based on the arguments given
ctx, planned, err := c.Context(contextOpts{
Destroy: c.Destroy,
Path: configPath,
StatePath: c.Meta.statePath,
Parallelism: c.Meta.parallelism,
})
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if c.Destroy && planned {
c.Ui.Error(fmt.Sprintf(
"Destroy can't be called with a plan file."))
return 1
}
if !destroyForce && c.Destroy {
// Default destroy message
desc := "Terraform will delete all your managed infrastructure.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
// If targets are specified, list those to user
if c.Meta.targets != nil {
var descBuffer bytes.Buffer
descBuffer.WriteString("Terraform will delete the following infrastructure:\n")
for _, target := range c.Meta.targets {
descBuffer.WriteString("\t")
descBuffer.WriteString(target)
descBuffer.WriteString("\n")
}
descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm")
desc = descBuffer.String()
}
v, err := c.UIInput().Input(&terraform.InputOpts{
Id: "destroy",
Query: "Do you really want to destroy?",
Description: desc,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
return 1
}
if v != "yes" {
c.Ui.Output("Destroy cancelled.")
return 1
}
}
if !planned {
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "input operation:"))
}
}
if !validateContext(ctx, c.Ui) {
return 1
}
// Plan if we haven't already
if !planned {
if refresh {
if _, err := ctx.Refresh(); err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}
}
if _, err := ctx.Plan(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error creating plan: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "plan operation:"))
}
}
// Setup the state hook for continuous state updates
{
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading state: %s", err))
return 1
}
stateHook.State = state
}
// Start the apply in a goroutine so that we can be interrupted.
var state *terraform.State
var applyErr error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
state, applyErr = ctx.Apply()
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "apply operation:"))
}
}()
// Wait for the apply to finish or for us to be interrupted so
// we can handle it properly.
err = nil
select {
case <-c.ShutdownCh:
c.Ui.Output("Interrupt received. Gracefully shutting down...")
// Stop execution
go ctx.Stop()
// Still get the result, since there is still one
select {
case <-c.ShutdownCh:
c.Ui.Error(
"Two interrupts received. Exiting immediately. Note that data\n" +
"loss may have occurred.")
return 1
case <-doneCh:
}
case <-doneCh:
}
// Persist the state
if state != nil {
if err := c.Meta.PersistState(state); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
return 1
}
}
if applyErr != nil {
c.Ui.Error(fmt.Sprintf(
"Error applying plan:\n\n"+
"%s\n\n"+
"Terraform does not automatically rollback in the face of errors.\n"+
"Instead, your Terraform state file has been partially updated with\n"+
"any resources that successfully completed. Please address the error\n"+
"above and apply again to incrementally change your infrastructure.",
multierror.Flatten(applyErr)))
return 1
}
if c.Destroy {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Destroy complete! Resources: %d destroyed.",
countHook.Removed)))
} else {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
countHook.Added,
countHook.Changed,
countHook.Removed)))
}
if countHook.Added > 0 || countHook.Changed > 0 {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset]\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
c.Meta.StateOutPath())))
}
if !c.Destroy {
if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}
}
// If we have an error in the shadow graph, let the user know.
c.outputShadowError(shadowErr, applyErr == nil)
return 0
}
func (c *ApplyCommand) Help() string {
if c.Destroy {
return c.helpDestroy()
}
return c.helpApply()
}
func (c *ApplyCommand) Synopsis() string {
if c.Destroy {
return "Destroy Terraform-managed infrastructure"
}
return "Builds or changes infrastructure"
}
func (c *ApplyCommand) helpApply() string {
helpText := `
Usage: terraform apply [options] [DIR-OR-PLAN]
Builds or changes infrastructure according to Terraform configuration
files in DIR.
By default, apply scans the current directory for the configuration
and applies the changes appropriately. However, a path to another
configuration or an execution plan can be provided. Execution plans can be
used to only execute a pre-determined set of actions.
DIR can also be a SOURCE as given to the "init" command. In this case,
apply behaves as though "init" was called followed by "apply". This only
works for sources that aren't files, and only if the current working
directory is empty of Terraform files. This is a shortcut for getting
started.
Options:
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup.
-input=true Ask for input for variables if not directly set.
-no-color If specified, output won't contain any color.
-parallelism=n Limit the number of concurrent operations.
Defaults to 10.
-refresh=true Update state prior to checking for differences. This
has no effect if a plan file is given to apply.
-state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate".
-state-out=path Path to write state to that is different than
"-state". This can be used to preserve the old
state.
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" is present, it will be
automatically loaded if this flag is not specified.
`
return strings.TrimSpace(helpText)
}
func (c *ApplyCommand) helpDestroy() string {
helpText := `
Usage: terraform destroy [options] [DIR]
Destroy Terraform-managed infrastructure.
Options:
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup.
-force Don't ask for input for destroy confirmation.
-no-color If specified, output won't contain any color.
-parallelism=n Limit the number of concurrent operations.
Defaults to 10.
-refresh=true Update state prior to checking for differences. This
has no effect if a plan file is given to apply.
-state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate".
-state-out=path Path to write state to that is different than
"-state". This can be used to preserve the old
state.
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" is present, it will be
automatically loaded if this flag is not specified.
`
return strings.TrimSpace(helpText)
}
func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
if state == nil {
return ""
}
ms := state.ModuleByPath(modPath)
if ms == nil {
return ""
}
outputs := ms.Outputs
outputBuf := new(bytes.Buffer)
if len(outputs) > 0 {
schemaMap := make(map[string]*config.Output)
if schema != nil {
for _, s := range schema {
schemaMap[s.Name] = s
}
}
if includeHeader {
outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
}
// Output the outputs in alphabetical order
keyLen := 0
ks := make([]string, 0, len(outputs))
for key, _ := range outputs {
ks = append(ks, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(ks)
for _, k := range ks {
schema, ok := schemaMap[k]
if ok && schema.Sensitive {
outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
continue
}
v := outputs[k]
switch typedV := v.Value.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
case []interface{}:
outputBuf.WriteString(formatListOutput("", k, typedV))
outputBuf.WriteString("\n")
case map[string]interface{}:
outputBuf.WriteString(formatMapOutput("", k, typedV))
outputBuf.WriteString("\n")
}
}
}
return strings.TrimSpace(outputBuf.String())
}