github.com/hugorut/terraform@v1.1.3/src/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/hugorut/terraform/src/backend" 11 "github.com/hugorut/terraform/src/configs" 12 "github.com/hugorut/terraform/src/configs/configload" 13 "github.com/hugorut/terraform/src/plans/planfile" 14 "github.com/hugorut/terraform/src/states/statemgr" 15 "github.com/hugorut/terraform/src/terraform" 16 "github.com/hugorut/terraform/src/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 if currentStateMeta != nil { 288 // If the caller sets this, we require that the stored prior state 289 // has the same metadata, which is an extra safety check that nothing 290 // has changed since the plan was created. (All of the "real-world" 291 // state manager implementations support this, but simpler test backends 292 // may not.) 293 294 // Because the plan always contains a state, even if it is empty, the 295 // first plan to be applied will have empty snapshot metadata. In this 296 // case we compare only the serial in order to provide a more correct 297 // error. 298 firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0 299 300 switch { 301 case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage: 302 diags = diags.Append(tfdiags.Sourceless( 303 tfdiags.Error, 304 "Saved plan does not match the given state", 305 "The given plan file can not be applied because it was created from a different state lineage.", 306 )) 307 308 case priorStateFile.Serial != currentStateMeta.Serial: 309 diags = diags.Append(tfdiags.Sourceless( 310 tfdiags.Error, 311 "Saved plan is stale", 312 "The given plan file can no longer be applied because the state was changed by another operation after the plan was created.", 313 )) 314 } 315 } 316 // When we're applying a saved plan, the input state is the "prior state" 317 // recorded in the plan, which incorporates the result of all of the 318 // refreshing we did while building the plan. 319 run.InputState = priorStateFile.State 320 321 plan, err := pf.ReadPlan() 322 if err != nil { 323 diags = diags.Append(tfdiags.Sourceless( 324 tfdiags.Error, 325 errSummary, 326 fmt.Sprintf("Failed to read plan from plan file: %s.", err), 327 )) 328 return nil, snap, diags 329 } 330 // When we're applying a saved plan, we populate Plan instead of PlanOpts, 331 // because a plan object incorporates the subset of data from PlanOps that 332 // we need to apply the plan. 333 run.Plan = plan 334 335 tfCtx, moreDiags := terraform.NewContext(coreOpts) 336 diags = diags.Append(moreDiags) 337 if moreDiags.HasErrors() { 338 return nil, nil, diags 339 } 340 run.Core = tfCtx 341 return run, snap, diags 342 } 343 344 // interactiveCollectVariables attempts to complete the given existing 345 // map of variables by interactively prompting for any variables that are 346 // declared as required but not yet present. 347 // 348 // If interactive input is disabled for this backend instance then this is 349 // a no-op. If input is enabled but fails for some reason, the resulting 350 // map will be incomplete. For these reasons, the caller must still validate 351 // that the result is complete and valid. 352 // 353 // This function does not modify the map given in "existing", but may return 354 // it unchanged if no modifications are required. If modifications are required, 355 // the result is a new map with all of the elements from "existing" plus 356 // additional elements as appropriate. 357 // 358 // Interactive prompting is a "best effort" thing for first-time user UX and 359 // not something we expect folks to be relying on for routine use. Terraform 360 // is primarily a non-interactive tool and so we prefer to report in error 361 // messages that variables are not set rather than reporting that input failed: 362 // the primary resolution to missing variables is to provide them by some other 363 // means. 364 func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { 365 var needed []string 366 if b.OpInput && uiInput != nil { 367 for name, vc := range vcs { 368 if !vc.Required() { 369 continue // We only prompt for required variables 370 } 371 if _, exists := existing[name]; !exists { 372 needed = append(needed, name) 373 } 374 } 375 } else { 376 log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled") 377 } 378 if len(needed) == 0 { 379 return existing 380 } 381 382 log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed) 383 384 // If we get here then we're planning to prompt for at least one additional 385 // variable's value. 386 sort.Strings(needed) // prompt in lexical order 387 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 388 for k, v := range existing { 389 ret[k] = v 390 } 391 for _, name := range needed { 392 vc := vcs[name] 393 rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ 394 Id: fmt.Sprintf("var.%s", name), 395 Query: fmt.Sprintf("var.%s", name), 396 Description: vc.Description, 397 }) 398 if err != nil { 399 // Since interactive prompts are best-effort, we'll just continue 400 // here and let subsequent validation report this as a variable 401 // not specified. 402 log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err) 403 continue 404 } 405 ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue} 406 } 407 return ret 408 } 409 410 // stubUnsetVariables ensures that all required variables defined in the 411 // configuration exist in the resulting map, by adding new elements as necessary. 412 // 413 // The stubbed value of any additions will be an unknown variable conforming 414 // to the variable's configured type constraint, meaning that no particular 415 // value is known and that one must be provided by the user in order to get 416 // a complete result. 417 // 418 // Unset optional attributes (those with default values) will not be populated 419 // by this function, under the assumption that a later step will handle those. 420 // In this sense, stubUnsetRequiredVariables is essentially a non-interactive, 421 // non-error-producing variant of interactiveCollectVariables that creates 422 // placeholders for values the user would be prompted for interactively on 423 // other operations. 424 // 425 // This function should be used only in situations where variables values 426 // will not be directly used and the variables map is being constructed only 427 // to produce a complete Terraform context for some ancillary functionality 428 // like "terraform console", "terraform state ...", etc. 429 // 430 // This function is guaranteed not to modify the given map, but it may return 431 // the given map unchanged if no additions are required. If additions are 432 // required then the result will be a new map containing everything in the 433 // given map plus additional elements. 434 func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue { 435 var missing bool // Do we need to add anything? 436 for name, vc := range vcs { 437 if !vc.Required() { 438 continue // We only stub required variables 439 } 440 if _, exists := existing[name]; !exists { 441 missing = true 442 } 443 } 444 if !missing { 445 return existing 446 } 447 448 // If we get down here then there's at least one variable value to add. 449 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 450 for k, v := range existing { 451 ret[k] = v 452 } 453 for name, vc := range vcs { 454 if !vc.Required() { 455 continue 456 } 457 if _, exists := existing[name]; !exists { 458 ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type} 459 } 460 } 461 return ret 462 } 463 464 type unparsedInteractiveVariableValue struct { 465 Name, RawValue string 466 } 467 468 var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} 469 470 func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 471 var diags tfdiags.Diagnostics 472 val, valDiags := mode.Parse(v.Name, v.RawValue) 473 diags = diags.Append(valDiags) 474 if diags.HasErrors() { 475 return nil, diags 476 } 477 return &terraform.InputValue{ 478 Value: val, 479 SourceType: terraform.ValueFromInput, 480 }, diags 481 } 482 483 type unparsedUnknownVariableValue struct { 484 Name string 485 WantType cty.Type 486 } 487 488 var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{} 489 490 func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 491 return &terraform.InputValue{ 492 Value: cty.UnknownVal(v.WantType), 493 SourceType: terraform.ValueFromInput, 494 }, nil 495 }