github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_local.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sort" 8 9 "github.com/hashicorp/errwrap" 10 "github.com/hashicorp/terraform/backend" 11 "github.com/hashicorp/terraform/command/clistate" 12 "github.com/hashicorp/terraform/configs" 13 "github.com/hashicorp/terraform/configs/configload" 14 "github.com/hashicorp/terraform/plans/planfile" 15 "github.com/hashicorp/terraform/states/statemgr" 16 "github.com/hashicorp/terraform/terraform" 17 "github.com/hashicorp/terraform/tfdiags" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 // backend.Local implementation. 22 func (b *Local) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) { 23 // Make sure the type is invalid. We use this as a way to know not 24 // to ask for input/validate. 25 op.Type = backend.OperationTypeInvalid 26 27 if op.LockState { 28 op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.Colorize()) 29 } else { 30 op.StateLocker = clistate.NewNoopLocker() 31 } 32 33 ctx, _, stateMgr, diags := b.context(op) 34 return ctx, stateMgr, diags 35 } 36 37 func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { 38 var diags tfdiags.Diagnostics 39 40 // Get the latest state. 41 log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace) 42 s, err := b.StateMgr(op.Workspace) 43 if err != nil { 44 diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) 45 return nil, nil, nil, diags 46 } 47 log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace) 48 if err := op.StateLocker.Lock(s, op.Type.String()); err != nil { 49 diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) 50 return nil, nil, nil, diags 51 } 52 log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace) 53 if err := s.RefreshState(); err != nil { 54 diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) 55 return nil, nil, nil, diags 56 } 57 58 // Initialize our context options 59 var opts terraform.ContextOpts 60 if v := b.ContextOpts; v != nil { 61 opts = *v 62 } 63 64 // Copy set options from the operation 65 opts.Destroy = op.Destroy 66 opts.Targets = op.Targets 67 opts.UIInput = op.UIIn 68 69 // Load the latest state. If we enter contextFromPlanFile below then the 70 // state snapshot in the plan file must match this, or else it'll return 71 // error diagnostics. 72 log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace) 73 opts.State = s.State() 74 75 var tfCtx *terraform.Context 76 var ctxDiags tfdiags.Diagnostics 77 var configSnap *configload.Snapshot 78 if op.PlanFile != nil { 79 var stateMeta *statemgr.SnapshotMeta 80 // If the statemgr implements our optional PersistentMeta interface then we'll 81 // additionally verify that the state snapshot in the plan file has 82 // consistent metadata, as an additional safety check. 83 if sm, ok := s.(statemgr.PersistentMeta); ok { 84 m := sm.StateSnapshotMeta() 85 stateMeta = &m 86 } 87 log.Printf("[TRACE] backend/local: building context from plan file") 88 tfCtx, configSnap, ctxDiags = b.contextFromPlanFile(op.PlanFile, opts, stateMeta) 89 // Write sources into the cache of the main loader so that they are 90 // available if we need to generate diagnostic message snippets. 91 op.ConfigLoader.ImportSourcesFromSnapshot(configSnap) 92 } else { 93 log.Printf("[TRACE] backend/local: building context for current working directory") 94 tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts) 95 } 96 diags = diags.Append(ctxDiags) 97 if diags.HasErrors() { 98 return nil, nil, nil, diags 99 } 100 log.Printf("[TRACE] backend/local: finished building terraform.Context") 101 102 // If we have an operation, then we automatically do the input/validate 103 // here since every option requires this. 104 if op.Type != backend.OperationTypeInvalid { 105 // If input asking is enabled, then do that 106 if op.PlanFile == nil && b.OpInput { 107 mode := terraform.InputModeProvider 108 109 log.Printf("[TRACE] backend/local: requesting interactive input, if necessary") 110 inputDiags := tfCtx.Input(mode) 111 diags = diags.Append(inputDiags) 112 if inputDiags.HasErrors() { 113 return nil, nil, nil, diags 114 } 115 } 116 117 // If validation is enabled, validate 118 if b.OpValidation { 119 log.Printf("[TRACE] backend/local: running validation operation") 120 validateDiags := tfCtx.Validate() 121 diags = diags.Append(validateDiags) 122 } 123 } 124 125 return tfCtx, configSnap, s, diags 126 } 127 128 func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) { 129 var diags tfdiags.Diagnostics 130 131 // Load the configuration using the caller-provided configuration loader. 132 config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) 133 diags = diags.Append(configDiags) 134 if configDiags.HasErrors() { 135 return nil, nil, diags 136 } 137 opts.Config = config 138 139 var rawVariables map[string]backend.UnparsedVariableValue 140 if op.AllowUnsetVariables { 141 // Rather than prompting for input, we'll just stub out the required 142 // but unset variables with unknown values to represent that they are 143 // placeholders for values the user would need to provide for other 144 // operations. 145 rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables) 146 } else { 147 // If interactive input is enabled, we might gather some more variable 148 // values through interactive prompts. 149 // TODO: Need to route the operation context through into here, so that 150 // the interactive prompts can be sensitive to its timeouts/etc. 151 rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, opts.UIInput) 152 } 153 154 variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables) 155 diags = diags.Append(varDiags) 156 if diags.HasErrors() { 157 return nil, nil, diags 158 } 159 opts.Variables = variables 160 161 tfCtx, ctxDiags := terraform.NewContext(&opts) 162 diags = diags.Append(ctxDiags) 163 return tfCtx, configSnap, diags 164 } 165 166 func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) { 167 var diags tfdiags.Diagnostics 168 169 const errSummary = "Invalid plan file" 170 171 // A plan file has a snapshot of configuration embedded inside it, which 172 // is used instead of whatever configuration might be already present 173 // in the filesystem. 174 snap, err := pf.ReadConfigSnapshot() 175 if err != nil { 176 diags = diags.Append(tfdiags.Sourceless( 177 tfdiags.Error, 178 errSummary, 179 fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err), 180 )) 181 return nil, snap, diags 182 } 183 loader := configload.NewLoaderFromSnapshot(snap) 184 config, configDiags := loader.LoadConfig(snap.Modules[""].Dir) 185 diags = diags.Append(configDiags) 186 if configDiags.HasErrors() { 187 return nil, snap, diags 188 } 189 opts.Config = config 190 191 // A plan file also contains a snapshot of the prior state the changes 192 // are intended to apply to. 193 priorStateFile, err := pf.ReadStateFile() 194 if err != nil { 195 diags = diags.Append(tfdiags.Sourceless( 196 tfdiags.Error, 197 errSummary, 198 fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err), 199 )) 200 return nil, snap, diags 201 } 202 if currentStateMeta != nil { 203 // If the caller sets this, we require that the stored prior state 204 // has the same metadata, which is an extra safety check that nothing 205 // has changed since the plan was created. (All of the "real-world" 206 // state manager implementstions support this, but simpler test backends 207 // may not.) 208 if currentStateMeta.Lineage != "" && priorStateFile.Lineage != "" { 209 if priorStateFile.Serial != currentStateMeta.Serial || priorStateFile.Lineage != currentStateMeta.Lineage { 210 diags = diags.Append(tfdiags.Sourceless( 211 tfdiags.Error, 212 "Saved plan is stale", 213 "The given plan file can no longer be applied because the state was changed by another operation after the plan was created.", 214 )) 215 } 216 } 217 } 218 // The caller already wrote the "current state" here, but we're overriding 219 // it here with the prior state. These two should actually be identical in 220 // normal use, particularly if we validated the state meta above, but 221 // we do this here anyway to ensure consistent behavior. 222 opts.State = priorStateFile.State 223 224 plan, err := pf.ReadPlan() 225 if err != nil { 226 diags = diags.Append(tfdiags.Sourceless( 227 tfdiags.Error, 228 errSummary, 229 fmt.Sprintf("Failed to read plan from plan file: %s.", err), 230 )) 231 return nil, snap, diags 232 } 233 234 variables := terraform.InputValues{} 235 for name, dyVal := range plan.VariableValues { 236 val, err := dyVal.Decode(cty.DynamicPseudoType) 237 if err != nil { 238 diags = diags.Append(tfdiags.Sourceless( 239 tfdiags.Error, 240 errSummary, 241 fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err), 242 )) 243 continue 244 } 245 246 variables[name] = &terraform.InputValue{ 247 Value: val, 248 SourceType: terraform.ValueFromPlan, 249 } 250 } 251 opts.Variables = variables 252 opts.Changes = plan.Changes 253 opts.Targets = plan.TargetAddrs 254 opts.ProviderSHA256s = plan.ProviderSHA256s 255 256 tfCtx, ctxDiags := terraform.NewContext(&opts) 257 diags = diags.Append(ctxDiags) 258 return tfCtx, snap, diags 259 } 260 261 // interactiveCollectVariables attempts to complete the given existing 262 // map of variables by interactively prompting for any variables that are 263 // declared as required but not yet present. 264 // 265 // If interactive input is disabled for this backend instance then this is 266 // a no-op. If input is enabled but fails for some reason, the resulting 267 // map will be incomplete. For these reasons, the caller must still validate 268 // that the result is complete and valid. 269 // 270 // This function does not modify the map given in "existing", but may return 271 // it unchanged if no modifications are required. If modifications are required, 272 // the result is a new map with all of the elements from "existing" plus 273 // additional elements as appropriate. 274 // 275 // Interactive prompting is a "best effort" thing for first-time user UX and 276 // not something we expect folks to be relying on for routine use. Terraform 277 // is primarily a non-interactive tool and so we prefer to report in error 278 // messages that variables are not set rather than reporting that input failed: 279 // the primary resolution to missing variables is to provide them by some other 280 // means. 281 func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { 282 var needed []string 283 if b.OpInput && uiInput != nil { 284 for name, vc := range vcs { 285 if !vc.Required() { 286 continue // We only prompt for required variables 287 } 288 if _, exists := existing[name]; !exists { 289 needed = append(needed, name) 290 } 291 } 292 } else { 293 log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled") 294 } 295 if len(needed) == 0 { 296 return existing 297 } 298 299 log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed) 300 301 // If we get here then we're planning to prompt for at least one additional 302 // variable's value. 303 sort.Strings(needed) // prompt in lexical order 304 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 305 for k, v := range existing { 306 ret[k] = v 307 } 308 for _, name := range needed { 309 vc := vcs[name] 310 rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ 311 Id: fmt.Sprintf("var.%s", name), 312 Query: fmt.Sprintf("var.%s", name), 313 Description: vc.Description, 314 }) 315 if err != nil { 316 // Since interactive prompts are best-effort, we'll just continue 317 // here and let subsequent validation report this as a variable 318 // not specified. 319 log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err) 320 continue 321 } 322 ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue} 323 } 324 return ret 325 } 326 327 // stubUnsetVariables ensures that all required variables defined in the 328 // configuration exist in the resulting map, by adding new elements as necessary. 329 // 330 // The stubbed value of any additions will be an unknown variable conforming 331 // to the variable's configured type constraint, meaning that no particular 332 // value is known and that one must be provided by the user in order to get 333 // a complete result. 334 // 335 // Unset optional attributes (those with default values) will not be populated 336 // by this function, under the assumption that a later step will handle those. 337 // In this sense, stubUnsetRequiredVariables is essentially a non-interactive, 338 // non-error-producing variant of interactiveCollectVariables that creates 339 // placeholders for values the user would be prompted for interactively on 340 // other operations. 341 // 342 // This function should be used only in situations where variables values 343 // will not be directly used and the variables map is being constructed only 344 // to produce a complete Terraform context for some ancillary functionality 345 // like "terraform console", "terraform state ...", etc. 346 // 347 // This function is guaranteed not to modify the given map, but it may return 348 // the given map unchanged if no additions are required. If additions are 349 // required then the result will be a new map containing everything in the 350 // given map plus additional elements. 351 func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue { 352 var missing bool // Do we need to add anything? 353 for name, vc := range vcs { 354 if !vc.Required() { 355 continue // We only stub required variables 356 } 357 if _, exists := existing[name]; !exists { 358 missing = true 359 } 360 } 361 if !missing { 362 return existing 363 } 364 365 // If we get down here then there's at least one variable value to add. 366 ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) 367 for k, v := range existing { 368 ret[k] = v 369 } 370 for name, vc := range vcs { 371 if !vc.Required() { 372 continue 373 } 374 if _, exists := existing[name]; !exists { 375 ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type} 376 } 377 } 378 return ret 379 } 380 381 type unparsedInteractiveVariableValue struct { 382 Name, RawValue string 383 } 384 385 var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} 386 387 func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 388 var diags tfdiags.Diagnostics 389 val, valDiags := mode.Parse(v.Name, v.RawValue) 390 diags = diags.Append(valDiags) 391 if diags.HasErrors() { 392 return nil, diags 393 } 394 return &terraform.InputValue{ 395 Value: val, 396 SourceType: terraform.ValueFromInput, 397 }, diags 398 } 399 400 type unparsedUnknownVariableValue struct { 401 Name string 402 WantType cty.Type 403 } 404 405 var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{} 406 407 func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 408 return &terraform.InputValue{ 409 Value: cty.UnknownVal(v.WantType), 410 SourceType: terraform.ValueFromInput, 411 }, nil 412 }