// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package stackruntime import ( "context" "time" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" "github.com/hashicorp/terraform/internal/stacks/stackstate" "github.com/hashicorp/terraform/internal/tfdiags" ) // Plan evaluates the given configuration to calculate a desired state, // updates the given prior state to match the current state of real // infrastructure, and then compares the desired state with the updated prior // state to produce a proposed set of changes that should reduce the number // of differences between the two. // // Plan does not return a result directly because it emits results in a // streaming fashion using channels provided in the given [PlanResponse]. // // Callers must not modify any values reachable directly or indirectly // through resp after passing it to this function, aside from the implicit // modifications to the internal state of channels caused by reading them. func Plan(ctx context.Context, req *PlanRequest, resp *PlanResponse) { // Whatever return path we take, we must close our channels to allow // a caller to see that the operation is complete. defer func() { close(resp.Diagnostics) close(resp.PlannedChanges) // MUST be the last channel to close }() var errored bool planTimestamp := time.Now().UTC() if req.ForcePlanTimestamp != nil { planTimestamp = *req.ForcePlanTimestamp } main := stackeval.NewForPlanning(req.Config, req.PrevState, stackeval.PlanOpts{ PlanningMode: req.PlanMode, InputVariableValues: req.InputValues, ProviderFactories: req.ProviderFactories, DependencyLocks: req.DependencyLocks, PlanTimestamp: planTimestamp, }) main.AllowLanguageExperiments(req.ExperimentsAllowed) main.PlanAll(ctx, stackeval.PlanOutput{ AnnouncePlannedChange: func(ctx context.Context, change stackplan.PlannedChange) { resp.PlannedChanges <- change }, AnnounceDiagnostics: func(ctx context.Context, diags tfdiags.Diagnostics) { for _, diag := range diags { if diag.Severity() == tfdiags.Error { errored = true } resp.Diagnostics <- diag } }, }) cleanupDiags := main.DoCleanup(ctx) for _, diag := range cleanupDiags { // cleanup diagnostics don't stop a plan from being applyable, because // the cleanup process should not affect the content of and validity // of the plan. This should only include transient operational errors // such as failing to terminate a provider plugin. resp.Diagnostics <- diag } // An overall stack plan is applyable if it has no error diagnostics. resp.Applyable = !errored // Before we return we'll emit one more special planned change just to // remember in the raw plan sequence whether we considered this plan to be // applyable, so we don't need to rely on the caller to remember // resp.Applyable separately. resp.PlannedChanges <- &stackplan.PlannedChangeApplyable{ Applyable: resp.Applyable, } } // PlanRequest represents the inputs to a [Plan] call. type PlanRequest struct { PlanMode plans.Mode Config *stackconfig.Config PrevState *stackstate.State InputValues map[stackaddrs.InputVariable]ExternalInputValue ProviderFactories map[addrs.Provider]providers.Factory DependencyLocks depsfile.Locks // ForcePlanTimestamp, if not nil, will force the plantimestamp function // to return the given value instead of whatever real time the plan // operation started. This is for testing purposes only. ForcePlanTimestamp *time.Time ExperimentsAllowed bool } // PlanResponse is used by [Plan] to describe the results of planning. // // [Plan] produces streaming results throughout its execution, and so it // communicates with the caller by writing to provided channels during its work // and then modifying other fields in this structure before returning. Callers // MUST NOT access any fields of PlanResponse until the PlannedChanges // channel has been closed to signal the completion of the planning process. type PlanResponse struct { // [Plan] will set this field to true if the plan ran to completion and // is valid enough to be applied, or set this to false if not. // // The initial value of this field is ignored; there's no reason to set // it to anything other than the zero value. Applyable bool // PlannedChanges is the channel that will be sent each individual // planned change, in no predictable order, during the planning // operation. // // Callers MUST provide a non-nil channel and read from it from // another Goroutine throughout the plan operation, or planning // progress will be blocked. Callers that read slowly should provide // a buffered channel to reduce the backpressure they exert on the // planning process. // // The plan operation will close this channel before it returns // PlannedChanges is guaranteed to be the last channel to close // (i.e. after Diagnostics is closed) so callers can use the close // signal of this channel alone to mark that the plan process is // over, but if Diagnostics is a buffered channel they must take // care to deplete its buffer afterwards to avoid losing diagnostics // delivered near the end of the planning process. PlannedChanges chan<- stackplan.PlannedChange // Diagnostics is the channel that will be sent any diagnostics // that arise during the planning process, in no particular order. // // In particular note that there's no guarantee that the diagnostics // for planning a particular object will be emitted in close proximity // to a PlannedChanges write for that same object. Diagnostics and // planned changes are totally decoupled, since diagnostics might be // collected up and emitted later as a large batch if the runtime // needs to perform aggregate operations such as deduplication on // the diagnostics before exposing them. // // Callers MUST provide a non-nil channel and read from it from // another Goroutine throughout the plan operation, or planning // progress will be blocked. Callers that read slowly should provide // a buffered channel to reduce the backpressure they exert on the // planning process. // // The plan operation will close this channel before it returns, but // callers should use the close event of PlannedChanges as the definitive // signal that planning is complete. Diagnostics chan<- tfdiags.Diagnostic } type ExternalInputValue = stackeval.ExternalInputValue