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