Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion internal/backend/backendrun/local_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package backendrun

import (
"context"

"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/policy"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terraform"
Expand All @@ -29,7 +32,11 @@ type Local interface {
// backend's implementations of this to understand what this actually
// does, because this operation has no well-defined contract aside from
// "whatever it already does".
LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics)
LocalRun(context.Context, *Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics)

// Finish should be called when the local run has completed executing and
// the resources should be cleaned up.
Finish()
}

// LocalRun represents the assortment of objects that we can collect or
Expand Down Expand Up @@ -77,4 +84,17 @@ type LocalRun struct {
//
// This is nil when we're not applying a saved plan.
Plan *plans.Plan

// PolicyClient is an optional argument that enables policy evaluations
// during the run.
PolicyClient policy.Client
}

func (lr *LocalRun) Finish() {
if lr == nil {
return
}
if lr.PolicyClient != nil {
lr.PolicyClient.Stop()
}
}
6 changes: 6 additions & 0 deletions internal/backend/backendrun/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/policy"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -165,6 +166,11 @@ type Operation struct {

// Query is true if the operation should be a query operation
Query bool

// PolicyPaths will trigger Terraform to load policies from the specified
// paths.
PolicyPaths []string
PolicyClient policy.Client
}

// HasConfig returns true if and only if the operation has a ConfigDir value
Expand Down
7 changes: 6 additions & 1 deletion internal/backend/local/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"sort"
"sync"

"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/views"
Expand All @@ -21,7 +23,6 @@ import (
"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"
)

const (
Expand Down Expand Up @@ -113,6 +114,10 @@ func NewWithBackend(backend backend.Backend) *Local {
}
}

func (b *Local) Finish() {
// nothing to do
}

func (b *Local) ConfigSchema() *configschema.Block {
if b.Backend != nil {
return b.Backend.ConfigSchema()
Expand Down
18 changes: 17 additions & 1 deletion internal/backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func (b *Local) opApply(

// Get our context
lr, _, opState, contextDiags := b.localRun(op)
defer lr.Finish()

diags = diags.Append(contextDiags)
if contextDiags.HasErrors() {
op.ReportResult(runningOp, diags)
Expand Down Expand Up @@ -94,6 +96,8 @@ func (b *Local) opApply(
combinedPlanApply := false
// If we weren't given a plan, then we refresh/plan
if op.PlanFile == nil {
// set the policy client to nil for the plan preceding apply
lr.PlanOpts.PolicyClient = nil
combinedPlanApply = true
// Perform the plan
log.Printf("[INFO] backend/local: apply calling Plan")
Expand All @@ -110,6 +114,8 @@ func (b *Local) opApply(
if plan != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) {
op.View.Plan(plan, schemas)
}
// Report all policy results that may have accumulated during the plan
op.View.PolicyResults(plan.PolicyResults)
op.ReportResult(runningOp, diags)
return
}
Expand Down Expand Up @@ -420,14 +426,21 @@ func (b *Local) opApply(
// Start the apply in a goroutine so that we can be interrupted.
var applyState *states.State
var applyDiags tfdiags.Diagnostics

// We use a new store for the apply policy results, as objects that failed during the plan policy
// evaluation may have updated data which yields a different policy evaluation result.
policyResults := plans.NewPolicyResults()
doneCh := make(chan struct{})
go func() {
defer logging.PanicHandler()
defer close(doneCh)

log.Printf("[INFO] backend/local: apply calling Apply")
applyState, applyDiags = lr.Core.Apply(plan, lr.Config, &terraform.ApplyOpts{
SetVariables: applyTimeValues,
SetVariables: applyTimeValues,
Locks: providerLocksSnapshot(op.DependencyLocks),
PolicyClient: lr.PolicyClient,
PolicyResults: policyResults,
})
}()

Expand All @@ -436,6 +449,9 @@ func (b *Local) opApply(
}
diags = diags.Append(applyDiags)

// Print the policy results we found during apply
op.View.PolicyResults(policyResults)

// Even on error with an empty state, the state value should not be nil.
// Return early here to prevent corrupting any existing state.
if diags.HasErrors() && applyState == nil {
Expand Down
46 changes: 31 additions & 15 deletions internal/backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/states/statemgr"
Expand All @@ -26,7 +28,7 @@ import (
)

// backendrun.Local implementation.
func (b *Local) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) {
func (b *Local) LocalRun(ctx context.Context, op *backendrun.Operation) (*backendrun.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
Expand All @@ -35,7 +37,7 @@ func (b *Local) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statem
// happens to do.
op.Type = backendrun.OperationTypeInvalid

op.StateLocker = op.StateLocker.WithContext(context.Background())
op.StateLocker = op.StateLocker.WithContext(ctx)

lr, _, stateMgr, diags := b.localRun(op)
return lr, stateMgr, diags
Expand Down Expand Up @@ -70,8 +72,6 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
return nil, nil, nil, diags
}

ret := &backendrun.LocalRun{}

// Initialize our context options
var coreOpts terraform.ContextOpts
if v := b.ContextOpts; v != nil {
Expand All @@ -80,11 +80,16 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
coreOpts.UIInput = op.UIIn
coreOpts.Hooks = op.Hooks

// the run must be closed now
ret := &backendrun.LocalRun{
PolicyClient: op.PolicyClient,
}

var ctxDiags tfdiags.Diagnostics
var configSnap *configload.Snapshot
if op.PlanFile.IsCloud() {
diags = diags.Append(fmt.Errorf("error: using a saved cloud plan when executing Terraform locally is not supported"))
return nil, nil, nil, diags
return ret, nil, nil, diags
}

if lp, ok := op.PlanFile.Local(); ok {
Expand All @@ -100,7 +105,7 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
ret, configSnap, ctxDiags = b.localRunForPlanFile(op, lp, ret, &coreOpts, stateMeta)
if ctxDiags.HasErrors() {
diags = diags.Append(ctxDiags)
return nil, nil, nil, diags
return ret, nil, nil, diags
}

// Write sources into the cache of the main loader so that they are
Expand All @@ -112,7 +117,7 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
}
diags = diags.Append(ctxDiags)
if diags.HasErrors() {
return nil, nil, nil, diags
return ret, nil, nil, diags
}

// If we have an operation, then we automatically do the input/validate
Expand All @@ -126,7 +131,7 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
inputDiags := ret.Core.Input(ret.Config, mode)
diags = diags.Append(inputDiags)
if inputDiags.HasErrors() {
return nil, nil, nil, diags
return ret, nil, nil, diags
}
}

Expand All @@ -149,7 +154,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
rootMod, configDiags := op.ConfigLoader.LoadRootModule(op.ConfigDir)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, nil, diags
return run, nil, diags
}

var rawVariables map[string]arguments.UnparsedVariableValue
Expand All @@ -170,7 +175,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
variables, varDiags := backendrun.ParseVariableValues(rawVariables, rootMod.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
return run, nil, diags
}

planOpts := &terraform.PlanOpts{
Expand All @@ -183,6 +188,8 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
GenerateConfigPath: op.GenerateConfigOut,
DeferralAllowed: op.DeferralAllowed,
Query: op.Query,
Locks: providerLocksSnapshot(op.DependencyLocks),
PolicyClient: run.PolicyClient,
}
run.PlanOpts = planOpts

Expand All @@ -193,7 +200,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
tfCtx, moreDiags := terraform.NewContext(coreOpts)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, nil, diags
return run, nil, diags
}
run.Core = tfCtx

Expand Down Expand Up @@ -261,7 +268,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
errSummary,
fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err),
))
return nil, snap, diags
return run, snap, diags
}
loader := configload.NewLoaderFromSnapshot(snap)
loader.AllowLanguageExperiments(op.ConfigLoader.AllowsLanguageExperiments())
Expand Down Expand Up @@ -299,7 +306,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
errSummary,
fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err),
))
return nil, snap, diags
return run, snap, diags
}

if currentStateMeta != nil {
Expand Down Expand Up @@ -343,7 +350,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
errSummary,
fmt.Sprintf("Failed to read plan from plan file: %s.", err),
))
return nil, snap, diags
return run, 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
Expand Down Expand Up @@ -377,7 +384,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
tfCtx, moreDiags := terraform.NewContext(coreOpts)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, nil, diags
return run, nil, diags
}
run.Core = tfCtx

Expand Down Expand Up @@ -596,3 +603,12 @@ func (v unparsedTestVariableValue) ParseVariableValue(mode configs.VariableParsi
SourceRange: tfdiags.SourceRangeFromHCL(v.Expr.Range()),
}, diags
}

// providerLocksSnapshot returns a read-only snapshot of provider locks for
// use during graph walks. Returns nil if locks is nil.
func providerLocksSnapshot(locks *depsfile.Locks) map[addrs.Provider]*depsfile.ProviderLock {
if locks == nil {
return nil
}
return locks.AllProviders()
}
8 changes: 4 additions & 4 deletions internal/backend/local/backend_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestLocalRun(t *testing.T) {
StateLocker: stateLocker,
}

_, _, diags := b.LocalRun(op)
_, _, diags := b.LocalRun(context.Background(), op)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err().Error())
}
Expand Down Expand Up @@ -79,7 +79,7 @@ func TestLocalRun_error(t *testing.T) {
StateLocker: stateLocker,
}

_, _, diags := b.LocalRun(op)
_, _, diags := b.LocalRun(context.Background(), op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}
Expand Down Expand Up @@ -114,7 +114,7 @@ func TestLocalRun_cloudPlan(t *testing.T) {
StateLocker: stateLocker,
}

_, _, diags := b.LocalRun(op)
_, _, diags := b.LocalRun(context.Background(), op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}
Expand Down Expand Up @@ -201,7 +201,7 @@ func TestLocalRun_stalePlan(t *testing.T) {
StateLocker: stateLocker,
}

_, _, diags := b.LocalRun(op)
_, _, diags := b.LocalRun(context.Background(), op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}
Expand Down
8 changes: 8 additions & 0 deletions internal/backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (b *Local) opPlan(

// Set up backend and get our context
lr, configSnap, opState, ctxDiags := b.localRun(op)
defer lr.Finish()

diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
op.ReportResult(runningOp, diags)
Expand Down Expand Up @@ -211,6 +213,9 @@ func (b *Local) opPlan(
return
}

// set the config sources of the plan
plan.ConfigSources = op.ConfigLoader.Sources()

// Write out any generated config, before we render the plan.
wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut)
diags = diags.Append(moreDiags)
Expand All @@ -221,6 +226,9 @@ func (b *Local) opPlan(

op.View.Plan(plan, schemas)

// Report all policy results that may have accumulated during the plan
op.View.PolicyResults(plan.PolicyResults)

// If we've accumulated any diagnostics along the way then we'll show them
// here just before we show the summary and next steps. This can potentially
// include errors, because we intentionally try to show a partial plan
Expand Down
2 changes: 2 additions & 0 deletions internal/backend/local/backend_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func (b *Local) opRefresh(

// Get our context
lr, _, opState, contextDiags := b.localRun(op)
defer lr.Finish()

diags = diags.Append(contextDiags)
if contextDiags.HasErrors() {
op.ReportResult(runningOp, diags)
Expand Down
Loading