github.com/ljosa/terraform@v0.7.0-rc2.0.20160617205345-fe540b408f59/command/format_plan.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/hashicorp/terraform/terraform" 10 "github.com/mitchellh/colorstring" 11 ) 12 13 // FormatPlanOpts are the options for formatting a plan. 14 type FormatPlanOpts struct { 15 // Plan is the plan to format. This is required. 16 Plan *terraform.Plan 17 18 // Color is the colorizer. This is optional. 19 Color *colorstring.Colorize 20 21 // ModuleDepth is the depth of the modules to expand. By default this 22 // is zero which will not expand modules at all. 23 ModuleDepth int 24 } 25 26 // FormatPlan takes a plan and returns a 27 func FormatPlan(opts *FormatPlanOpts) string { 28 p := opts.Plan 29 if p.Diff == nil || p.Diff.Empty() { 30 return "This plan does nothing." 31 } 32 33 if opts.Color == nil { 34 opts.Color = &colorstring.Colorize{ 35 Colors: colorstring.DefaultColors, 36 Reset: false, 37 } 38 } 39 40 buf := new(bytes.Buffer) 41 for _, m := range p.Diff.Modules { 42 if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 { 43 formatPlanModuleExpand(buf, m, opts) 44 } else { 45 formatPlanModuleSingle(buf, m, opts) 46 } 47 } 48 49 return strings.TrimSpace(buf.String()) 50 } 51 52 // formatPlanModuleExpand will output the given module and all of its 53 // resources. 54 func formatPlanModuleExpand( 55 buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) { 56 // Ignore empty diffs 57 if m.Empty() { 58 return 59 } 60 61 var moduleName string 62 if !m.IsRoot() { 63 moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) 64 } 65 66 // We want to output the resources in sorted order to make things 67 // easier to scan through, so get all the resource names and sort them. 68 names := make([]string, 0, len(m.Resources)) 69 for name, _ := range m.Resources { 70 names = append(names, name) 71 } 72 sort.Strings(names) 73 74 // Go through each sorted name and start building the output 75 for _, name := range names { 76 rdiff := m.Resources[name] 77 if rdiff.Empty() { 78 continue 79 } 80 81 if moduleName != "" { 82 name = moduleName + "." + name 83 } 84 85 // Determine the color for the text (green for adding, yellow 86 // for change, red for delete), and symbol, and output the 87 // resource header. 88 color := "yellow" 89 symbol := "~" 90 oldValues := true 91 switch rdiff.ChangeType() { 92 case terraform.DiffDestroyCreate: 93 color = "green" 94 symbol = "-/+" 95 case terraform.DiffCreate: 96 color = "green" 97 symbol = "+" 98 oldValues = false 99 100 // If we're "creating" a data resource then we'll present it 101 // to the user as a "read" operation, so it's clear that this 102 // operation won't change anything outside of the Terraform state. 103 // Unfortunately by the time we get here we only have the name 104 // to work with, so we need to cheat and exploit knowledge of the 105 // naming scheme for data resources. 106 if strings.HasPrefix(name, "data.") { 107 symbol = "<=" 108 color = "cyan" 109 } 110 case terraform.DiffDestroy: 111 color = "red" 112 symbol = "-" 113 } 114 115 taintStr := "" 116 if rdiff.DestroyTainted { 117 taintStr = " (tainted)" 118 } 119 120 buf.WriteString(opts.Color.Color(fmt.Sprintf( 121 "[%s]%s %s%s\n", 122 color, symbol, name, taintStr))) 123 124 // Get all the attributes that are changing, and sort them. Also 125 // determine the longest key so that we can align them all. 126 keyLen := 0 127 keys := make([]string, 0, len(rdiff.Attributes)) 128 for key, _ := range rdiff.Attributes { 129 // Skip the ID since we do that specially 130 if key == "id" { 131 continue 132 } 133 134 keys = append(keys, key) 135 if len(key) > keyLen { 136 keyLen = len(key) 137 } 138 } 139 sort.Strings(keys) 140 141 // Go through and output each attribute 142 for _, attrK := range keys { 143 attrDiff := rdiff.Attributes[attrK] 144 145 v := attrDiff.New 146 if attrDiff.NewComputed { 147 v = "<computed>" 148 } 149 150 if attrDiff.Sensitive { 151 v = "<sensitive>" 152 } 153 154 updateMsg := "" 155 if attrDiff.RequiresNew && rdiff.Destroy { 156 updateMsg = opts.Color.Color(" [red](forces new resource)") 157 } else if attrDiff.Sensitive && oldValues { 158 updateMsg = opts.Color.Color(" [yellow](attribute changed)") 159 } 160 161 if oldValues { 162 var u string 163 if attrDiff.Sensitive { 164 u = "<sensitive>" 165 } else { 166 u = attrDiff.Old 167 } 168 buf.WriteString(fmt.Sprintf( 169 " %s:%s %#v => %#v%s\n", 170 attrK, 171 strings.Repeat(" ", keyLen-len(attrK)), 172 u, 173 v, 174 updateMsg)) 175 } else { 176 buf.WriteString(fmt.Sprintf( 177 " %s:%s %#v%s\n", 178 attrK, 179 strings.Repeat(" ", keyLen-len(attrK)), 180 v, 181 updateMsg)) 182 } 183 } 184 185 // Write the reset color so we don't overload the user's terminal 186 buf.WriteString(opts.Color.Color("[reset]\n")) 187 } 188 } 189 190 // formatPlanModuleSingle will output the given module and all of its 191 // resources. 192 func formatPlanModuleSingle( 193 buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) { 194 // Ignore empty diffs 195 if m.Empty() { 196 return 197 } 198 199 moduleName := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) 200 201 // Determine the color for the text (green for adding, yellow 202 // for change, red for delete), and symbol, and output the 203 // resource header. 204 color := "yellow" 205 symbol := "~" 206 switch m.ChangeType() { 207 case terraform.DiffCreate: 208 color = "green" 209 symbol = "+" 210 case terraform.DiffDestroy: 211 color = "red" 212 symbol = "-" 213 } 214 215 buf.WriteString(opts.Color.Color(fmt.Sprintf( 216 "[%s]%s %s\n", 217 color, symbol, moduleName))) 218 buf.WriteString(fmt.Sprintf( 219 " %d resource(s)", 220 len(m.Resources))) 221 buf.WriteString(opts.Color.Color("[reset]\n")) 222 }