github.com/magodo/terraform@v0.11.12-beta1/command/format/plan.go (about) 1 package format 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/hashicorp/terraform/config" 10 "github.com/hashicorp/terraform/terraform" 11 "github.com/mitchellh/colorstring" 12 ) 13 14 // Plan is a representation of a plan optimized for display to 15 // an end-user, as opposed to terraform.Plan which is for internal use. 16 // 17 // DisplayPlan excludes implementation details that may otherwise appear 18 // in the main plan, such as destroy actions on data sources (which are 19 // there only to clean up the state). 20 type Plan struct { 21 Resources []*InstanceDiff 22 } 23 24 // InstanceDiff is a representation of an instance diff optimized 25 // for display, in conjunction with DisplayPlan. 26 type InstanceDiff struct { 27 Addr *terraform.ResourceAddress 28 Action terraform.DiffChangeType 29 30 // Attributes describes changes to the attributes of the instance. 31 // 32 // For destroy diffs this is always nil. 33 Attributes []*AttributeDiff 34 35 Tainted bool 36 Deposed bool 37 } 38 39 // AttributeDiff is a representation of an attribute diff optimized 40 // for display, in conjunction with DisplayInstanceDiff. 41 type AttributeDiff struct { 42 // Path is a dot-delimited traversal through possibly many levels of list and map structure, 43 // intended for display purposes only. 44 Path string 45 46 Action terraform.DiffChangeType 47 48 OldValue string 49 NewValue string 50 51 NewComputed bool 52 Sensitive bool 53 ForcesNew bool 54 } 55 56 // PlanStats gives summary counts for a Plan. 57 type PlanStats struct { 58 ToAdd, ToChange, ToDestroy int 59 } 60 61 // NewPlan produces a display-oriented Plan from a terraform.Plan. 62 func NewPlan(plan *terraform.Plan) *Plan { 63 ret := &Plan{} 64 if plan == nil || plan.Diff == nil || plan.Diff.Empty() { 65 // Nothing to do! 66 return ret 67 } 68 69 for _, m := range plan.Diff.Modules { 70 var modulePath []string 71 if !m.IsRoot() { 72 // trim off the leading "root" path segment, since it's implied 73 // when we use a path in a resource address. 74 modulePath = m.Path[1:] 75 } 76 77 for k, r := range m.Resources { 78 if r.Empty() { 79 continue 80 } 81 82 addr, err := terraform.ParseResourceAddressForInstanceDiff(modulePath, k) 83 if err != nil { 84 // should never happen; indicates invalid diff 85 panic("invalid resource address in diff") 86 } 87 88 dataSource := addr.Mode == config.DataResourceMode 89 90 // We create "destroy" actions for data resources so we can clean 91 // up their entries in state, but this is an implementation detail 92 // that users shouldn't see. 93 if dataSource && r.ChangeType() == terraform.DiffDestroy { 94 continue 95 } 96 97 did := &InstanceDiff{ 98 Addr: addr, 99 Action: r.ChangeType(), 100 Tainted: r.DestroyTainted, 101 Deposed: r.DestroyDeposed, 102 } 103 104 if dataSource && did.Action == terraform.DiffCreate { 105 // Use "refresh" as the action for display, since core 106 // currently uses Create for this. 107 did.Action = terraform.DiffRefresh 108 } 109 110 ret.Resources = append(ret.Resources, did) 111 112 if did.Action == terraform.DiffDestroy { 113 // Don't show any outputs for destroy actions 114 continue 115 } 116 117 for k, a := range r.Attributes { 118 var action terraform.DiffChangeType 119 switch { 120 case a.NewRemoved: 121 action = terraform.DiffDestroy 122 case did.Action == terraform.DiffCreate: 123 action = terraform.DiffCreate 124 default: 125 action = terraform.DiffUpdate 126 } 127 128 did.Attributes = append(did.Attributes, &AttributeDiff{ 129 Path: k, 130 Action: action, 131 132 OldValue: a.Old, 133 NewValue: a.New, 134 135 Sensitive: a.Sensitive, 136 ForcesNew: a.RequiresNew, 137 NewComputed: a.NewComputed, 138 }) 139 } 140 141 // Sort the attributes by their paths for display 142 sort.Slice(did.Attributes, func(i, j int) bool { 143 iPath := did.Attributes[i].Path 144 jPath := did.Attributes[j].Path 145 146 // as a special case, "id" is always first 147 switch { 148 case iPath != jPath && (iPath == "id" || jPath == "id"): 149 return iPath == "id" 150 default: 151 return iPath < jPath 152 } 153 }) 154 155 } 156 } 157 158 // Sort the instance diffs by their addresses for display. 159 sort.Slice(ret.Resources, func(i, j int) bool { 160 iAddr := ret.Resources[i].Addr 161 jAddr := ret.Resources[j].Addr 162 return iAddr.Less(jAddr) 163 }) 164 165 return ret 166 } 167 168 // Format produces and returns a text representation of the receiving plan 169 // intended for display in a terminal. 170 // 171 // If color is not nil, it is used to colorize the output. 172 func (p *Plan) Format(color *colorstring.Colorize) string { 173 if p.Empty() { 174 return "This plan does nothing." 175 } 176 177 if color == nil { 178 color = &colorstring.Colorize{ 179 Colors: colorstring.DefaultColors, 180 Reset: false, 181 } 182 } 183 184 // Find the longest path length of all the paths that are changing, 185 // so we can align them all. 186 keyLen := 0 187 for _, r := range p.Resources { 188 for _, attr := range r.Attributes { 189 key := attr.Path 190 191 if len(key) > keyLen { 192 keyLen = len(key) 193 } 194 } 195 } 196 197 buf := new(bytes.Buffer) 198 for _, r := range p.Resources { 199 formatPlanInstanceDiff(buf, r, keyLen, color) 200 } 201 202 return strings.TrimSpace(buf.String()) 203 } 204 205 // Stats returns statistics about the plan 206 func (p *Plan) Stats() PlanStats { 207 var ret PlanStats 208 for _, r := range p.Resources { 209 switch r.Action { 210 case terraform.DiffCreate: 211 ret.ToAdd++ 212 case terraform.DiffUpdate: 213 ret.ToChange++ 214 case terraform.DiffDestroyCreate: 215 ret.ToAdd++ 216 ret.ToDestroy++ 217 case terraform.DiffDestroy: 218 ret.ToDestroy++ 219 } 220 } 221 return ret 222 } 223 224 // ActionCounts returns the number of diffs for each action type 225 func (p *Plan) ActionCounts() map[terraform.DiffChangeType]int { 226 ret := map[terraform.DiffChangeType]int{} 227 for _, r := range p.Resources { 228 ret[r.Action]++ 229 } 230 return ret 231 } 232 233 // Empty returns true if there is at least one resource diff in the receiving plan. 234 func (p *Plan) Empty() bool { 235 return len(p.Resources) == 0 236 } 237 238 // DiffActionSymbol returns a string that, once passed through a 239 // colorstring.Colorize, will produce a result that can be written 240 // to a terminal to produce a symbol made of three printable 241 // characters, possibly interspersed with VT100 color codes. 242 func DiffActionSymbol(action terraform.DiffChangeType) string { 243 switch action { 244 case terraform.DiffDestroyCreate: 245 return "[red]-[reset]/[green]+[reset]" 246 case terraform.DiffCreate: 247 return " [green]+[reset]" 248 case terraform.DiffDestroy: 249 return " [red]-[reset]" 250 case terraform.DiffRefresh: 251 return " [cyan]<=[reset]" 252 default: 253 return " [yellow]~[reset]" 254 } 255 } 256 257 // formatPlanInstanceDiff writes the text representation of the given instance diff 258 // to the given buffer, using the given colorizer. 259 func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colorizer *colorstring.Colorize) { 260 addrStr := r.Addr.String() 261 262 // Determine the color for the text (green for adding, yellow 263 // for change, red for delete), and symbol, and output the 264 // resource header. 265 color := "yellow" 266 symbol := DiffActionSymbol(r.Action) 267 oldValues := true 268 switch r.Action { 269 case terraform.DiffDestroyCreate: 270 color = "yellow" 271 case terraform.DiffCreate: 272 color = "green" 273 oldValues = false 274 case terraform.DiffDestroy: 275 color = "red" 276 case terraform.DiffRefresh: 277 color = "cyan" 278 oldValues = false 279 } 280 281 var extraStr string 282 if r.Tainted { 283 extraStr = extraStr + " (tainted)" 284 } 285 if r.Deposed { 286 extraStr = extraStr + " (deposed)" 287 } 288 if r.Action == terraform.DiffDestroyCreate { 289 extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)") 290 } 291 292 buf.WriteString( 293 colorizer.Color(fmt.Sprintf( 294 "[%s]%s [%s]%s%s\n", 295 color, symbol, color, addrStr, extraStr, 296 )), 297 ) 298 299 for _, attr := range r.Attributes { 300 301 v := attr.NewValue 302 var dispV string 303 switch { 304 case v == "" && attr.NewComputed: 305 dispV = "<computed>" 306 case attr.Sensitive: 307 dispV = "<sensitive>" 308 default: 309 dispV = fmt.Sprintf("%q", v) 310 } 311 312 updateMsg := "" 313 switch { 314 case attr.ForcesNew && r.Action == terraform.DiffDestroyCreate: 315 updateMsg = colorizer.Color(" [red](forces new resource)") 316 case attr.Sensitive && oldValues: 317 updateMsg = colorizer.Color(" [yellow](attribute changed)") 318 } 319 320 if oldValues { 321 u := attr.OldValue 322 var dispU string 323 switch { 324 case attr.Sensitive: 325 dispU = "<sensitive>" 326 default: 327 dispU = fmt.Sprintf("%q", u) 328 } 329 buf.WriteString(fmt.Sprintf( 330 " %s:%s %s => %s%s\n", 331 attr.Path, 332 strings.Repeat(" ", keyLen-len(attr.Path)), 333 dispU, dispV, 334 updateMsg, 335 )) 336 } else { 337 buf.WriteString(fmt.Sprintf( 338 " %s:%s %s%s\n", 339 attr.Path, 340 strings.Repeat(" ", keyLen-len(attr.Path)), 341 dispV, 342 updateMsg, 343 )) 344 } 345 } 346 347 // Write the reset color so we don't bleed color into later text 348 buf.WriteString(colorizer.Color("[reset]\n")) 349 }