| // 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 |