github.com/cgmcandrews/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  }