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