blob: a6a53d076818bdfb56b33ffb7b7f3181a5f56246 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"context"
"fmt"
"sync/atomic"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"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"
)
// Apply performs the changes described in a previously-generated plan,
// aiming to make the real system converge with the desired state and
// then emit a series of patches that the caller must make to the
// current state to represent what has changed.
//
// Apply does not return a result directly because it emits results in a
// streaming fashion using channels provided in the given [ApplyResponse].
//
// 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 Apply(ctx context.Context, req *ApplyRequest, resp *ApplyResponse) {
resp.Complete = false // We'll reset this to true only if we actually succeed
var seenAnyErrors atomic.Bool
outp := stackeval.ApplyOutput{
AnnounceAppliedChange: func(ctx context.Context, change stackstate.AppliedChange) {
resp.AppliedChanges <- change
},
AnnounceDiagnostics: func(ctx context.Context, diags tfdiags.Diagnostics) {
for _, diag := range diags {
if diag.Severity() == tfdiags.Error {
seenAnyErrors.Store(true) // never becomes false again
}
resp.Diagnostics <- diag
}
},
}
// 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.AppliedChanges) // MUST be the last channel to close
}()
main, err := stackeval.ApplyPlan(
ctx,
req.Config,
req.Plan,
stackeval.ApplyOpts{
InputVariableValues: req.InputValues,
ProviderFactories: req.ProviderFactories,
ExperimentsAllowed: req.ExperimentsAllowed,
DependencyLocks: req.DependencyLocks,
},
outp,
)
if err != nil {
// An error here means that the apply wasn't even able to _start_,
// typically because the request itself was invalid. We'll announce
// that as a diagnostic and then halt, though if we get here then
// it's most likely a bug in the caller rather than end-user error.
resp.Diagnostics <- tfdiags.Sourceless(
tfdiags.Error,
"Invalid apply request",
fmt.Sprintf("Cannot begin the apply phase: %s.", err),
)
return
}
if !seenAnyErrors.Load() {
resp.Complete = true
}
cleanupDiags := main.DoCleanup(ctx)
for _, diag := range cleanupDiags {
// cleanup diagnostics don't stop the apply from being "complete",
// since this should include only transient operational errors such
// as failing to terminate a provider plugin.
resp.Diagnostics <- diag
}
}
// ApplyRequest represents the inputs to an [Apply] call.
type ApplyRequest struct {
Config *stackconfig.Config
Plan *stackplan.Plan
InputValues map[stackaddrs.InputVariable]ExternalInputValue
ProviderFactories map[addrs.Provider]providers.Factory
ExperimentsAllowed bool
DependencyLocks depsfile.Locks
}
// ApplyResponse is used by [Apply] to describe the results of applying.
//
// [Apply] 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 non-channel fields of ApplyResponse until the
// AppliedChanges channel has been closed to signal the completion of the
// apply process.
type ApplyResponse struct {
// [Apply] will set this field to true if the apply ran to completion
// without encountering any errors, or set this to false if not.
//
// A caller might react to Complete: true by creating one follow-up plan
// just to confirm that everything has converged and then, if so, consider
// all of the configuration versions that contributed to this plan to now
// be converged. If unsuccessful, none of the contributing configurations
// are known to be converged and the operator will need to decide whether
// to immediately try creating a new plan (if they think the error was
// transient) or push a new configuration update to correct the problem.
//
// If this field is false after applying is complete then it's likely that
// at least some of the planned side-effects already occurred, and so
// it's important to still handle anything that was written to the
// AppliedChanges channel to partially update the state with the subset
// of changes that were completed.
//
// The initial value of this field is ignored; there's no reason to set
// it to anything other than the zero value.
Complete bool
// AppliedChanges is the channel that will be sent each individual
// applied change, in no predictable order, during the apply
// operation.
//
// Callers MUST provide a non-nil channel and read from it from
// another Goroutine throughout the apply operation, or apply
// progress will be blocked. Callers that read slowly should provide
// a buffered channel to reduce the backpressure they exert on the
// apply process.
//
// The apply operation will close this channel before it returns.
// AppliedChanges 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 apply 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 apply process.
AppliedChanges chan<- stackstate.AppliedChange
// Diagnostics is the channel that will be sent any diagnostics
// that arise during the apply process, in no particular order.
//
// In particular note that there's no guarantee that the diagnostics
// for applying changes to a particular object will be emitted in close
// proximity to an AppliedChanges write for that same object. Diagnostics
// and applied 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 apply
// progress will be blocked. Callers that read slowly should provide
// a buffered channel to reduce the backpressure they exert on the
// apply process.
//
// The apply operation will close this channel before it returns, but
// callers should use the close event of AppliedChanges as the definitive
// signal that planning is complete.
Diagnostics chan<- tfdiags.Diagnostic
}