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