-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
backend_local.go
496 lines (448 loc) · 18.7 KB
/
backend_local.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
package local
import (
"context"
"fmt"
"log"
"sort"
"strings"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// backend.Local implementation.
func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) {
// Make sure the type is invalid. We use this as a way to know not
// to ask for input/validate. We're modifying this through a pointer,
// so we're mutating an object that belongs to the caller here, which
// seems bad but we're preserving it for now until we have time to
// properly design this API, vs. just preserving whatever it currently
// happens to do.
op.Type = backend.OperationTypeInvalid
op.StateLocker = op.StateLocker.WithContext(context.Background())
lr, _, stateMgr, diags := b.localRun(op)
return lr, stateMgr, diags
}
func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Get the latest state.
log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace)
s, err := b.StateMgr(op.Workspace)
if err != nil {
diags = diags.Append(fmt.Errorf("error loading state: %w", err))
return nil, nil, nil, diags
}
log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace)
if diags := op.StateLocker.Lock(s, op.Type.String()); diags.HasErrors() {
return nil, nil, nil, diags
}
defer func() {
// If we're returning with errors, and thus not producing a valid
// context, we'll want to avoid leaving the workspace locked.
if diags.HasErrors() {
diags = diags.Append(op.StateLocker.Unlock())
}
}()
log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace)
if err := s.RefreshState(); err != nil {
diags = diags.Append(fmt.Errorf("error loading state: %w", err))
return nil, nil, nil, diags
}
ret := &backend.LocalRun{}
// Initialize our context options
var coreOpts terraform.ContextOpts
if v := b.ContextOpts; v != nil {
coreOpts = *v
}
coreOpts.UIInput = op.UIIn
coreOpts.Hooks = op.Hooks
var ctxDiags tfdiags.Diagnostics
var configSnap *configload.Snapshot
if op.PlanFile != nil {
var stateMeta *statemgr.SnapshotMeta
// If the statemgr implements our optional PersistentMeta interface then we'll
// additionally verify that the state snapshot in the plan file has
// consistent metadata, as an additional safety check.
if sm, ok := s.(statemgr.PersistentMeta); ok {
m := sm.StateSnapshotMeta()
stateMeta = &m
}
log.Printf("[TRACE] backend/local: populating backend.LocalRun from plan file")
ret, configSnap, ctxDiags = b.localRunForPlanFile(op, op.PlanFile, ret, &coreOpts, stateMeta)
if ctxDiags.HasErrors() {
diags = diags.Append(ctxDiags)
return nil, nil, nil, diags
}
// Write sources into the cache of the main loader so that they are
// available if we need to generate diagnostic message snippets.
op.ConfigLoader.ImportSourcesFromSnapshot(configSnap)
} else {
log.Printf("[TRACE] backend/local: populating backend.LocalRun for current working directory")
ret, configSnap, ctxDiags = b.localRunDirect(op, ret, &coreOpts, s)
}
diags = diags.Append(ctxDiags)
if diags.HasErrors() {
return nil, nil, nil, diags
}
// If we have an operation, then we automatically do the input/validate
// here since every option requires this.
if op.Type != backend.OperationTypeInvalid {
// If input asking is enabled, then do that
if op.PlanFile == nil && b.OpInput {
mode := terraform.InputModeProvider
log.Printf("[TRACE] backend/local: requesting interactive input, if necessary")
inputDiags := ret.Core.Input(ret.Config, mode)
diags = diags.Append(inputDiags)
if inputDiags.HasErrors() {
return nil, nil, nil, diags
}
}
// If validation is enabled, validate
if b.OpValidation {
log.Printf("[TRACE] backend/local: running validation operation")
validateDiags := ret.Core.Validate(ret.Config)
diags = diags.Append(validateDiags)
}
}
return ret, configSnap, s, diags
}
func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Load the configuration using the caller-provided configuration loader.
config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, nil, diags
}
run.Config = config
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
var buf strings.Builder
for _, err := range errs {
fmt.Fprintf(&buf, "\n - %s", err.Error())
}
var suggestion string
switch {
case op.DependencyLocks == nil:
// If we get here then it suggests that there's a caller that we
// didn't yet update to populate DependencyLocks, which is a bug.
suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!"
case op.DependencyLocks.Empty():
suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init"
default:
suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade"
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(
"The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s",
buf.String(), suggestion,
),
))
}
var rawVariables map[string]backend.UnparsedVariableValue
if op.AllowUnsetVariables {
// Rather than prompting for input, we'll just stub out the required
// but unset variables with unknown values to represent that they are
// placeholders for values the user would need to provide for other
// operations.
rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables)
} else {
// If interactive input is enabled, we might gather some more variable
// values through interactive prompts.
// TODO: Need to route the operation context through into here, so that
// the interactive prompts can be sensitive to its timeouts/etc.
rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn)
}
variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
}
planOpts := &terraform.PlanOpts{
Mode: op.PlanMode,
Targets: op.Targets,
ForceReplace: op.ForceReplace,
SetVariables: variables,
SkipRefresh: op.Type != backend.OperationTypeRefresh && !op.PlanRefresh,
}
run.PlanOpts = planOpts
// For a "direct" local run, the input state is the most recently stored
// snapshot, from the previous run.
run.InputState = s.State()
tfCtx, moreDiags := terraform.NewContext(coreOpts)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, nil, diags
}
run.Core = tfCtx
return run, configSnap, diags
}
func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, run *backend.LocalRun, coreOpts *terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
const errSummary = "Invalid plan file"
// A plan file has a snapshot of configuration embedded inside it, which
// is used instead of whatever configuration might be already present
// in the filesystem.
snap, err := pf.ReadConfigSnapshot()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errSummary,
fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err),
))
return nil, snap, diags
}
loader := configload.NewLoaderFromSnapshot(snap)
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, snap, diags
}
run.Config = config
// NOTE: We're intentionally comparing the current locks with the
// configuration snapshot, rather than the lock snapshot in the plan file,
// because it's the current locks which dictate our plugin selections
// in coreOpts below. However, we'll also separately check that the
// plan file has identical locked plugins below, and thus we're effectively
// checking consistency with both here.
if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
var buf strings.Builder
for _, err := range errs {
fmt.Fprintf(&buf, "\n - %s", err.Error())
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(
"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
buf.String(),
),
))
}
// This check is an important complement to the check above: the locked
// dependencies in the configuration must match the configuration, and
// the locked dependencies in the plan must match the locked dependencies
// in the configuration, and so transitively we ensure that the locked
// dependencies in the plan match the configuration too. However, this
// additionally catches any inconsistency between the two sets of locks
// even if they both happen to be valid per the current configuration,
// which is one of several ways we try to catch the mistake of applying
// a saved plan file in a different place than where we created it.
depLocksFromPlan, moreDiags := pf.ReadDependencyLocks()
diags = diags.Append(moreDiags)
if depLocksFromPlan != nil && !op.DependencyLocks.Equal(depLocksFromPlan) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
"The given plan file was created with a different set of external dependency selections than the current configuration. A saved plan can be applied only to the same configuration it was created from.\n\nCreate a new plan from the updated configuration.",
))
}
// A plan file also contains a snapshot of the prior state the changes
// are intended to apply to.
priorStateFile, err := pf.ReadStateFile()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errSummary,
fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err),
))
return nil, snap, diags
}
if currentStateMeta != nil {
// If the caller sets this, we require that the stored prior state
// has the same metadata, which is an extra safety check that nothing
// has changed since the plan was created. (All of the "real-world"
// state manager implementations support this, but simpler test backends
// may not.)
// Because the plan always contains a state, even if it is empty, the
// first plan to be applied will have empty snapshot metadata. In this
// case we compare only the serial in order to provide a more correct
// error.
firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0
switch {
case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Saved plan does not match the given state",
"The given plan file can not be applied because it was created from a different state lineage.",
))
case priorStateFile.Serial != currentStateMeta.Serial:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Saved plan is stale",
"The given plan file can no longer be applied because the state was changed by another operation after the plan was created.",
))
}
}
// When we're applying a saved plan, the input state is the "prior state"
// recorded in the plan, which incorporates the result of all of the
// refreshing we did while building the plan.
run.InputState = priorStateFile.State
plan, err := pf.ReadPlan()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errSummary,
fmt.Sprintf("Failed to read plan from plan file: %s.", err),
))
return nil, snap, diags
}
// When we're applying a saved plan, we populate Plan instead of PlanOpts,
// because a plan object incorporates the subset of data from PlanOps that
// we need to apply the plan.
run.Plan = plan
tfCtx, moreDiags := terraform.NewContext(coreOpts)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, nil, diags
}
run.Core = tfCtx
return run, snap, diags
}
// interactiveCollectVariables attempts to complete the given existing
// map of variables by interactively prompting for any variables that are
// declared as required but not yet present.
//
// If interactive input is disabled for this backend instance then this is
// a no-op. If input is enabled but fails for some reason, the resulting
// map will be incomplete. For these reasons, the caller must still validate
// that the result is complete and valid.
//
// This function does not modify the map given in "existing", but may return
// it unchanged if no modifications are required. If modifications are required,
// the result is a new map with all of the elements from "existing" plus
// additional elements as appropriate.
//
// Interactive prompting is a "best effort" thing for first-time user UX and
// not something we expect folks to be relying on for routine use. Terraform
// is primarily a non-interactive tool and so we prefer to report in error
// messages that variables are not set rather than reporting that input failed:
// the primary resolution to missing variables is to provide them by some other
// means.
func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue {
var needed []string
if b.OpInput && uiInput != nil {
for name, vc := range vcs {
if !vc.Required() {
continue // We only prompt for required variables
}
if _, exists := existing[name]; !exists {
needed = append(needed, name)
}
}
} else {
log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled")
}
if len(needed) == 0 {
return existing
}
log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed)
// If we get here then we're planning to prompt for at least one additional
// variable's value.
sort.Strings(needed) // prompt in lexical order
ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
for k, v := range existing {
ret[k] = v
}
for _, name := range needed {
vc := vcs[name]
rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{
Id: fmt.Sprintf("var.%s", name),
Query: fmt.Sprintf("var.%s", name),
Description: vc.Description,
})
if err != nil {
// Since interactive prompts are best-effort, we'll just continue
// here and let subsequent validation report this as a variable
// not specified.
log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err)
continue
}
ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue}
}
return ret
}
// stubUnsetVariables ensures that all required variables defined in the
// configuration exist in the resulting map, by adding new elements as necessary.
//
// The stubbed value of any additions will be an unknown variable conforming
// to the variable's configured type constraint, meaning that no particular
// value is known and that one must be provided by the user in order to get
// a complete result.
//
// Unset optional attributes (those with default values) will not be populated
// by this function, under the assumption that a later step will handle those.
// In this sense, stubUnsetRequiredVariables is essentially a non-interactive,
// non-error-producing variant of interactiveCollectVariables that creates
// placeholders for values the user would be prompted for interactively on
// other operations.
//
// This function should be used only in situations where variables values
// will not be directly used and the variables map is being constructed only
// to produce a complete Terraform context for some ancillary functionality
// like "terraform console", "terraform state ...", etc.
//
// This function is guaranteed not to modify the given map, but it may return
// the given map unchanged if no additions are required. If additions are
// required then the result will be a new map containing everything in the
// given map plus additional elements.
func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue {
var missing bool // Do we need to add anything?
for name, vc := range vcs {
if !vc.Required() {
continue // We only stub required variables
}
if _, exists := existing[name]; !exists {
missing = true
}
}
if !missing {
return existing
}
// If we get down here then there's at least one variable value to add.
ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
for k, v := range existing {
ret[k] = v
}
for name, vc := range vcs {
if !vc.Required() {
continue
}
if _, exists := existing[name]; !exists {
ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type}
}
}
return ret
}
type unparsedInteractiveVariableValue struct {
Name, RawValue string
}
var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{}
func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
val, valDiags := mode.Parse(v.Name, v.RawValue)
diags = diags.Append(valDiags)
if diags.HasErrors() {
return nil, diags
}
return &terraform.InputValue{
Value: val,
SourceType: terraform.ValueFromInput,
}, diags
}
type unparsedUnknownVariableValue struct {
Name string
WantType cty.Type
}
var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{}
func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
return &terraform.InputValue{
Value: cty.UnknownVal(v.WantType),
SourceType: terraform.ValueFromInput,
}, nil
}