mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-13 09:32:24 -06:00
254 lines
8.1 KiB
Go
254 lines
8.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package local
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/genconfig"
|
|
"github.com/opentofu/opentofu/internal/logging"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/plans/planfile"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
)
|
|
|
|
func (b *Local) opPlan(
|
|
stopCtx context.Context,
|
|
cancelCtx context.Context,
|
|
op *backend.Operation,
|
|
runningOp *backend.RunningOperation) {
|
|
|
|
log.Printf("[INFO] backend/local: starting Plan operation")
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
if op.PlanFile != nil {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Can't re-plan a saved plan",
|
|
"The plan command was given a saved plan file as its input. This command generates "+
|
|
"a new plan, and so it requires a configuration directory as its argument.",
|
|
))
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
// Local planning requires a config, unless we're planning to destroy.
|
|
if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No configuration files",
|
|
"Plan requires configuration to be present. Planning without a configuration would "+
|
|
"mark everything for destruction, which is normally not what is desired. If you "+
|
|
"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
|
|
"create a OpenTofu configuration file (.tf file) and try again.",
|
|
))
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
if len(op.GenerateConfigOut) > 0 {
|
|
if op.PlanMode != plans.NormalMode {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid generate-config-out flag",
|
|
"Config can only be generated during a normal plan operation, and not during a refresh-only or destroy plan."))
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut))
|
|
if diags.HasErrors() {
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
}
|
|
|
|
if b.ContextOpts == nil {
|
|
b.ContextOpts = new(tofu.ContextOpts)
|
|
}
|
|
|
|
// Get our context
|
|
lr, configSnap, opState, ctxDiags := b.localRun(op)
|
|
diags = diags.Append(ctxDiags)
|
|
if ctxDiags.HasErrors() {
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
// the state was locked during succesfull context creation; unlock the state
|
|
// when the operation completes
|
|
defer func() {
|
|
diags := op.StateLocker.Unlock()
|
|
if diags.HasErrors() {
|
|
op.View.Diagnostics(diags)
|
|
runningOp.Result = backend.OperationFailure
|
|
}
|
|
}()
|
|
|
|
// Since planning doesn't immediately change the persisted state, the
|
|
// resulting state is always just the input state.
|
|
runningOp.State = lr.InputState
|
|
|
|
// Perform the plan in a goroutine so we can be interrupted
|
|
var plan *plans.Plan
|
|
var planDiags tfdiags.Diagnostics
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
defer logging.PanicHandler()
|
|
defer close(doneCh)
|
|
log.Printf("[INFO] backend/local: plan calling Plan")
|
|
plan, planDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts)
|
|
}()
|
|
|
|
if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) {
|
|
// If we get in here then the operation was cancelled, which is always
|
|
// considered to be a failure.
|
|
log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt")
|
|
runningOp.Result = backend.OperationFailure
|
|
return
|
|
}
|
|
log.Printf("[INFO] backend/local: plan operation completed")
|
|
|
|
// NOTE: We intentionally don't stop here on errors because we always want
|
|
// to try to present a partial plan report and, if the user chose to,
|
|
// generate a partial saved plan file for external analysis.
|
|
diags = diags.Append(planDiags)
|
|
|
|
// Even if there are errors we need to handle anything that may be
|
|
// contained within the plan, so only exit if there is no data at all.
|
|
if plan == nil {
|
|
runningOp.PlanEmpty = true
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
// Record whether this plan includes any side-effects that could be applied.
|
|
runningOp.PlanEmpty = !plan.CanApply()
|
|
|
|
// Save the plan to disk
|
|
if path := op.PlanOutPath; path != "" {
|
|
if op.PlanOutBackend == nil {
|
|
// This is always a bug in the operation caller; it's not valid
|
|
// to set PlanOutPath without also setting PlanOutBackend.
|
|
diags = diags.Append(fmt.Errorf(
|
|
"PlanOutPath set without also setting PlanOutBackend (this is a bug in OpenTofu)"),
|
|
)
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
plan.Backend = *op.PlanOutBackend
|
|
|
|
// We may have updated the state in the refresh step above, but we
|
|
// will freeze that updated state in the plan file for now and
|
|
// only write it if this plan is subsequently applied.
|
|
plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState)
|
|
|
|
// We also include a file containing the state as it existed before
|
|
// we took any action at all, but this one isn't intended to ever
|
|
// be saved to the backend (an equivalent snapshot should already be
|
|
// there) and so we just use a stub state file header in this case.
|
|
// NOTE: This won't be exactly identical to the latest state snapshot
|
|
// in the backend because it's still been subject to state upgrading
|
|
// to make it consumable by the current OpenTofu version, and
|
|
// intentionally doesn't preserve the header info.
|
|
prevStateFile := &statefile.File{
|
|
State: plan.PrevRunState,
|
|
}
|
|
|
|
log.Printf("[INFO] backend/local: writing plan output to: %s", path)
|
|
err := planfile.Create(path, planfile.CreateArgs{
|
|
ConfigSnapshot: configSnap,
|
|
PreviousRunStateFile: prevStateFile,
|
|
StateFile: plannedStateFile,
|
|
Plan: plan,
|
|
DependencyLocks: op.DependencyLocks,
|
|
})
|
|
if err != nil {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Failed to write plan file",
|
|
fmt.Sprintf("The plan file could not be written: %s.", err),
|
|
))
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Render the plan, if we produced one.
|
|
// (This might potentially be a partial plan with Errored set to true)
|
|
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
// Write out any generated config, before we render the plan.
|
|
wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
op.ReportResult(runningOp, diags)
|
|
return
|
|
}
|
|
|
|
op.View.Plan(plan, schemas)
|
|
|
|
// 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
|
|
// above even if OpenTofu Core encountered an error partway through
|
|
// creating it.
|
|
op.ReportResult(runningOp, diags)
|
|
|
|
if !runningOp.PlanEmpty {
|
|
if wroteConfig {
|
|
op.View.PlanNextStep(op.PlanOutPath, op.GenerateConfigOut)
|
|
} else {
|
|
op.View.PlanNextStep(op.PlanOutPath, "")
|
|
}
|
|
}
|
|
}
|
|
|
|
func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) {
|
|
if genconfig.ShouldWriteConfig(out) {
|
|
diags := genconfig.ValidateTargetFile(out)
|
|
if diags.HasErrors() {
|
|
return false, diags
|
|
}
|
|
|
|
var writer io.Writer
|
|
for _, c := range plan.Changes.Resources {
|
|
change := genconfig.Change{
|
|
Addr: c.Addr.String(),
|
|
GeneratedConfig: c.GeneratedConfig,
|
|
}
|
|
if c.Importing != nil {
|
|
change.ImportID = c.Importing.ID
|
|
}
|
|
|
|
var moreDiags tfdiags.Diagnostics
|
|
writer, wroteConfig, moreDiags = change.MaybeWriteConfig(writer, out)
|
|
if moreDiags.HasErrors() {
|
|
return false, diags.Append(moreDiags)
|
|
}
|
|
}
|
|
}
|
|
|
|
if wroteConfig {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Warning,
|
|
"Config generation is experimental",
|
|
"Generating configuration during import is currently experimental, and the generated configuration format may change in future versions."))
|
|
}
|
|
|
|
return wroteConfig, diags
|
|
}
|