255 lines
8.5 KiB
Go
255 lines
8.5 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
"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"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// 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.
|
|
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(errwrap.Wrapf("Error loading state: {{err}}", err))
|
|
return nil, nil, nil, diags
|
|
}
|
|
log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace)
|
|
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
|
|
}
|
|
log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace)
|
|
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.
|
|
log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace)
|
|
opts.State = s.State()
|
|
|
|
var tfCtx *terraform.Context
|
|
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: building context from plan file")
|
|
tfCtx, configSnap, ctxDiags = b.contextFromPlanFile(op.PlanFile, opts, stateMeta)
|
|
// 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: building context for current working directory")
|
|
tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts)
|
|
}
|
|
diags = diags.Append(ctxDiags)
|
|
if diags.HasErrors() {
|
|
return nil, nil, nil, diags
|
|
}
|
|
log.Printf("[TRACE] backend/local: finished building terraform.Context")
|
|
|
|
// 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
|
|
|
|
log.Printf("[TRACE] backend/local: requesting interactive input, if necessary")
|
|
inputDiags := tfCtx.Input(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 := 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, currentStateMeta *statemgr.SnapshotMeta) (*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),
|
|
))
|
|
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
|
|
}
|
|
opts.Config = config
|
|
|
|
// 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 implementstions support this, but simpler test backends
|
|
// may not.)
|
|
if currentStateMeta.Lineage != "" && priorStateFile.Lineage != "" {
|
|
if priorStateFile.Serial != currentStateMeta.Serial || priorStateFile.Lineage != currentStateMeta.Lineage {
|
|
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.",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
// The caller already wrote the "current state" here, but we're overriding
|
|
// it here with the prior state. These two should actually be identical in
|
|
// normal use, particularly if we validated the state meta above, but
|
|
// we do this here anyway to ensure consistent behavior.
|
|
opts.State = 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
|
|
}
|
|
|
|
variables := terraform.InputValues{}
|
|
for name, dyVal := range plan.VariableValues {
|
|
val, err := dyVal.Decode(cty.DynamicPseudoType)
|
|
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
|
|
opts.Changes = plan.Changes
|
|
opts.Targets = plan.TargetAddrs
|
|
opts.ProviderSHA256s = plan.ProviderSHA256s
|
|
|
|
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:
|
|
`
|