@ -11,232 +11,315 @@ import (
"github.com/mitchellh/colorstring"
)
// PlanOpts are the options for formatting a plan.
type PlanOpts struct {
// Plan is the plan to format. This is required.
Plan * terraform . Plan
// 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
// Color is the colorizer. This is optional.
Color * colorstring . Colorize
// Attributes describes changes to the attributes of the instance.
//
// For destroy diffs this is always nil.
Attributes [ ] * AttributeDiff
// ModuleDepth is the depth of the modules to expand. By default this
// is zero which will not expand modules at all.
ModuleDepth int
Tainted bool
Deposed bool
}
// Plan takes a plan and returns a
func Plan ( opts * PlanOpts ) string {
p := opts . Plan
if p . Diff == nil || p . Diff . Empty ( ) {
return "This plan does nothing."
}
// 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
if opts . Color == nil {
opts . Color = & colorstring . Colorize {
Colors : colorstring . DefaultColors ,
Reset : false ,
}
}
Action terraform . DiffChangeType
buf := new ( bytes . Buffer )
for _ , m := range p . Diff . Modules {
if len ( m . Path ) - 1 <= opts . ModuleDepth || opts . ModuleDepth == - 1 {
formatPlanModuleExpand ( buf , m , opts )
} else {
formatPlanModuleSingle ( buf , m , opts )
}
}
OldValue string
NewValue string
return strings . TrimSpace ( buf . String ( ) )
NewComputed bool
Sensitive bool
ForcesNew bool
}
// formatPlanModuleExpand will output the given module and all of its
// resources.
func formatPlanModuleExpand (
buf * bytes . Buffer , m * terraform . ModuleDiff , opts * PlanOpts ) {
// Ignore empty diffs
if m . Empty ( ) {
return
}
// PlanStats gives summary counts for a Plan.
type PlanStats struct {
ToAdd , ToChange , ToDestroy int
}
var modulePath [ ] string
if ! m . IsRoot ( ) {
modulePath = m . Path [ 1 : ]
// 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
}
// We want to output the resources in sorted order to make things
// easier to scan through, so get all the resource names and sort them.
names := make ( [ ] string , 0 , len ( m . Resources ) )
addrs := map [ string ] * terraform . ResourceAddress { }
for name := range m . Resources {
names = append ( names , name )
var err error
addrs [ name ] , err = terraform . ParseResourceAddressForInstanceDiff ( modulePath , name )
if err != nil {
// should never happen; indicates invalid diff
panic ( "invalid resource address in diff" )
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 : ]
}
}
sort . Slice ( names , func ( i , j int ) bool {
return addrs [ names [ i ] ] . Less ( addrs [ names [ j ] ] )
} )
// Go through each sorted name and start building the output
for _ , name := range names {
rdiff := m . Resources [ name ]
if rdiff . Empty ( ) {
continue
}
for k , r := range m . Resources {
if r . Empty ( ) {
continue
}
addr := addrs [ name ]
addrStr := addr . String ( )
dataSource := addr . Mode == config . DataResourceMode
// 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 := " ~"
oldValues := true
switch rdiff . ChangeType ( ) {
case terraform . DiffDestroyCreate :
color = "yellow"
symbol = "[red]-[reset]/[green]+[reset][yellow]"
case terraform . DiffCreate :
color = "green"
symbol = " +"
oldValues = false
// If we're "creating" a data resource then we'll present it
// to the user as a "read" operation, so it's clear that this
// operation won't change anything outside of the Terraform state.
// Unfortunately by the time we get here we only have the name
// to work with, so we need to cheat and exploit knowledge of the
// naming scheme for data resources.
if dataSource {
symbol = " <="
color = "cyan"
addr , err := terraform . ParseResourceAddressForInstanceDiff ( modulePath , k )
if err != nil {
// should never happen; indicates invalid diff
panic ( "invalid resource address in diff" )
}
case terraform . DiffDestroy :
color = "red"
symbol = " -"
}
var extraAttr [ ] string
if rdiff . DestroyTainted {
extraAttr = append ( extraAttr , "tainted" )
}
if rdiff . DestroyDeposed {
extraAttr = append ( extraAttr , "deposed" )
}
var extraStr string
if len ( extraAttr ) > 0 {
extraStr = fmt . Sprintf ( " (%s)" , strings . Join ( extraAttr , ", " ) )
}
if rdiff . ChangeType ( ) == terraform . DiffDestroyCreate {
extraStr = extraStr + opts . Color . Color ( " [red][bold](new resource required)" )
}
dataSource := addr . Mode == config . DataResourceMode
buf . WriteString ( opts . Color . Color ( fmt . Sprintf (
"[%s]%s %s%s\n" ,
color , symbol , addrStr , extraStr ) ) )
// Get all the attributes that are changing, and sort them. Also
// determine the longest key so that we can align them all.
keyLen := 0
keys := make ( [ ] string , 0 , len ( rdiff . Attributes ) )
for key , _ := range rdiff . Attributes {
// Skip the ID since we do that specially
if key == "id" {
// 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
}
keys = append ( keys , key )
if len ( key ) > keyLen {
keyLen = len ( key )
did := & InstanceDiff {
Addr : addr ,
Action : r . ChangeType ( ) ,
Tainted : r . DestroyTainted ,
Deposed : r . DestroyDeposed ,
}
}
sort . Strings ( keys )
// Go through and output each attribute
for _ , attrK := range keys {
attrDiff := rdiff . Attributes [ attrK ]
v := attrDiff . New
if v == "" && attrDiff . NewComputed {
v = "<computed>"
if dataSource && did . Action == terraform . DiffCreate {
// Use "refresh" as the action for display, since core
// currently uses Create for this.
did . Action = terraform . DiffRefresh
}
if attrDiff . Sensitive {
v = "<sensitive>"
ret . Resources = append ( ret . Resources , did )
if did . Action == terraform . DiffDestroy {
// Don't show any outputs for destroy actions
continue
}
updateMsg := ""
if attrDiff . RequiresNew && rdiff . Destroy {
updateMsg = opts . Color . Color ( " [red](forces new resource)" )
} else if attrDiff . Sensitive && oldValues {
updateMsg = opts . Color . Color ( " [yellow](attribute changed)" )
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 ,
} )
}
if oldValues {
var u string
if attrDiff . Sensitive {
u = "<sensitive>"
} else {
u = attrDiff . Old
// 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
}
buf . WriteString ( fmt . Sprintf (
" %s:%s %#v => %#v%s\n" ,
attrK ,
strings . Repeat ( " " , keyLen - len ( attrK ) ) ,
u ,
v ,
updateMsg ) )
} else {
buf . WriteString ( fmt . Sprintf (
" %s:%s %#v%s\n" ,
attrK ,
strings . Repeat ( " " , keyLen - len ( attrK ) ) ,
v ,
updateMsg ) )
} )
}
}
// 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 )
}
}
}
// Write the reset color so we don't overload the user's terminal
buf . WriteString ( opts . Color . Color ( "[reset]\n" ) )
buf := new ( bytes . Buffer )
for _ , r := range p . Resources {
formatPlanInstanceDiff ( buf , r , keyLen , color )
}
return strings . TrimSpace ( buf . String ( ) )
}
// formatPlanModuleSingle will output the given module and all of its
// resources.
func formatPlanModuleSingle (
buf * bytes . Buffer , m * terraform . ModuleDiff , opts * PlanOpts ) {
// Ignore empty diffs
if m . Empty ( ) {
return
// 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
}
moduleName := fmt . Sprintf ( "module.%s" , strings . Join ( m . Path [ 1 : ] , "." ) )
// 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
}
// 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 := "~"
switch m . ChangeType ( ) {
symbol := " ~"
oldValues := true
switch r . Action {
case terraform . DiffDestroyCreate :
color = "yellow"
symbol = "[red]-[reset]/[green]+[reset][yellow]"
case terraform . DiffCreate :
color = "green"
symbol = "+"
symbol = " +"
oldValues = false
case terraform . DiffDestroy :
color = "red"
symbol = "-"
symbol = " -"
case terraform . DiffRefresh :
symbol = " <="
color = "cyan"
oldValues = false
}
var extraStr string
if r . Tainted {
extraStr = extraStr + colorizer . Color ( " (tainted)" )
}
if r . Deposed {
extraStr = extraStr + colorizer . Color ( " (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\n" ,
color , symbol , 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 ,
) )
}
}
buf . WriteString ( opts . Color . Color ( fmt . Sprintf (
"[%s]%s %s\n" ,
color , symbol , moduleName ) ) )
buf . WriteString ( fmt . Sprintf (
" %d resource(s)" ,
len ( m . Resources ) ) )
buf . WriteString ( opts . Color . Color ( "[reset]\n" ) )
// Write the reset color so we don't bleed color into later text
buf . WriteString ( colorizer . Color ( "[reset]\n" ) )
}