github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/local/backend_local.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sort" 8 "strings" 9 10 "github.com/hashicorp/terraform/internal/backend" 11 "github.com/hashicorp/terraform/internal/configs" 12 "github.com/hashicorp/terraform/internal/configs/configload" 13 "github.com/hashicorp/terraform/internal/plans/planfile" 14 "github.com/hashicorp/terraform/internal/states/statemgr" 15 "github.com/hashicorp/terraform/internal/terraform" 16 "github.com/hashicorp/terraform/internal/tfdiags" 17 "github.com/zclconf/go-cty/cty" 18 ) 19 20 // backend.Local implementation. 21 func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) { 22 // Make sure the type is invalid. We use this as a way to know not 23 // to ask for input/validate. We're modifying this through a pointer, 24 // so we're mutating an object that belongs to the caller here, which 25 // seems bad but we're preserving it for now until we have time to 26 // properly design this API, vs. just preserving whatever it currently 27 // happens to do. 28 op.Type = backend.OperationTypeInvalid 29 30 op.StateLocker = op.StateLocker.WithContext(context.Background()) 31 32 lr, _, stateMgr, diags := b.localRun(op) 33 return lr, stateMgr, diags 34 } 35 36 func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { 37 var diags tfdiags.Diagnostics 38 39 // Get the latest state. 40 log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace) 41 s, err := b.StateMgr(op.Workspace) 42 if err != nil { 43 diags = diags.Append(fmt.Errorf("error loading state: %w", err)) 44 return nil, nil, nil, diags 45 } 46 log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace) 47 if diags := op.StateLocker.Lock(s, op.Type.String()); diags.HasErrors() { 48 return nil, nil, nil, diags 49 } 50 51 defer func() { 52 // If we're returning with errors, and thus not producing a valid 53 // context, we'll want to avoid leaving the workspace locked. 54 if diags.HasErrors() { 55 diags = diags.Append(op.StateLocker.Unlock()) 56 } 57 }() 58 59 log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace) 60 if err := s.RefreshState(); err != nil { 61 diags = diags.Append(fmt.Errorf("error loading state: %w", err)) 62 return nil, nil, nil, diags 63 } 64 65 ret := &backend.LocalRun{} 66 67 // Initialize our context options 68 var coreOpts terraform.ContextOpts 69 if v := b.ContextOpts; v != nil { 70 coreOpts = *v 71 } 72 coreOpts.UIInput = op.UIIn 73 coreOpts.Hooks = op.Hooks 74 75 var ctxDiags tfdiags.Diagnostics 76 var configSnap *configload.Snapshot 77 if op.PlanFile != nil { 78 var stateMeta *statemgr.SnapshotMeta 79 // If the statemgr implements our optional PersistentMeta interface then we'll 80 // additionally verify that the state snapshot in the plan file has 81 // consistent metadata, as an additional safety check. 82 if sm, ok := s.(statemgr.PersistentMeta); ok { 83 m := sm.StateSnapshotMeta() 84 stateMeta = &m 85 } 86 log.Printf("[TRACE] backend/local: populating backend.LocalRun from plan file") 87 ret, configSnap, ctxDiags = b.localRunForPlanFile(op, op.PlanFile, ret, &coreOpts, stateMeta) 88 if ctxDiags.HasErrors() { 89 diags = diags.Append(ctxDiags) 90 return nil, nil, nil, diags 91 } 92 93 // Write sources into the cache of the main loader so that they are 94 // available if we need to generate diagnostic message snippets. 95 op.ConfigLoader.ImportSourcesFromSnapshot(configSnap) 96 } else { 97 log.Printf("[TRACE] backend/local: populating backend.LocalRun for current working directory") 98 ret, configSnap, ctxDiags = b.localRunDirect(op, ret, &coreOpts, s) 99 } 100 diags = diags.Append(ctxDiags) 101 if diags.HasErrors() { 102 return nil, nil, nil, diags 103 } 104 105 // If we have an operation, then we automatically do the input/validate 106 // here since every option requires this. 107 if op.Type != backend.OperationTypeInvalid { 108 // If input asking is enabled, then do that 109 if op.PlanFile == nil && b.OpInput { 110 mode := terraform.InputModeProvider 111 112 log.Printf("[TRACE] backend/local: requesting interactive input, if necessary") 113 inputDiags := ret.Core.Input(ret.Config, mode) 114 diags = diags.Append(inputDiags) 115 if inputDiags.HasErrors() { 116 return nil, nil, nil, diags 117 } 118 } 119 120 // If validation is enabled, validate 121 if b.OpValidation { 122 log.Printf("[TRACE] backend/local: running validation operation") 123 validateDiags := ret.Core.Validate(ret.Config) 124 diags = diags.Append(validateDiags) 125 } 126 } 127 128 return ret, configSnap, s, diags 129 } 130 131 func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { 132 var diags tfdiags.Diagnostics 133 134 // Load the configuration using the caller-provided configuration loader. 135 config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) 136 diags = diags.Append(configDiags) 137 if configDiags.HasErrors() { 138 return nil, nil, diags 139 } 140 run.Config = config 141 142 if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 { 143 var buf strings.Builder 144 for _, err := range errs { 145 fmt.Fprintf(&buf, "\n - %s", err.Error()) 146 } 147 var suggestion string 148 switch { 149 case op.DependencyLocks == nil: 150 // If we get here then it suggests that there's a caller that we 151 // didn't yet update to populate DependencyLocks, which is a bug. 152 suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!" 153 case op.DependencyLocks.Empty(): 154 suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init" 155 default: 156 suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade" 157 } 158 diags = diags.Append(tfdiags.Sourceless( 159 tfdiags.Error, 160 "Inconsistent dependency lock file", 161 fmt.Sprintf( 162 "The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s", 163 buf.String(), suggestion, 164 ), 165 )) 166 } 167 168 var rawVariables map[string]backend.UnparsedVariableValue 169 if op.AllowUnsetVariables { 170 // Rather than prompting for input, we'll just stub out the required 171 // but unset variables with unknown values to represent that they are 172 // placeholders for values the user would need to provide for other 173 // operations. 174 rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables) 175 } else { 176 // If interactive input is enabled, we might gather some more variable 177 // values through interactive prompts. 178 // TODO: Need to route the operation context through into here, so that 179 // the interactive prompts can be sensitive to its timeouts/etc. 180 rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn) 181 } 182 183 variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables) 184 diags = diags.Append(varDiags) 185 if diags.HasErrors() { 186 return nil, nil, diags 187 } 188 189 planOpts := &terraform.PlanOpts{ 190 Mode: op.PlanMode, 191 Targets: op.Targets, 192 ForceReplace: op.ForceReplace, 193 SetVariables: variables, 194 SkipRefresh: op.Type != backend.OperationTypeRefresh && !op.PlanRefresh, 195 } 196 run.PlanOpts = planOpts 197 198 // For a "direct" local run, the input state is the most recently stored 199 // snapshot, from the previous run. 200 run.InputState = s.State() 201 202 tfCtx, moreDiags := terraform.NewContext(coreOpts) 203 diags = diags.Append(moreDiags) 204 if moreDiags.HasErrors() { 205 return nil, nil, diags 206 } 207 run.Core = tfCtx 208 return run, configSnap, diags 209 } 210 211 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) { 212 var diags tfdiags.Diagnostics 213 214 const errSummary = "Invalid plan file" 215 216 // A plan file has a snapshot of configuration embedded inside it, which 217 // is used instead of whatever configuration might be already present 218 // in the filesystem. 219 snap, err := pf.ReadConfigSnapshot() 220 if err != nil { 221 diags = diags.Append(tfdiags.Sourceless( 222 tfdiags.Error, 223 errSummary, 224 fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err), 225 )) 226 return nil, snap, diags 227 } 228 loader := configload.NewLoaderFromSnapshot(snap) 229 config, configDiags := loader.LoadConfig(snap.Modules[""].Dir) 230 diags = diags.Append(configDiags) 231 if configDiags.HasErrors() { 232 return nil, snap, diags 233 } 234 run.Config = config 235 236 // NOTE: We're intentionally comparing the current locks with the 237 // configuration snapshot, rather than the lock snapshot in the plan file, 238 // because it's the current locks which dictate our plugin selections 239 // in coreOpts below. However, we'll also separately check that the 240 // plan file has identical locked plugins below, and thus we're effectively 241 // checking consistency with both here. 242 if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 { 243 var buf strings.Builder 244 for _, err := range errs { 245 fmt.Fprintf(&buf, "\n - %s", err.Error()) 246 } 247 diags = diags.Append(tfdiags.Sourceless( 248 tfdiags.Error, 249 "Inconsistent dependency lock file", 250 fmt.Sprintf( 251 "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.", 252 buf.String(), 253 ), 254 )) 255 } 256 257 // This check is an important complement to the check above: the locked 258 // dependencies in the configuration must match the configuration, and 259 // the locked dependencies in the plan must match the locked dependencies 260 // in the configuration, and so transitively we ensure that the locked 261 // dependencies in the plan match the configuration too. However, this 262 // additionally catches any inconsistency between the two sets of locks 263 // even if they both happen to be valid per the current configuration, 264 // which is one of several ways we try to catch the mistake of applying 265 // a saved plan file in a different place than where we created it. 266 depLocksFromPlan, moreDiags := pf.ReadDependencyLocks() 267 diags = diags.Append(moreDiags) 268 if depLocksFromPlan != nil && !op.DependencyLocks.Equal(depLocksFromPlan) { 269 diags = diags.Append(tfdiags.Sourceless( 270 tfdiags.Error, 271 "Inconsistent dependency lock file", 272 "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.", 273 )) 274 } 275 276 // A plan file also contains a snapshot of the prior state the changes 277 // are intended to apply to. 278 priorStateFile, err := pf.ReadStateFile() 279 if err != nil { 280 diags = diags.Append(tfdiags.Sourceless( 281 tfdiags.Error, 282 errSummary, 283 fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err), 284 )) 285 return nil, snap, diags 286 } 287 288 if currentStateMeta != nil { 289 // If the caller sets this, we require that the stored prior state 290 // has the same metadata, which is an extra safety check that nothing 291 // has changed since the plan was created. (All of the "real-world" 292 // state manager implementations support this, but simpler test backends 293 // may not.) 294 295 // Because the plan always contains a state, even if it is empty, the 296 // first plan to be applied will have empty snapshot metadata. In this 297 // case we compare only the serial in order to provide a more correct 298 // error. 299 firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0 300 301 switch { 302 case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage: 303 diags = diags.Append(tfdiags.Sourceless( 304 tfdiags.Error, 305 "Saved plan does not match the given state", 306 "The given plan file can not be applied because it was created from a different state lineage.", 307 )) 308 309 case priorStateFile.Serial != currentStateMeta.Serial: 310 diags = diags.Append(tfdiags.Sourceless( 311 tfdiags.Error, 312 "Saved plan is stale", 313 "The given plan file can no longer be applied because the state was changed by another operation after the plan was created.", 314 )) 315 } 316 } 317 // When we're applying a saved plan, the input state is the "prior state" 318 // recorded in the plan, which incorporates the result of all of the 319 // refreshing we did while building the plan. 320 run.InputState = priorStateFile.State 321 322 plan, err := pf.ReadPlan() 323 if err != nil { 324 diags = diags.Append(tfdiags.Sourceless( 325 tfdiags.Error, 326 errSummary, 327 fmt.Sprintf("Failed to read plan from plan file: %s.", err), 328 )) 329 return nil, snap, diags 330 } 331 // When we're applying a saved plan, we populate Plan instead of PlanOpts, 332 // because a plan object incorporates the subset of data from PlanOps that 333 // we need to apply the plan. 334 run.Plan = plan 335 336 tfCtx, moreDiags := terraform.NewContext(coreOpts) 337 diags = diags.Append(moreDiags) 338 if moreDiags.HasErrors() { 339 return nil, nil, diags 340 } 341 run.Core = tfCtx 342 return run, snap, diags 343 } 344 345 // interactiveCollectVariables attempts to complete the given existing 346 // map of variables by interactively prompting for any variables that are 347 // declared as required but not yet present. 348 // 349 // If interactive input is disabled for this backend instance then this is 350 // a no-op. If input is enabled but fails for some reason, the resulting 351 // map will be incomplete. For these reasons, the caller must still validate 352 // that the result is complete and valid. 353 // 354 // This function does not modify the map given in "existing", but may return 355 // it unchanged if no modifications are required. If modifications are required, 356 // the result is a new map with all of the elements from "existing" plus 357 // additional elements as appropriate. 358 // 359 // Interactive prompting is a "best effort" thing for first-time user UX and 360 // not something we expect folks to be relying on for routine use. Terraform 361 // is primarily a non-interactive tool and so we prefer to report in error 362 // messages that variables are not set rather than reporting that input failed: 363 // the primary resolution to missing variables is to provide them by some other 364 // means. 365 func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { 366 var needed []string 367 if b.OpInput && uiInput != nil { 368 for name, vc := range vcs { 369 if !vc.Required() { 370 continue // We only prompt for required variables 371 } 372 if _, exists := existing[name]; !exists { 373 needed = append(needed, name) 374 } 375 } 376 } else { 377 log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled") 378 } 379 if len(needed) == 0 { 380 return existing 381 } 382 383 log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed) 384 385 // If we get here then we're planning to prompt for at least one additional 386 // variable's value. 387 sort.Strings(needed) // prompt in lexical order 388 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 389 for k, v := range existing { 390 ret[k] = v 391 } 392 for _, name := range needed { 393 vc := vcs[name] 394 rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ 395 Id: fmt.Sprintf("var.%s", name), 396 Query: fmt.Sprintf("var.%s", name), 397 Description: vc.Description, 398 Secret: vc.Sensitive, 399 }) 400 if err != nil { 401 // Since interactive prompts are best-effort, we'll just continue 402 // here and let subsequent validation report this as a variable 403 // not specified. 404 log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err) 405 continue 406 } 407 ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue} 408 } 409 return ret 410 } 411 412 // stubUnsetVariables ensures that all required variables defined in the 413 // configuration exist in the resulting map, by adding new elements as necessary. 414 // 415 // The stubbed value of any additions will be an unknown variable conforming 416 // to the variable's configured type constraint, meaning that no particular 417 // value is known and that one must be provided by the user in order to get 418 // a complete result. 419 // 420 // Unset optional attributes (those with default values) will not be populated 421 // by this function, under the assumption that a later step will handle those. 422 // In this sense, stubUnsetRequiredVariables is essentially a non-interactive, 423 // non-error-producing variant of interactiveCollectVariables that creates 424 // placeholders for values the user would be prompted for interactively on 425 // other operations. 426 // 427 // This function should be used only in situations where variables values 428 // will not be directly used and the variables map is being constructed only 429 // to produce a complete Terraform context for some ancillary functionality 430 // like "terraform console", "terraform state ...", etc. 431 // 432 // This function is guaranteed not to modify the given map, but it may return 433 // the given map unchanged if no additions are required. If additions are 434 // required then the result will be a new map containing everything in the 435 // given map plus additional elements. 436 func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue { 437 var missing bool // Do we need to add anything? 438 for name, vc := range vcs { 439 if !vc.Required() { 440 continue // We only stub required variables 441 } 442 if _, exists := existing[name]; !exists { 443 missing = true 444 } 445 } 446 if !missing { 447 return existing 448 } 449 450 // If we get down here then there's at least one variable value to add. 451 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 452 for k, v := range existing { 453 ret[k] = v 454 } 455 for name, vc := range vcs { 456 if !vc.Required() { 457 continue 458 } 459 if _, exists := existing[name]; !exists { 460 ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type} 461 } 462 } 463 return ret 464 } 465 466 type unparsedInteractiveVariableValue struct { 467 Name, RawValue string 468 } 469 470 var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} 471 472 func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 473 var diags tfdiags.Diagnostics 474 val, valDiags := mode.Parse(v.Name, v.RawValue) 475 diags = diags.Append(valDiags) 476 if diags.HasErrors() { 477 return nil, diags 478 } 479 return &terraform.InputValue{ 480 Value: val, 481 SourceType: terraform.ValueFromInput, 482 }, diags 483 } 484 485 type unparsedUnknownVariableValue struct { 486 Name string 487 WantType cty.Type 488 } 489 490 var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{} 491 492 func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 493 return &terraform.InputValue{ 494 Value: cty.UnknownVal(v.WantType), 495 SourceType: terraform.ValueFromInput, 496 }, nil 497 }