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