mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
The previous implementation of views was copying and embedding the base View struct in each individual view. While this allowed for easy access to the interface of that struct (both in the view and externally), it more importantly completely broke the ability of the diagnostic printer to output source code snippets. This is because the `configSources` field on the base view is lazily set after the config loader is initialized. In the commands ported to use views, this happens after the base View struct is copied, so we are updating the wrong copy of the struct. This commit fixes this with a simple mechanical refactor: keep a pointer to the base View struct instead, and update all of the individual views to explicitly refer to that struct to access its fields and methods. This is not a particularly satisfying solution, but I can't find anything clearly better. It might be worth exploring the alternative approach in the view for the new test command, which explicitly pulls its dependencies out of the base view, rather than retaining a full reference. Maybe there's a third way which is better still.
198 lines
5.8 KiB
Go
198 lines
5.8 KiB
Go
package views
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/command/arguments"
|
|
"github.com/hashicorp/terraform/command/format"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
// The Plan view is used for the plan command.
|
|
type Plan interface {
|
|
Operation() Operation
|
|
Hooks() []terraform.Hook
|
|
|
|
Diagnostics(diags tfdiags.Diagnostics)
|
|
HelpPrompt()
|
|
}
|
|
|
|
// NewPlan returns an initialized Plan implementation for the given ViewType.
|
|
func NewPlan(vt arguments.ViewType, runningInAutomation bool, view *View) Plan {
|
|
switch vt {
|
|
case arguments.ViewHuman:
|
|
return &PlanHuman{
|
|
view: view,
|
|
inAutomation: runningInAutomation,
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unknown view type %v", vt))
|
|
}
|
|
}
|
|
|
|
// The PlanHuman implementation renders human-readable text logs, suitable for
|
|
// a scrolling terminal.
|
|
type PlanHuman struct {
|
|
view *View
|
|
|
|
inAutomation bool
|
|
}
|
|
|
|
var _ Plan = (*PlanHuman)(nil)
|
|
|
|
func (v *PlanHuman) Operation() Operation {
|
|
return NewOperation(arguments.ViewHuman, v.inAutomation, v.view)
|
|
}
|
|
|
|
func (v *PlanHuman) Hooks() []terraform.Hook {
|
|
return []terraform.Hook{
|
|
NewUiHook(v.view),
|
|
}
|
|
}
|
|
|
|
func (v *PlanHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
|
v.view.Diagnostics(diags)
|
|
}
|
|
|
|
func (v *PlanHuman) HelpPrompt() {
|
|
v.view.HelpPrompt("plan")
|
|
}
|
|
|
|
// The plan renderer is used by the Operation view (for plan and apply
|
|
// commands) and the Show view (for the show command).
|
|
func renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, view *View) {
|
|
counts := map[plans.Action]int{}
|
|
var rChanges []*plans.ResourceInstanceChangeSrc
|
|
for _, change := range plan.Changes.Resources {
|
|
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
|
// Avoid rendering data sources on deletion
|
|
continue
|
|
}
|
|
|
|
rChanges = append(rChanges, change)
|
|
counts[change.Action]++
|
|
}
|
|
|
|
headerBuf := &bytes.Buffer{}
|
|
fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns())))
|
|
if counts[plans.Create] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create))
|
|
}
|
|
if counts[plans.Update] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update))
|
|
}
|
|
if counts[plans.Delete] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete))
|
|
}
|
|
if counts[plans.DeleteThenCreate] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate))
|
|
}
|
|
if counts[plans.CreateThenDelete] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete))
|
|
}
|
|
if counts[plans.Read] > 0 {
|
|
fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read))
|
|
}
|
|
|
|
view.streams.Println(view.colorize.Color(headerBuf.String()))
|
|
|
|
view.streams.Printf("Terraform will perform the following actions:\n\n")
|
|
|
|
// Note: we're modifying the backing slice of this plan object in-place
|
|
// here. The ordering of resource changes in a plan is not significant,
|
|
// but we can only do this safely here because we can assume that nobody
|
|
// is concurrently modifying our changes while we're trying to print it.
|
|
sort.Slice(rChanges, func(i, j int) bool {
|
|
iA := rChanges[i].Addr
|
|
jA := rChanges[j].Addr
|
|
if iA.String() == jA.String() {
|
|
return rChanges[i].DeposedKey < rChanges[j].DeposedKey
|
|
}
|
|
return iA.Less(jA)
|
|
})
|
|
|
|
for _, rcs := range rChanges {
|
|
if rcs.Action == plans.NoOp {
|
|
continue
|
|
}
|
|
|
|
providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider)
|
|
if providerSchema == nil {
|
|
// Should never happen
|
|
view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr)
|
|
continue
|
|
}
|
|
rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource)
|
|
if rSchema == nil {
|
|
// Should never happen
|
|
view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr)
|
|
continue
|
|
}
|
|
|
|
// check if the change is due to a tainted resource
|
|
tainted := false
|
|
if !baseState.Empty() {
|
|
if is := baseState.ResourceInstance(rcs.Addr); is != nil {
|
|
if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil {
|
|
tainted = obj.Status == states.ObjectTainted
|
|
}
|
|
}
|
|
}
|
|
|
|
view.streams.Println(format.ResourceChange(
|
|
rcs,
|
|
tainted,
|
|
rSchema,
|
|
view.colorize,
|
|
))
|
|
}
|
|
|
|
// stats is similar to counts above, but:
|
|
// - it considers only resource changes
|
|
// - it simplifies "replace" into both a create and a delete
|
|
stats := map[plans.Action]int{}
|
|
for _, change := range rChanges {
|
|
switch change.Action {
|
|
case plans.CreateThenDelete, plans.DeleteThenCreate:
|
|
stats[plans.Create]++
|
|
stats[plans.Delete]++
|
|
default:
|
|
stats[change.Action]++
|
|
}
|
|
}
|
|
view.streams.Printf(
|
|
view.colorize.Color("[reset][bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
|
|
stats[plans.Create], stats[plans.Update], stats[plans.Delete],
|
|
)
|
|
|
|
// If there is at least one planned change to the root module outputs
|
|
// then we'll render a summary of those too.
|
|
var changedRootModuleOutputs []*plans.OutputChangeSrc
|
|
for _, output := range plan.Changes.Outputs {
|
|
if !output.Addr.Module.IsRoot() {
|
|
continue
|
|
}
|
|
if output.ChangeSrc.Action == plans.NoOp {
|
|
continue
|
|
}
|
|
changedRootModuleOutputs = append(changedRootModuleOutputs, output)
|
|
}
|
|
if len(changedRootModuleOutputs) > 0 {
|
|
view.streams.Println(
|
|
view.colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]") +
|
|
format.OutputChanges(changedRootModuleOutputs, view.colorize),
|
|
)
|
|
}
|
|
}
|
|
|
|
const planHeaderIntro = `
|
|
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
|
|
`
|