github.com/rhenning/terraform@v0.8.0-beta2/terraform/diff.go (about)

     1  package terraform
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"reflect"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/mitchellh/copystructure"
    14  )
    15  
    16  // DiffChangeType is an enum with the kind of changes a diff has planned.
    17  type DiffChangeType byte
    18  
    19  const (
    20  	DiffInvalid DiffChangeType = iota
    21  	DiffNone
    22  	DiffCreate
    23  	DiffUpdate
    24  	DiffDestroy
    25  	DiffDestroyCreate
    26  )
    27  
    28  // Diff trackes the changes that are necessary to apply a configuration
    29  // to an existing infrastructure.
    30  type Diff struct {
    31  	// Modules contains all the modules that have a diff
    32  	Modules []*ModuleDiff
    33  }
    34  
    35  // Prune cleans out unused structures in the diff without affecting
    36  // the behavior of the diff at all.
    37  //
    38  // This is not safe to call concurrently. This is safe to call on a
    39  // nil Diff.
    40  func (d *Diff) Prune() {
    41  	if d == nil {
    42  		return
    43  	}
    44  
    45  	// Prune all empty modules
    46  	newModules := make([]*ModuleDiff, 0, len(d.Modules))
    47  	for _, m := range d.Modules {
    48  		// If the module isn't empty, we keep it
    49  		if !m.Empty() {
    50  			newModules = append(newModules, m)
    51  		}
    52  	}
    53  	if len(newModules) == 0 {
    54  		newModules = nil
    55  	}
    56  	d.Modules = newModules
    57  }
    58  
    59  // AddModule adds the module with the given path to the diff.
    60  //
    61  // This should be the preferred method to add module diffs since it
    62  // allows us to optimize lookups later as well as control sorting.
    63  func (d *Diff) AddModule(path []string) *ModuleDiff {
    64  	m := &ModuleDiff{Path: path}
    65  	m.init()
    66  	d.Modules = append(d.Modules, m)
    67  	return m
    68  }
    69  
    70  // ModuleByPath is used to lookup the module diff for the given path.
    71  // This should be the preferred lookup mechanism as it allows for future
    72  // lookup optimizations.
    73  func (d *Diff) ModuleByPath(path []string) *ModuleDiff {
    74  	if d == nil {
    75  		return nil
    76  	}
    77  	for _, mod := range d.Modules {
    78  		if mod.Path == nil {
    79  			panic("missing module path")
    80  		}
    81  		if reflect.DeepEqual(mod.Path, path) {
    82  			return mod
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  // RootModule returns the ModuleState for the root module
    89  func (d *Diff) RootModule() *ModuleDiff {
    90  	root := d.ModuleByPath(rootModulePath)
    91  	if root == nil {
    92  		panic("missing root module")
    93  	}
    94  	return root
    95  }
    96  
    97  // Empty returns true if the diff has no changes.
    98  func (d *Diff) Empty() bool {
    99  	if d == nil {
   100  		return true
   101  	}
   102  
   103  	for _, m := range d.Modules {
   104  		if !m.Empty() {
   105  			return false
   106  		}
   107  	}
   108  
   109  	return true
   110  }
   111  
   112  // Equal compares two diffs for exact equality.
   113  //
   114  // This is different from the Same comparison that is supported which
   115  // checks for operation equality taking into account computed values. Equal
   116  // instead checks for exact equality.
   117  func (d *Diff) Equal(d2 *Diff) bool {
   118  	// If one is nil, they must both be nil
   119  	if d == nil || d2 == nil {
   120  		return d == d2
   121  	}
   122  
   123  	// Sort the modules
   124  	sort.Sort(moduleDiffSort(d.Modules))
   125  	sort.Sort(moduleDiffSort(d2.Modules))
   126  
   127  	// Copy since we have to modify the module destroy flag to false so
   128  	// we don't compare that. TODO: delete this when we get rid of the
   129  	// destroy flag on modules.
   130  	dCopy := d.DeepCopy()
   131  	d2Copy := d2.DeepCopy()
   132  	for _, m := range dCopy.Modules {
   133  		m.Destroy = false
   134  	}
   135  	for _, m := range d2Copy.Modules {
   136  		m.Destroy = false
   137  	}
   138  
   139  	// Use DeepEqual
   140  	return reflect.DeepEqual(dCopy, d2Copy)
   141  }
   142  
   143  // DeepCopy performs a deep copy of all parts of the Diff, making the
   144  // resulting Diff safe to use without modifying this one.
   145  func (d *Diff) DeepCopy() *Diff {
   146  	copy, err := copystructure.Config{Lock: true}.Copy(d)
   147  	if err != nil {
   148  		panic(err)
   149  	}
   150  
   151  	return copy.(*Diff)
   152  }
   153  
   154  func (d *Diff) String() string {
   155  	var buf bytes.Buffer
   156  
   157  	keys := make([]string, 0, len(d.Modules))
   158  	lookup := make(map[string]*ModuleDiff)
   159  	for _, m := range d.Modules {
   160  		key := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], "."))
   161  		keys = append(keys, key)
   162  		lookup[key] = m
   163  	}
   164  	sort.Strings(keys)
   165  
   166  	for _, key := range keys {
   167  		m := lookup[key]
   168  		mStr := m.String()
   169  
   170  		// If we're the root module, we just write the output directly.
   171  		if reflect.DeepEqual(m.Path, rootModulePath) {
   172  			buf.WriteString(mStr + "\n")
   173  			continue
   174  		}
   175  
   176  		buf.WriteString(fmt.Sprintf("%s:\n", key))
   177  
   178  		s := bufio.NewScanner(strings.NewReader(mStr))
   179  		for s.Scan() {
   180  			buf.WriteString(fmt.Sprintf("  %s\n", s.Text()))
   181  		}
   182  	}
   183  
   184  	return strings.TrimSpace(buf.String())
   185  }
   186  
   187  func (d *Diff) init() {
   188  	if d.Modules == nil {
   189  		rootDiff := &ModuleDiff{Path: rootModulePath}
   190  		d.Modules = []*ModuleDiff{rootDiff}
   191  	}
   192  	for _, m := range d.Modules {
   193  		m.init()
   194  	}
   195  }
   196  
   197  // ModuleDiff tracks the differences between resources to apply within
   198  // a single module.
   199  type ModuleDiff struct {
   200  	Path      []string
   201  	Resources map[string]*InstanceDiff
   202  	Destroy   bool // Set only by the destroy plan
   203  }
   204  
   205  func (d *ModuleDiff) init() {
   206  	if d.Resources == nil {
   207  		d.Resources = make(map[string]*InstanceDiff)
   208  	}
   209  	for _, r := range d.Resources {
   210  		r.init()
   211  	}
   212  }
   213  
   214  // ChangeType returns the type of changes that the diff for this
   215  // module includes.
   216  //
   217  // At a module level, this will only be DiffNone, DiffUpdate, DiffDestroy, or
   218  // DiffCreate. If an instance within the module has a DiffDestroyCreate
   219  // then this will register as a DiffCreate for a module.
   220  func (d *ModuleDiff) ChangeType() DiffChangeType {
   221  	result := DiffNone
   222  	for _, r := range d.Resources {
   223  		change := r.ChangeType()
   224  		switch change {
   225  		case DiffCreate, DiffDestroy:
   226  			if result == DiffNone {
   227  				result = change
   228  			}
   229  		case DiffDestroyCreate, DiffUpdate:
   230  			result = DiffUpdate
   231  		}
   232  	}
   233  
   234  	return result
   235  }
   236  
   237  // Empty returns true if the diff has no changes within this module.
   238  func (d *ModuleDiff) Empty() bool {
   239  	if d.Destroy {
   240  		return false
   241  	}
   242  
   243  	if len(d.Resources) == 0 {
   244  		return true
   245  	}
   246  
   247  	for _, rd := range d.Resources {
   248  		if !rd.Empty() {
   249  			return false
   250  		}
   251  	}
   252  
   253  	return true
   254  }
   255  
   256  // Instances returns the instance diffs for the id given. This can return
   257  // multiple instance diffs if there are counts within the resource.
   258  func (d *ModuleDiff) Instances(id string) []*InstanceDiff {
   259  	var result []*InstanceDiff
   260  	for k, diff := range d.Resources {
   261  		if k == id || strings.HasPrefix(k, id+".") {
   262  			if !diff.Empty() {
   263  				result = append(result, diff)
   264  			}
   265  		}
   266  	}
   267  
   268  	return result
   269  }
   270  
   271  // IsRoot says whether or not this module diff is for the root module.
   272  func (d *ModuleDiff) IsRoot() bool {
   273  	return reflect.DeepEqual(d.Path, rootModulePath)
   274  }
   275  
   276  // String outputs the diff in a long but command-line friendly output
   277  // format that users can read to quickly inspect a diff.
   278  func (d *ModuleDiff) String() string {
   279  	var buf bytes.Buffer
   280  
   281  	names := make([]string, 0, len(d.Resources))
   282  	for name, _ := range d.Resources {
   283  		names = append(names, name)
   284  	}
   285  	sort.Strings(names)
   286  
   287  	for _, name := range names {
   288  		rdiff := d.Resources[name]
   289  
   290  		crud := "UPDATE"
   291  		switch {
   292  		case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()):
   293  			crud = "DESTROY/CREATE"
   294  		case rdiff.GetDestroy():
   295  			crud = "DESTROY"
   296  		case rdiff.RequiresNew():
   297  			crud = "CREATE"
   298  		}
   299  
   300  		buf.WriteString(fmt.Sprintf(
   301  			"%s: %s\n",
   302  			crud,
   303  			name))
   304  
   305  		keyLen := 0
   306  		rdiffAttrs := rdiff.CopyAttributes()
   307  		keys := make([]string, 0, len(rdiffAttrs))
   308  		for key, _ := range rdiffAttrs {
   309  			if key == "id" {
   310  				continue
   311  			}
   312  
   313  			keys = append(keys, key)
   314  			if len(key) > keyLen {
   315  				keyLen = len(key)
   316  			}
   317  		}
   318  		sort.Strings(keys)
   319  
   320  		for _, attrK := range keys {
   321  			attrDiff, _ := rdiff.GetAttribute(attrK)
   322  
   323  			v := attrDiff.New
   324  			u := attrDiff.Old
   325  			if attrDiff.NewComputed {
   326  				v = "<computed>"
   327  			}
   328  
   329  			if attrDiff.Sensitive {
   330  				u = "<sensitive>"
   331  				v = "<sensitive>"
   332  			}
   333  
   334  			updateMsg := ""
   335  			if attrDiff.RequiresNew {
   336  				updateMsg = " (forces new resource)"
   337  			} else if attrDiff.Sensitive {
   338  				updateMsg = " (attribute changed)"
   339  			}
   340  
   341  			buf.WriteString(fmt.Sprintf(
   342  				"  %s:%s %#v => %#v%s\n",
   343  				attrK,
   344  				strings.Repeat(" ", keyLen-len(attrK)),
   345  				u,
   346  				v,
   347  				updateMsg))
   348  		}
   349  	}
   350  
   351  	return buf.String()
   352  }
   353  
   354  // InstanceDiff is the diff of a resource from some state to another.
   355  type InstanceDiff struct {
   356  	mu             sync.Mutex
   357  	Attributes     map[string]*ResourceAttrDiff
   358  	Destroy        bool
   359  	DestroyTainted bool
   360  }
   361  
   362  func (d *InstanceDiff) Lock()   { d.mu.Lock() }
   363  func (d *InstanceDiff) Unlock() { d.mu.Unlock() }
   364  
   365  // ResourceAttrDiff is the diff of a single attribute of a resource.
   366  type ResourceAttrDiff struct {
   367  	Old         string      // Old Value
   368  	New         string      // New Value
   369  	NewComputed bool        // True if new value is computed (unknown currently)
   370  	NewRemoved  bool        // True if this attribute is being removed
   371  	NewExtra    interface{} // Extra information for the provider
   372  	RequiresNew bool        // True if change requires new resource
   373  	Sensitive   bool        // True if the data should not be displayed in UI output
   374  	Type        DiffAttrType
   375  }
   376  
   377  // Empty returns true if the diff for this attr is neutral
   378  func (d *ResourceAttrDiff) Empty() bool {
   379  	return d.Old == d.New && !d.NewComputed && !d.NewRemoved
   380  }
   381  
   382  func (d *ResourceAttrDiff) GoString() string {
   383  	return fmt.Sprintf("*%#v", *d)
   384  }
   385  
   386  // DiffAttrType is an enum type that says whether a resource attribute
   387  // diff is an input attribute (comes from the configuration) or an
   388  // output attribute (comes as a result of applying the configuration). An
   389  // example input would be "ami" for AWS and an example output would be
   390  // "private_ip".
   391  type DiffAttrType byte
   392  
   393  const (
   394  	DiffAttrUnknown DiffAttrType = iota
   395  	DiffAttrInput
   396  	DiffAttrOutput
   397  )
   398  
   399  func (d *InstanceDiff) init() {
   400  	if d.Attributes == nil {
   401  		d.Attributes = make(map[string]*ResourceAttrDiff)
   402  	}
   403  }
   404  
   405  func NewInstanceDiff() *InstanceDiff {
   406  	return &InstanceDiff{Attributes: make(map[string]*ResourceAttrDiff)}
   407  }
   408  
   409  func (d *InstanceDiff) Copy() (*InstanceDiff, error) {
   410  	if d == nil {
   411  		return nil, nil
   412  	}
   413  
   414  	dCopy, err := copystructure.Config{Lock: true}.Copy(d)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	return dCopy.(*InstanceDiff), nil
   420  }
   421  
   422  // ChangeType returns the DiffChangeType represented by the diff
   423  // for this single instance.
   424  func (d *InstanceDiff) ChangeType() DiffChangeType {
   425  	if d.Empty() {
   426  		return DiffNone
   427  	}
   428  
   429  	if d.RequiresNew() && (d.GetDestroy() || d.GetDestroyTainted()) {
   430  		return DiffDestroyCreate
   431  	}
   432  
   433  	if d.GetDestroy() {
   434  		return DiffDestroy
   435  	}
   436  
   437  	if d.RequiresNew() {
   438  		return DiffCreate
   439  	}
   440  
   441  	return DiffUpdate
   442  }
   443  
   444  // Empty returns true if this diff encapsulates no changes.
   445  func (d *InstanceDiff) Empty() bool {
   446  	if d == nil {
   447  		return true
   448  	}
   449  
   450  	d.mu.Lock()
   451  	defer d.mu.Unlock()
   452  	return !d.Destroy && !d.DestroyTainted && len(d.Attributes) == 0
   453  }
   454  
   455  // Equal compares two diffs for exact equality.
   456  //
   457  // This is different from the Same comparison that is supported which
   458  // checks for operation equality taking into account computed values. Equal
   459  // instead checks for exact equality.
   460  func (d *InstanceDiff) Equal(d2 *InstanceDiff) bool {
   461  	// If one is nil, they must both be nil
   462  	if d == nil || d2 == nil {
   463  		return d == d2
   464  	}
   465  
   466  	// Use DeepEqual
   467  	return reflect.DeepEqual(d, d2)
   468  }
   469  
   470  // DeepCopy performs a deep copy of all parts of the InstanceDiff
   471  func (d *InstanceDiff) DeepCopy() *InstanceDiff {
   472  	copy, err := copystructure.Config{Lock: true}.Copy(d)
   473  	if err != nil {
   474  		panic(err)
   475  	}
   476  
   477  	return copy.(*InstanceDiff)
   478  }
   479  
   480  func (d *InstanceDiff) GoString() string {
   481  	return fmt.Sprintf("*%#v", InstanceDiff{
   482  		Attributes:     d.Attributes,
   483  		Destroy:        d.Destroy,
   484  		DestroyTainted: d.DestroyTainted,
   485  	})
   486  }
   487  
   488  // RequiresNew returns true if the diff requires the creation of a new
   489  // resource (implying the destruction of the old).
   490  func (d *InstanceDiff) RequiresNew() bool {
   491  	if d == nil {
   492  		return false
   493  	}
   494  
   495  	d.mu.Lock()
   496  	defer d.mu.Unlock()
   497  
   498  	return d.requiresNew()
   499  }
   500  
   501  func (d *InstanceDiff) requiresNew() bool {
   502  	if d == nil {
   503  		return false
   504  	}
   505  
   506  	if d.DestroyTainted {
   507  		return true
   508  	}
   509  
   510  	for _, rd := range d.Attributes {
   511  		if rd != nil && rd.RequiresNew {
   512  			return true
   513  		}
   514  	}
   515  
   516  	return false
   517  }
   518  
   519  // These methods are properly locked, for use outside other InstanceDiff
   520  // methods but everywhere else within in the terraform package.
   521  // TODO refactor the locking scheme
   522  func (d *InstanceDiff) SetTainted(b bool) {
   523  	d.mu.Lock()
   524  	defer d.mu.Unlock()
   525  
   526  	d.DestroyTainted = b
   527  }
   528  
   529  func (d *InstanceDiff) GetDestroyTainted() bool {
   530  	d.mu.Lock()
   531  	defer d.mu.Unlock()
   532  
   533  	return d.DestroyTainted
   534  }
   535  
   536  func (d *InstanceDiff) SetDestroy(b bool) {
   537  	d.mu.Lock()
   538  	defer d.mu.Unlock()
   539  
   540  	d.Destroy = b
   541  }
   542  
   543  func (d *InstanceDiff) GetDestroy() bool {
   544  	d.mu.Lock()
   545  	defer d.mu.Unlock()
   546  
   547  	return d.Destroy
   548  }
   549  
   550  func (d *InstanceDiff) SetAttribute(key string, attr *ResourceAttrDiff) {
   551  	d.mu.Lock()
   552  	defer d.mu.Unlock()
   553  
   554  	d.Attributes[key] = attr
   555  }
   556  
   557  func (d *InstanceDiff) DelAttribute(key string) {
   558  	d.mu.Lock()
   559  	defer d.mu.Unlock()
   560  
   561  	delete(d.Attributes, key)
   562  }
   563  
   564  func (d *InstanceDiff) GetAttribute(key string) (*ResourceAttrDiff, bool) {
   565  	d.mu.Lock()
   566  	defer d.mu.Unlock()
   567  
   568  	attr, ok := d.Attributes[key]
   569  	return attr, ok
   570  }
   571  func (d *InstanceDiff) GetAttributesLen() int {
   572  	d.mu.Lock()
   573  	defer d.mu.Unlock()
   574  
   575  	return len(d.Attributes)
   576  }
   577  
   578  // Safely copies the Attributes map
   579  func (d *InstanceDiff) CopyAttributes() map[string]*ResourceAttrDiff {
   580  	d.mu.Lock()
   581  	defer d.mu.Unlock()
   582  
   583  	attrs := make(map[string]*ResourceAttrDiff)
   584  	for k, v := range d.Attributes {
   585  		attrs[k] = v
   586  	}
   587  
   588  	return attrs
   589  }
   590  
   591  // Same checks whether or not two InstanceDiff's are the "same". When
   592  // we say "same", it is not necessarily exactly equal. Instead, it is
   593  // just checking that the same attributes are changing, a destroy
   594  // isn't suddenly happening, etc.
   595  func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) {
   596  	// we can safely compare the pointers without a lock
   597  	switch {
   598  	case d == nil && d2 == nil:
   599  		return true, ""
   600  	case d == nil || d2 == nil:
   601  		return false, "one nil"
   602  	case d == d2:
   603  		return true, ""
   604  	}
   605  
   606  	d.mu.Lock()
   607  	defer d.mu.Unlock()
   608  
   609  	// If we're going from requiring new to NOT requiring new, then we have
   610  	// to see if all required news were computed. If so, it is allowed since
   611  	// computed may also mean "same value and therefore not new".
   612  	oldNew := d.requiresNew()
   613  	newNew := d2.RequiresNew()
   614  	if oldNew && !newNew {
   615  		oldNew = false
   616  		for _, rd := range d.Attributes {
   617  			// If the field is requires new and NOT computed, then what
   618  			// we have is a diff mismatch for sure. We set that the old
   619  			// diff does REQUIRE a ForceNew.
   620  			if rd != nil && rd.RequiresNew && !rd.NewComputed {
   621  				oldNew = true
   622  				break
   623  			}
   624  		}
   625  	}
   626  
   627  	if oldNew != newNew {
   628  		return false, fmt.Sprintf(
   629  			"diff RequiresNew; old: %t, new: %t", oldNew, newNew)
   630  	}
   631  
   632  	// Verify that destroy matches. The second boolean here allows us to
   633  	// have mismatching Destroy if we're moving from RequiresNew true
   634  	// to false above. Therefore, the second boolean will only pass if
   635  	// we're moving from Destroy: true to false as well.
   636  	if d.Destroy != d2.GetDestroy() && d.requiresNew() == oldNew {
   637  		return false, fmt.Sprintf(
   638  			"diff: Destroy; old: %t, new: %t", d.Destroy, d2.GetDestroy())
   639  	}
   640  
   641  	// Go through the old diff and make sure the new diff has all the
   642  	// same attributes. To start, build up the check map to be all the keys.
   643  	checkOld := make(map[string]struct{})
   644  	checkNew := make(map[string]struct{})
   645  	for k, _ := range d.Attributes {
   646  		checkOld[k] = struct{}{}
   647  	}
   648  	for k, _ := range d2.CopyAttributes() {
   649  		checkNew[k] = struct{}{}
   650  	}
   651  
   652  	// Make an ordered list so we are sure the approximated hashes are left
   653  	// to process at the end of the loop
   654  	keys := make([]string, 0, len(d.Attributes))
   655  	for k, _ := range d.Attributes {
   656  		keys = append(keys, k)
   657  	}
   658  	sort.StringSlice(keys).Sort()
   659  
   660  	for _, k := range keys {
   661  		diffOld := d.Attributes[k]
   662  
   663  		if _, ok := checkOld[k]; !ok {
   664  			// We're not checking this key for whatever reason (see where
   665  			// check is modified).
   666  			continue
   667  		}
   668  
   669  		// Remove this key since we'll never hit it again
   670  		delete(checkOld, k)
   671  		delete(checkNew, k)
   672  
   673  		_, ok := d2.GetAttribute(k)
   674  		if !ok {
   675  			// If there's no new attribute, and the old diff expected the attribute
   676  			// to be removed, that's just fine.
   677  			if diffOld.NewRemoved {
   678  				continue
   679  			}
   680  
   681  			// If the last diff was a computed value then the absense of
   682  			// that value is allowed since it may mean the value ended up
   683  			// being the same.
   684  			if diffOld.NewComputed {
   685  				ok = true
   686  			}
   687  
   688  			// No exact match, but maybe this is a set containing computed
   689  			// values. So check if there is an approximate hash in the key
   690  			// and if so, try to match the key.
   691  			if strings.Contains(k, "~") {
   692  				parts := strings.Split(k, ".")
   693  				parts2 := append([]string(nil), parts...)
   694  
   695  				re := regexp.MustCompile(`^~\d+$`)
   696  				for i, part := range parts {
   697  					if re.MatchString(part) {
   698  						// we're going to consider this the base of a
   699  						// computed hash, and remove all longer matching fields
   700  						ok = true
   701  
   702  						parts2[i] = `\d+`
   703  						parts2 = parts2[:i+1]
   704  						break
   705  					}
   706  				}
   707  
   708  				re, err := regexp.Compile("^" + strings.Join(parts2, `\.`))
   709  				if err != nil {
   710  					return false, fmt.Sprintf("regexp failed to compile; err: %#v", err)
   711  				}
   712  
   713  				for k2, _ := range checkNew {
   714  					if re.MatchString(k2) {
   715  						delete(checkNew, k2)
   716  					}
   717  				}
   718  			}
   719  
   720  			// This is a little tricky, but when a diff contains a computed
   721  			// list, set, or map that can only be interpolated after the apply
   722  			// command has created the dependent resources, it could turn out
   723  			// that the result is actually the same as the existing state which
   724  			// would remove the key from the diff.
   725  			if diffOld.NewComputed && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) {
   726  				ok = true
   727  			}
   728  
   729  			// Similarly, in a RequiresNew scenario, a list that shows up in the plan
   730  			// diff can disappear from the apply diff, which is calculated from an
   731  			// empty state.
   732  			if d.requiresNew() && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) {
   733  				ok = true
   734  			}
   735  
   736  			if !ok {
   737  				return false, fmt.Sprintf("attribute mismatch: %s", k)
   738  			}
   739  		}
   740  
   741  		// search for the suffix of the base of a [computed] map, list or set.
   742  		multiVal := regexp.MustCompile(`\.(#|~#|%)$`)
   743  		match := multiVal.FindStringSubmatch(k)
   744  
   745  		if diffOld.NewComputed && len(match) == 2 {
   746  			matchLen := len(match[1])
   747  
   748  			// This is a computed list, set, or map, so remove any keys with
   749  			// this prefix from the check list.
   750  			kprefix := k[:len(k)-matchLen]
   751  			for k2, _ := range checkOld {
   752  				if strings.HasPrefix(k2, kprefix) {
   753  					delete(checkOld, k2)
   754  				}
   755  			}
   756  			for k2, _ := range checkNew {
   757  				if strings.HasPrefix(k2, kprefix) {
   758  					delete(checkNew, k2)
   759  				}
   760  			}
   761  		}
   762  
   763  		// TODO: check for the same value if not computed
   764  	}
   765  
   766  	// Check for leftover attributes
   767  	if len(checkNew) > 0 {
   768  		extras := make([]string, 0, len(checkNew))
   769  		for attr, _ := range checkNew {
   770  			extras = append(extras, attr)
   771  		}
   772  		return false,
   773  			fmt.Sprintf("extra attributes: %s", strings.Join(extras, ", "))
   774  	}
   775  
   776  	return true, ""
   777  }
   778  
   779  // moduleDiffSort implements sort.Interface to sort module diffs by path.
   780  type moduleDiffSort []*ModuleDiff
   781  
   782  func (s moduleDiffSort) Len() int      { return len(s) }
   783  func (s moduleDiffSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   784  func (s moduleDiffSort) Less(i, j int) bool {
   785  	a := s[i]
   786  	b := s[j]
   787  
   788  	// If the lengths are different, then the shorter one always wins
   789  	if len(a.Path) != len(b.Path) {
   790  		return len(a.Path) < len(b.Path)
   791  	}
   792  
   793  	// Otherwise, compare lexically
   794  	return strings.Join(a.Path, ".") < strings.Join(b.Path, ".")
   795  }