terraform/backend/local/backend_local.go

213 lines
6.2 KiB
Go

package local
import (
"context"
"fmt"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// backend.Local implementation.
func (b *Local) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) {
// Make sure the type is invalid. We use this as a way to know not
// to ask for input/validate.
op.Type = backend.OperationTypeInvalid
if op.LockState {
op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.Colorize())
} else {
op.StateLocker = clistate.NewNoopLocker()
}
ctx, _, stateMgr, diags := b.context(op)
return ctx, stateMgr, diags
}
func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Get the latest state.
s, err := b.StateMgr(op.Workspace)
if err != nil {
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, nil, diags
}
if err := op.StateLocker.Lock(s, op.Type.String()); err != nil {
diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err))
return nil, nil, nil, diags
}
if err := s.RefreshState(); err != nil {
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, nil, diags
}
// Initialize our context options
var opts terraform.ContextOpts
if v := b.ContextOpts; v != nil {
opts = *v
}
// Copy set options from the operation
opts.Destroy = op.Destroy
opts.Targets = op.Targets
opts.UIInput = op.UIIn
// Load the latest state. If we enter contextFromPlanFile below then the
// state snapshot in the plan file must match this, or else it'll return
// error diagnostics.
opts.State = s.State()
var tfCtx *terraform.Context
var ctxDiags tfdiags.Diagnostics
var configSnap *configload.Snapshot
if op.PlanFile != nil {
tfCtx, configSnap, ctxDiags = b.contextFromPlanFile(op.PlanFile, opts)
// 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 {
tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts)
}
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
mode |= terraform.InputModeVar
mode |= terraform.InputModeVarUnset
inputDiags := tfCtx.Input(mode)
diags = diags.Append(inputDiags)
if inputDiags.HasErrors() {
return nil, nil, nil, diags
}
}
// If validation is enabled, validate
if b.OpValidation {
validateDiags := tfCtx.Validate()
diags = diags.Append(validateDiags)
}
}
return tfCtx, configSnap, s, diags
}
func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts) (*terraform.Context, *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
}
opts.Config = config
variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
}
if op.Variables != nil {
opts.Variables = variables
}
tfCtx, ctxDiags := terraform.NewContext(&opts)
diags = diags.Append(ctxDiags)
return tfCtx, configSnap, diags
}
func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextOpts) (*terraform.Context, *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),
))
}
loader := configload.NewLoaderFromSnapshot(snap)
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, snap, diags
}
opts.Config = config
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),
))
}
variables := terraform.InputValues{}
for name, dyVal := range plan.VariableValues {
ty, err := dyVal.ImpliedType()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errSummary,
fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
))
continue
}
val, err := dyVal.Decode(ty)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
errSummary,
fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
))
continue
}
variables[name] = &terraform.InputValue{
Value: val,
SourceType: terraform.ValueFromPlan,
}
}
opts.Variables = variables
// TODO: populate the changes (formerly diff)
// TODO: targets
// TODO: check that the states match
// TODO: impose provider SHA256 constraints
tfCtx, ctxDiags := terraform.NewContext(&opts)
diags = diags.Append(ctxDiags)
return tfCtx, snap, diags
}
const validateWarnHeader = `
There are warnings related to your configuration. If no errors occurred,
Terraform will continue despite these warnings. It is a good idea to resolve
these warnings in the near future.
Warnings:
`