| package local |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "sort" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/configs/configload" |
| "github.com/hashicorp/terraform/internal/plans/planfile" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // backend.Local implementation. |
| func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) { |
| // Make sure the type is invalid. We use this as a way to know not |
| // to ask for input/validate. We're modifying this through a pointer, |
| // so we're mutating an object that belongs to the caller here, which |
| // seems bad but we're preserving it for now until we have time to |
| // properly design this API, vs. just preserving whatever it currently |
| // happens to do. |
| op.Type = backend.OperationTypeInvalid |
| |
| op.StateLocker = op.StateLocker.WithContext(context.Background()) |
| |
| lr, _, stateMgr, diags := b.localRun(op) |
| return lr, stateMgr, diags |
| } |
| |
| func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // Get the latest state. |
| log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace) |
| s, err := b.StateMgr(op.Workspace) |
| if err != nil { |
| diags = diags.Append(fmt.Errorf("error loading state: %w", err)) |
| return nil, nil, nil, diags |
| } |
| log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace) |
| if diags := op.StateLocker.Lock(s, op.Type.String()); diags.HasErrors() { |
| return nil, nil, nil, diags |
| } |
| |
| defer func() { |
| // If we're returning with errors, and thus not producing a valid |
| // context, we'll want to avoid leaving the workspace locked. |
| if diags.HasErrors() { |
| diags = diags.Append(op.StateLocker.Unlock()) |
| } |
| }() |
| |
| log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace) |
| if err := s.RefreshState(); err != nil { |
| diags = diags.Append(fmt.Errorf("error loading state: %w", err)) |
| return nil, nil, nil, diags |
| } |
| |
| ret := &backend.LocalRun{} |
| |
| // Initialize our context options |
| var coreOpts terraform.ContextOpts |
| if v := b.ContextOpts; v != nil { |
| coreOpts = *v |
| } |
| coreOpts.UIInput = op.UIIn |
| coreOpts.Hooks = op.Hooks |
| |
| var ctxDiags tfdiags.Diagnostics |
| var configSnap *configload.Snapshot |
| if op.PlanFile != nil { |
| var stateMeta *statemgr.SnapshotMeta |
| // If the statemgr implements our optional PersistentMeta interface then we'll |
| // additionally verify that the state snapshot in the plan file has |
| // consistent metadata, as an additional safety check. |
| if sm, ok := s.(statemgr.PersistentMeta); ok { |
| m := sm.StateSnapshotMeta() |
| stateMeta = &m |
| } |
| log.Printf("[TRACE] backend/local: populating backend.LocalRun from plan file") |
| ret, configSnap, ctxDiags = b.localRunForPlanFile(op, op.PlanFile, ret, &coreOpts, stateMeta) |
| if ctxDiags.HasErrors() { |
| diags = diags.Append(ctxDiags) |
| return nil, nil, nil, diags |
| } |
| |
| // Write sources into the cache of the main loader so that they are |
| // available if we need to generate diagnostic message snippets. |
| op.ConfigLoader.ImportSourcesFromSnapshot(configSnap) |
| } else { |
| log.Printf("[TRACE] backend/local: populating backend.LocalRun for current working directory") |
| ret, configSnap, ctxDiags = b.localRunDirect(op, ret, &coreOpts, s) |
| } |
| diags = diags.Append(ctxDiags) |
| if diags.HasErrors() { |
| return nil, nil, nil, diags |
| } |
| |
| // If we have an operation, then we automatically do the input/validate |
| // here since every option requires this. |
| if op.Type != backend.OperationTypeInvalid { |
| // If input asking is enabled, then do that |
| if op.PlanFile == nil && b.OpInput { |
| mode := terraform.InputModeProvider |
| |
| log.Printf("[TRACE] backend/local: requesting interactive input, if necessary") |
| inputDiags := ret.Core.Input(ret.Config, mode) |
| diags = diags.Append(inputDiags) |
| if inputDiags.HasErrors() { |
| return nil, nil, nil, diags |
| } |
| } |
| |
| // If validation is enabled, validate |
| if b.OpValidation { |
| log.Printf("[TRACE] backend/local: running validation operation") |
| validateDiags := ret.Core.Validate(ret.Config) |
| diags = diags.Append(validateDiags) |
| } |
| } |
| |
| return ret, configSnap, s, diags |
| } |
| |
| func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // Load the configuration using the caller-provided configuration loader. |
| config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) |
| diags = diags.Append(configDiags) |
| if configDiags.HasErrors() { |
| return nil, nil, diags |
| } |
| run.Config = config |
| |
| if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 { |
| var buf strings.Builder |
| for _, err := range errs { |
| fmt.Fprintf(&buf, "\n - %s", err.Error()) |
| } |
| var suggestion string |
| switch { |
| case op.DependencyLocks == nil: |
| // If we get here then it suggests that there's a caller that we |
| // didn't yet update to populate DependencyLocks, which is a bug. |
| suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!" |
| case op.DependencyLocks.Empty(): |
| suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init" |
| default: |
| suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade" |
| } |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Inconsistent dependency lock file", |
| fmt.Sprintf( |
| "The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s", |
| buf.String(), suggestion, |
| ), |
| )) |
| } |
| |
| var rawVariables map[string]backend.UnparsedVariableValue |
| if op.AllowUnsetVariables { |
| // Rather than prompting for input, we'll just stub out the required |
| // but unset variables with unknown values to represent that they are |
| // placeholders for values the user would need to provide for other |
| // operations. |
| rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables) |
| } else { |
| // If interactive input is enabled, we might gather some more variable |
| // values through interactive prompts. |
| // TODO: Need to route the operation context through into here, so that |
| // the interactive prompts can be sensitive to its timeouts/etc. |
| rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn) |
| } |
| |
| variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables) |
| diags = diags.Append(varDiags) |
| if diags.HasErrors() { |
| return nil, nil, diags |
| } |
| |
| planOpts := &terraform.PlanOpts{ |
| Mode: op.PlanMode, |
| Targets: op.Targets, |
| ForceReplace: op.ForceReplace, |
| SetVariables: variables, |
| SkipRefresh: op.Type != backend.OperationTypeRefresh && !op.PlanRefresh, |
| } |
| run.PlanOpts = planOpts |
| |
| // For a "direct" local run, the input state is the most recently stored |
| // snapshot, from the previous run. |
| run.InputState = s.State() |
| |
| tfCtx, moreDiags := terraform.NewContext(coreOpts) |
| diags = diags.Append(moreDiags) |
| if moreDiags.HasErrors() { |
| return nil, nil, diags |
| } |
| run.Core = tfCtx |
| return run, configSnap, diags |
| } |
| |
| 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) { |
| var diags tfdiags.Diagnostics |
| |
| const errSummary = "Invalid plan file" |
| |
| // A plan file has a snapshot of configuration embedded inside it, which |
| // is used instead of whatever configuration might be already present |
| // in the filesystem. |
| snap, err := pf.ReadConfigSnapshot() |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| errSummary, |
| fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err), |
| )) |
| return nil, snap, diags |
| } |
| loader := configload.NewLoaderFromSnapshot(snap) |
| config, configDiags := loader.LoadConfig(snap.Modules[""].Dir) |
| diags = diags.Append(configDiags) |
| if configDiags.HasErrors() { |
| return nil, snap, diags |
| } |
| run.Config = config |
| |
| // NOTE: We're intentionally comparing the current locks with the |
| // configuration snapshot, rather than the lock snapshot in the plan file, |
| // because it's the current locks which dictate our plugin selections |
| // in coreOpts below. However, we'll also separately check that the |
| // plan file has identical locked plugins below, and thus we're effectively |
| // checking consistency with both here. |
| if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 { |
| var buf strings.Builder |
| for _, err := range errs { |
| fmt.Fprintf(&buf, "\n - %s", err.Error()) |
| } |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Inconsistent dependency lock file", |
| fmt.Sprintf( |
| "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.", |
| buf.String(), |
| ), |
| )) |
| } |
| |
| // This check is an important complement to the check above: the locked |
| // dependencies in the configuration must match the configuration, and |
| // the locked dependencies in the plan must match the locked dependencies |
| // in the configuration, and so transitively we ensure that the locked |
| // dependencies in the plan match the configuration too. However, this |
| // additionally catches any inconsistency between the two sets of locks |
| // even if they both happen to be valid per the current configuration, |
| // which is one of several ways we try to catch the mistake of applying |
| // a saved plan file in a different place than where we created it. |
| depLocksFromPlan, moreDiags := pf.ReadDependencyLocks() |
| diags = diags.Append(moreDiags) |
| if depLocksFromPlan != nil && !op.DependencyLocks.Equal(depLocksFromPlan) { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Inconsistent dependency lock file", |
| "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.", |
| )) |
| } |
| |
| // A plan file also contains a snapshot of the prior state the changes |
| // are intended to apply to. |
| priorStateFile, err := pf.ReadStateFile() |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| errSummary, |
| fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err), |
| )) |
| return nil, snap, diags |
| } |
| if currentStateMeta != nil { |
| // If the caller sets this, we require that the stored prior state |
| // has the same metadata, which is an extra safety check that nothing |
| // has changed since the plan was created. (All of the "real-world" |
| // state manager implementations support this, but simpler test backends |
| // may not.) |
| |
| // Because the plan always contains a state, even if it is empty, the |
| // first plan to be applied will have empty snapshot metadata. In this |
| // case we compare only the serial in order to provide a more correct |
| // error. |
| firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0 |
| |
| switch { |
| case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Saved plan does not match the given state", |
| "The given plan file can not be applied because it was created from a different state lineage.", |
| )) |
| |
| case priorStateFile.Serial != currentStateMeta.Serial: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Saved plan is stale", |
| "The given plan file can no longer be applied because the state was changed by another operation after the plan was created.", |
| )) |
| } |
| } |
| // When we're applying a saved plan, the input state is the "prior state" |
| // recorded in the plan, which incorporates the result of all of the |
| // refreshing we did while building the plan. |
| run.InputState = priorStateFile.State |
| |
| plan, err := pf.ReadPlan() |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| errSummary, |
| fmt.Sprintf("Failed to read plan from plan file: %s.", err), |
| )) |
| return nil, snap, diags |
| } |
| // When we're applying a saved plan, we populate Plan instead of PlanOpts, |
| // because a plan object incorporates the subset of data from PlanOps that |
| // we need to apply the plan. |
| run.Plan = plan |
| |
| tfCtx, moreDiags := terraform.NewContext(coreOpts) |
| diags = diags.Append(moreDiags) |
| if moreDiags.HasErrors() { |
| return nil, nil, diags |
| } |
| run.Core = tfCtx |
| return run, snap, diags |
| } |
| |
| // interactiveCollectVariables attempts to complete the given existing |
| // map of variables by interactively prompting for any variables that are |
| // declared as required but not yet present. |
| // |
| // If interactive input is disabled for this backend instance then this is |
| // a no-op. If input is enabled but fails for some reason, the resulting |
| // map will be incomplete. For these reasons, the caller must still validate |
| // that the result is complete and valid. |
| // |
| // This function does not modify the map given in "existing", but may return |
| // it unchanged if no modifications are required. If modifications are required, |
| // the result is a new map with all of the elements from "existing" plus |
| // additional elements as appropriate. |
| // |
| // Interactive prompting is a "best effort" thing for first-time user UX and |
| // not something we expect folks to be relying on for routine use. Terraform |
| // is primarily a non-interactive tool and so we prefer to report in error |
| // messages that variables are not set rather than reporting that input failed: |
| // the primary resolution to missing variables is to provide them by some other |
| // means. |
| func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { |
| var needed []string |
| if b.OpInput && uiInput != nil { |
| for name, vc := range vcs { |
| if !vc.Required() { |
| continue // We only prompt for required variables |
| } |
| if _, exists := existing[name]; !exists { |
| needed = append(needed, name) |
| } |
| } |
| } else { |
| log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled") |
| } |
| if len(needed) == 0 { |
| return existing |
| } |
| |
| log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed) |
| |
| // If we get here then we're planning to prompt for at least one additional |
| // variable's value. |
| sort.Strings(needed) // prompt in lexical order |
| ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) |
| for k, v := range existing { |
| ret[k] = v |
| } |
| for _, name := range needed { |
| vc := vcs[name] |
| rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ |
| Id: fmt.Sprintf("var.%s", name), |
| Query: fmt.Sprintf("var.%s", name), |
| Description: vc.Description, |
| }) |
| if err != nil { |
| // Since interactive prompts are best-effort, we'll just continue |
| // here and let subsequent validation report this as a variable |
| // not specified. |
| log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err) |
| continue |
| } |
| ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue} |
| } |
| return ret |
| } |
| |
| // stubUnsetVariables ensures that all required variables defined in the |
| // configuration exist in the resulting map, by adding new elements as necessary. |
| // |
| // The stubbed value of any additions will be an unknown variable conforming |
| // to the variable's configured type constraint, meaning that no particular |
| // value is known and that one must be provided by the user in order to get |
| // a complete result. |
| // |
| // Unset optional attributes (those with default values) will not be populated |
| // by this function, under the assumption that a later step will handle those. |
| // In this sense, stubUnsetRequiredVariables is essentially a non-interactive, |
| // non-error-producing variant of interactiveCollectVariables that creates |
| // placeholders for values the user would be prompted for interactively on |
| // other operations. |
| // |
| // This function should be used only in situations where variables values |
| // will not be directly used and the variables map is being constructed only |
| // to produce a complete Terraform context for some ancillary functionality |
| // like "terraform console", "terraform state ...", etc. |
| // |
| // This function is guaranteed not to modify the given map, but it may return |
| // the given map unchanged if no additions are required. If additions are |
| // required then the result will be a new map containing everything in the |
| // given map plus additional elements. |
| func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue { |
| var missing bool // Do we need to add anything? |
| for name, vc := range vcs { |
| if !vc.Required() { |
| continue // We only stub required variables |
| } |
| if _, exists := existing[name]; !exists { |
| missing = true |
| } |
| } |
| if !missing { |
| return existing |
| } |
| |
| // If we get down here then there's at least one variable value to add. |
| ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) |
| for k, v := range existing { |
| ret[k] = v |
| } |
| for name, vc := range vcs { |
| if !vc.Required() { |
| continue |
| } |
| if _, exists := existing[name]; !exists { |
| ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type} |
| } |
| } |
| return ret |
| } |
| |
| type unparsedInteractiveVariableValue struct { |
| Name, RawValue string |
| } |
| |
| var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} |
| |
| func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| val, valDiags := mode.Parse(v.Name, v.RawValue) |
| diags = diags.Append(valDiags) |
| if diags.HasErrors() { |
| return nil, diags |
| } |
| return &terraform.InputValue{ |
| Value: val, |
| SourceType: terraform.ValueFromInput, |
| }, diags |
| } |
| |
| type unparsedUnknownVariableValue struct { |
| Name string |
| WantType cty.Type |
| } |
| |
| var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{} |
| |
| func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { |
| return &terraform.InputValue{ |
| Value: cty.UnknownVal(v.WantType), |
| SourceType: terraform.ValueFromInput, |
| }, nil |
| } |