terraform/command/format/plan.go

350 lines
8.4 KiB
Go

package format
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/colorstring"
)
// Plan is a representation of a plan optimized for display to
// an end-user, as opposed to terraform.Plan which is for internal use.
//
// DisplayPlan excludes implementation details that may otherwise appear
// in the main plan, such as destroy actions on data sources (which are
// there only to clean up the state).
type Plan struct {
Resources []*InstanceDiff
}
// InstanceDiff is a representation of an instance diff optimized
// for display, in conjunction with DisplayPlan.
type InstanceDiff struct {
Addr *terraform.ResourceAddress
Action terraform.DiffChangeType
// Attributes describes changes to the attributes of the instance.
//
// For destroy diffs this is always nil.
Attributes []*AttributeDiff
Tainted bool
Deposed bool
}
// AttributeDiff is a representation of an attribute diff optimized
// for display, in conjunction with DisplayInstanceDiff.
type AttributeDiff struct {
// Path is a dot-delimited traversal through possibly many levels of list and map structure,
// intended for display purposes only.
Path string
Action terraform.DiffChangeType
OldValue string
NewValue string
NewComputed bool
Sensitive bool
ForcesNew bool
}
// PlanStats gives summary counts for a Plan.
type PlanStats struct {
ToAdd, ToChange, ToDestroy int
}
// NewPlan produces a display-oriented Plan from a terraform.Plan.
func NewPlan(plan *terraform.Plan) *Plan {
ret := &Plan{}
if plan == nil || plan.Diff == nil || plan.Diff.Empty() {
// Nothing to do!
return ret
}
for _, m := range plan.Diff.Modules {
var modulePath []string
if !m.IsRoot() {
// trim off the leading "root" path segment, since it's implied
// when we use a path in a resource address.
modulePath = m.Path[1:]
}
for k, r := range m.Resources {
if r.Empty() {
continue
}
addr, err := terraform.ParseResourceAddressForInstanceDiff(modulePath, k)
if err != nil {
// should never happen; indicates invalid diff
panic("invalid resource address in diff")
}
dataSource := addr.Mode == config.DataResourceMode
// We create "destroy" actions for data resources so we can clean
// up their entries in state, but this is an implementation detail
// that users shouldn't see.
if dataSource && r.ChangeType() == terraform.DiffDestroy {
continue
}
did := &InstanceDiff{
Addr: addr,
Action: r.ChangeType(),
Tainted: r.DestroyTainted,
Deposed: r.DestroyDeposed,
}
if dataSource && did.Action == terraform.DiffCreate {
// Use "refresh" as the action for display, since core
// currently uses Create for this.
did.Action = terraform.DiffRefresh
}
ret.Resources = append(ret.Resources, did)
if did.Action == terraform.DiffDestroy {
// Don't show any outputs for destroy actions
continue
}
for k, a := range r.Attributes {
var action terraform.DiffChangeType
switch {
case a.NewRemoved:
action = terraform.DiffDestroy
case did.Action == terraform.DiffCreate:
action = terraform.DiffCreate
default:
action = terraform.DiffUpdate
}
did.Attributes = append(did.Attributes, &AttributeDiff{
Path: k,
Action: action,
OldValue: a.Old,
NewValue: a.New,
Sensitive: a.Sensitive,
ForcesNew: a.RequiresNew,
NewComputed: a.NewComputed,
})
}
// Sort the attributes by their paths for display
sort.Slice(did.Attributes, func(i, j int) bool {
iPath := did.Attributes[i].Path
jPath := did.Attributes[j].Path
// as a special case, "id" is always first
switch {
case iPath != jPath && (iPath == "id" || jPath == "id"):
return iPath == "id"
default:
return iPath < jPath
}
})
}
}
// Sort the instance diffs by their addresses for display.
sort.Slice(ret.Resources, func(i, j int) bool {
iAddr := ret.Resources[i].Addr
jAddr := ret.Resources[j].Addr
return iAddr.Less(jAddr)
})
return ret
}
// Format produces and returns a text representation of the receiving plan
// intended for display in a terminal.
//
// If color is not nil, it is used to colorize the output.
func (p *Plan) Format(color *colorstring.Colorize) string {
if p.Empty() {
return "This plan does nothing."
}
if color == nil {
color = &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Reset: false,
}
}
// Find the longest path length of all the paths that are changing,
// so we can align them all.
keyLen := 0
for _, r := range p.Resources {
for _, attr := range r.Attributes {
key := attr.Path
if len(key) > keyLen {
keyLen = len(key)
}
}
}
buf := new(bytes.Buffer)
for _, r := range p.Resources {
formatPlanInstanceDiff(buf, r, keyLen, color)
}
return strings.TrimSpace(buf.String())
}
// Stats returns statistics about the plan
func (p *Plan) Stats() PlanStats {
var ret PlanStats
for _, r := range p.Resources {
switch r.Action {
case terraform.DiffCreate:
ret.ToAdd++
case terraform.DiffUpdate:
ret.ToChange++
case terraform.DiffDestroyCreate:
ret.ToAdd++
ret.ToDestroy++
case terraform.DiffDestroy:
ret.ToDestroy++
}
}
return ret
}
// ActionCounts returns the number of diffs for each action type
func (p *Plan) ActionCounts() map[terraform.DiffChangeType]int {
ret := map[terraform.DiffChangeType]int{}
for _, r := range p.Resources {
ret[r.Action]++
}
return ret
}
// Empty returns true if there is at least one resource diff in the receiving plan.
func (p *Plan) Empty() bool {
return len(p.Resources) == 0
}
// DiffActionSymbol returns a string that, once passed through a
// colorstring.Colorize, will produce a result that can be written
// to a terminal to produce a symbol made of three printable
// characters, possibly interspersed with VT100 color codes.
func DiffActionSymbol(action terraform.DiffChangeType) string {
switch action {
case terraform.DiffDestroyCreate:
return "[red]-[reset]/[green]+[reset]"
case terraform.DiffCreate:
return " [green]+[reset]"
case terraform.DiffDestroy:
return " [red]-[reset]"
case terraform.DiffRefresh:
return " [cyan]<=[reset]"
default:
return " [yellow]~[reset]"
}
}
// formatPlanInstanceDiff writes the text representation of the given instance diff
// to the given buffer, using the given colorizer.
func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colorizer *colorstring.Colorize) {
addrStr := r.Addr.String()
// Determine the color for the text (green for adding, yellow
// for change, red for delete), and symbol, and output the
// resource header.
color := "yellow"
symbol := DiffActionSymbol(r.Action)
oldValues := true
switch r.Action {
case terraform.DiffDestroyCreate:
color = "yellow"
case terraform.DiffCreate:
color = "green"
oldValues = false
case terraform.DiffDestroy:
color = "red"
case terraform.DiffRefresh:
color = "cyan"
oldValues = false
}
var extraStr string
if r.Tainted {
extraStr = extraStr + " (tainted)"
}
if r.Deposed {
extraStr = extraStr + " (deposed)"
}
if r.Action == terraform.DiffDestroyCreate {
extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)")
}
buf.WriteString(
colorizer.Color(fmt.Sprintf(
"[%s]%s [%s]%s%s\n",
color, symbol, color, addrStr, extraStr,
)),
)
for _, attr := range r.Attributes {
v := attr.NewValue
var dispV string
switch {
case v == "" && attr.NewComputed:
dispV = "<computed>"
case attr.Sensitive:
dispV = "<sensitive>"
default:
dispV = fmt.Sprintf("%q", v)
}
updateMsg := ""
switch {
case attr.ForcesNew && r.Action == terraform.DiffDestroy:
updateMsg = colorizer.Color(" [red](forces new resource)")
case attr.Sensitive && oldValues:
updateMsg = colorizer.Color(" [yellow](attribute changed)")
}
if oldValues {
u := attr.OldValue
var dispU string
switch {
case attr.Sensitive:
dispU = "<sensitive>"
default:
dispU = fmt.Sprintf("%q", u)
}
buf.WriteString(fmt.Sprintf(
" %s:%s %s => %s%s\n",
attr.Path,
strings.Repeat(" ", keyLen-len(attr.Path)),
dispU, dispV,
updateMsg,
))
} else {
buf.WriteString(fmt.Sprintf(
" %s:%s %s%s\n",
attr.Path,
strings.Repeat(" ", keyLen-len(attr.Path)),
dispV,
updateMsg,
))
}
}
// Write the reset color so we don't bleed color into later text
buf.WriteString(colorizer.Color("[reset]\n"))
}