github.com/hernad/nomad@v1.6.112/nomad/structs/diff.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package structs
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"reflect"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/hernad/nomad/helper/flatmap"
    15  	"github.com/mitchellh/hashstructure"
    16  )
    17  
    18  // DiffableWithID defines an object that has a unique and stable value that can
    19  // be used as an identifier when generating a diff.
    20  type DiffableWithID interface {
    21  	// DiffID returns the value to use to match entities between the old and
    22  	// the new input.
    23  	DiffID() string
    24  }
    25  
    26  // DiffType denotes the type of a diff object.
    27  type DiffType string
    28  
    29  var (
    30  	DiffTypeNone    DiffType = "None"
    31  	DiffTypeAdded   DiffType = "Added"
    32  	DiffTypeDeleted DiffType = "Deleted"
    33  	DiffTypeEdited  DiffType = "Edited"
    34  )
    35  
    36  func (d DiffType) Less(other DiffType) bool {
    37  	// Edited > Added > Deleted > None
    38  	// But we do a reverse sort
    39  	if d == other {
    40  		return false
    41  	}
    42  
    43  	if d == DiffTypeEdited {
    44  		return true
    45  	} else if other == DiffTypeEdited {
    46  		return false
    47  	} else if d == DiffTypeAdded {
    48  		return true
    49  	} else if other == DiffTypeAdded {
    50  		return false
    51  	} else if d == DiffTypeDeleted {
    52  		return true
    53  	} else if other == DiffTypeDeleted {
    54  		return false
    55  	}
    56  
    57  	return true
    58  }
    59  
    60  // JobDiff contains the diff of two jobs.
    61  type JobDiff struct {
    62  	Type       DiffType
    63  	ID         string
    64  	Fields     []*FieldDiff
    65  	Objects    []*ObjectDiff
    66  	TaskGroups []*TaskGroupDiff
    67  }
    68  
    69  // Diff returns a diff of two jobs and a potential error if the Jobs are not
    70  // diffable. If contextual diff is enabled, objects within the job will contain
    71  // field information even if unchanged.
    72  func (j *Job) Diff(other *Job, contextual bool) (*JobDiff, error) {
    73  	// See agent.ApiJobToStructJob Update is a default for TaskGroups
    74  	diff := &JobDiff{Type: DiffTypeNone}
    75  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
    76  	filter := []string{"ID", "Status", "StatusDescription", "Version", "Stable", "CreateIndex",
    77  		"ModifyIndex", "JobModifyIndex", "Update", "SubmitTime", "NomadTokenID", "VaultToken"}
    78  
    79  	if j == nil && other == nil {
    80  		return diff, nil
    81  	} else if j == nil {
    82  		j = &Job{}
    83  		diff.Type = DiffTypeAdded
    84  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
    85  		diff.ID = other.ID
    86  	} else if other == nil {
    87  		other = &Job{}
    88  		diff.Type = DiffTypeDeleted
    89  		oldPrimitiveFlat = flatmap.Flatten(j, filter, true)
    90  		diff.ID = j.ID
    91  	} else {
    92  		if j.ID != other.ID {
    93  			return nil, fmt.Errorf("can not diff jobs with different IDs: %q and %q", j.ID, other.ID)
    94  		}
    95  
    96  		oldPrimitiveFlat = flatmap.Flatten(j, filter, true)
    97  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
    98  		diff.ID = other.ID
    99  	}
   100  
   101  	// Diff the primitive fields.
   102  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
   103  
   104  	// Datacenters diff
   105  	if setDiff := stringSetDiff(j.Datacenters, other.Datacenters, "Datacenters", contextual); setDiff != nil && setDiff.Type != DiffTypeNone {
   106  		diff.Objects = append(diff.Objects, setDiff)
   107  	}
   108  
   109  	// Constraints diff
   110  	conDiff := primitiveObjectSetDiff(
   111  		interfaceSlice(j.Constraints),
   112  		interfaceSlice(other.Constraints),
   113  		[]string{"str"},
   114  		"Constraint",
   115  		contextual)
   116  	if conDiff != nil {
   117  		diff.Objects = append(diff.Objects, conDiff...)
   118  	}
   119  
   120  	// Affinities diff
   121  	affinitiesDiff := primitiveObjectSetDiff(
   122  		interfaceSlice(j.Affinities),
   123  		interfaceSlice(other.Affinities),
   124  		[]string{"str"},
   125  		"Affinity",
   126  		contextual)
   127  	if affinitiesDiff != nil {
   128  		diff.Objects = append(diff.Objects, affinitiesDiff...)
   129  	}
   130  
   131  	// Task groups diff
   132  	tgs, err := taskGroupDiffs(j.TaskGroups, other.TaskGroups, contextual)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	diff.TaskGroups = tgs
   137  
   138  	// Periodic diff
   139  	if pDiff := primitiveObjectDiff(j.Periodic, other.Periodic, nil, "Periodic", contextual); pDiff != nil {
   140  		diff.Objects = append(diff.Objects, pDiff)
   141  	}
   142  
   143  	// ParameterizedJob diff
   144  	if cDiff := parameterizedJobDiff(j.ParameterizedJob, other.ParameterizedJob, contextual); cDiff != nil {
   145  		diff.Objects = append(diff.Objects, cDiff)
   146  	}
   147  
   148  	// Multiregion diff
   149  	if mrDiff := multiregionDiff(j.Multiregion, other.Multiregion, contextual); mrDiff != nil {
   150  		diff.Objects = append(diff.Objects, mrDiff)
   151  	}
   152  
   153  	// Check to see if there is a diff. We don't use reflect because we are
   154  	// filtering quite a few fields that will change on each diff.
   155  	if diff.Type == DiffTypeNone {
   156  		for _, fd := range diff.Fields {
   157  			if fd.Type != DiffTypeNone {
   158  				diff.Type = DiffTypeEdited
   159  				break
   160  			}
   161  		}
   162  	}
   163  
   164  	if diff.Type == DiffTypeNone {
   165  		for _, od := range diff.Objects {
   166  			if od.Type != DiffTypeNone {
   167  				diff.Type = DiffTypeEdited
   168  				break
   169  			}
   170  		}
   171  	}
   172  
   173  	if diff.Type == DiffTypeNone {
   174  		for _, tg := range diff.TaskGroups {
   175  			if tg.Type != DiffTypeNone {
   176  				diff.Type = DiffTypeEdited
   177  				break
   178  			}
   179  		}
   180  	}
   181  
   182  	return diff, nil
   183  }
   184  
   185  func (j *JobDiff) GoString() string {
   186  	out := fmt.Sprintf("Job %q (%s):\n", j.ID, j.Type)
   187  
   188  	for _, f := range j.Fields {
   189  		out += fmt.Sprintf("%#v\n", f)
   190  	}
   191  
   192  	for _, o := range j.Objects {
   193  		out += fmt.Sprintf("%#v\n", o)
   194  	}
   195  
   196  	for _, tg := range j.TaskGroups {
   197  		out += fmt.Sprintf("%#v\n", tg)
   198  	}
   199  
   200  	return out
   201  }
   202  
   203  // TaskGroupDiff contains the diff of two task groups.
   204  type TaskGroupDiff struct {
   205  	Type    DiffType
   206  	Name    string
   207  	Fields  []*FieldDiff
   208  	Objects []*ObjectDiff
   209  	Tasks   []*TaskDiff
   210  	Updates map[string]uint64
   211  }
   212  
   213  // Diff returns a diff of two task groups. If contextual diff is enabled,
   214  // objects' fields will be stored even if no diff occurred as long as one field
   215  // changed.
   216  func (tg *TaskGroup) Diff(other *TaskGroup, contextual bool) (*TaskGroupDiff, error) {
   217  	diff := &TaskGroupDiff{Type: DiffTypeNone}
   218  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   219  	filter := []string{"Name"}
   220  
   221  	if tg == nil && other == nil {
   222  		return diff, nil
   223  	} else if tg == nil {
   224  		tg = &TaskGroup{}
   225  		diff.Type = DiffTypeAdded
   226  		diff.Name = other.Name
   227  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
   228  	} else if other == nil {
   229  		other = &TaskGroup{}
   230  		diff.Type = DiffTypeDeleted
   231  		diff.Name = tg.Name
   232  		oldPrimitiveFlat = flatmap.Flatten(tg, filter, true)
   233  	} else {
   234  		if !reflect.DeepEqual(tg, other) {
   235  			diff.Type = DiffTypeEdited
   236  		}
   237  		if tg.Name != other.Name {
   238  			return nil, fmt.Errorf("can not diff task groups with different names: %q and %q", tg.Name, other.Name)
   239  		}
   240  		diff.Name = other.Name
   241  		oldPrimitiveFlat = flatmap.Flatten(tg, filter, true)
   242  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
   243  	}
   244  
   245  	// ShutdownDelay diff
   246  	if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
   247  		if tg.ShutdownDelay == nil {
   248  			oldPrimitiveFlat["ShutdownDelay"] = ""
   249  		} else {
   250  			oldPrimitiveFlat["ShutdownDelay"] = fmt.Sprintf("%d", *tg.ShutdownDelay)
   251  		}
   252  		if other.ShutdownDelay == nil {
   253  			newPrimitiveFlat["ShutdownDelay"] = ""
   254  		} else {
   255  			newPrimitiveFlat["ShutdownDelay"] = fmt.Sprintf("%d", *other.ShutdownDelay)
   256  		}
   257  	}
   258  
   259  	// StopAfterClientDisconnect diff
   260  	if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
   261  		if tg.StopAfterClientDisconnect == nil {
   262  			oldPrimitiveFlat["StopAfterClientDisconnect"] = ""
   263  		} else {
   264  			oldPrimitiveFlat["StopAfterClientDisconnect"] = fmt.Sprintf("%d", *tg.StopAfterClientDisconnect)
   265  		}
   266  		if other.StopAfterClientDisconnect == nil {
   267  			newPrimitiveFlat["StopAfterClientDisconnect"] = ""
   268  		} else {
   269  			newPrimitiveFlat["StopAfterClientDisconnect"] = fmt.Sprintf("%d", *other.StopAfterClientDisconnect)
   270  		}
   271  	}
   272  
   273  	// MaxClientDisconnect diff
   274  	if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
   275  		if tg.MaxClientDisconnect == nil {
   276  			oldPrimitiveFlat["MaxClientDisconnect"] = ""
   277  		} else {
   278  			oldPrimitiveFlat["MaxClientDisconnect"] = fmt.Sprintf("%d", *tg.MaxClientDisconnect)
   279  		}
   280  		if other.MaxClientDisconnect == nil {
   281  			newPrimitiveFlat["MaxClientDisconnect"] = ""
   282  		} else {
   283  			newPrimitiveFlat["MaxClientDisconnect"] = fmt.Sprintf("%d", *other.MaxClientDisconnect)
   284  		}
   285  	}
   286  
   287  	// Diff the primitive fields.
   288  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
   289  
   290  	// Constraints diff
   291  	conDiff := primitiveObjectSetDiff(
   292  		interfaceSlice(tg.Constraints),
   293  		interfaceSlice(other.Constraints),
   294  		[]string{"str"},
   295  		"Constraint",
   296  		contextual)
   297  	if conDiff != nil {
   298  		diff.Objects = append(diff.Objects, conDiff...)
   299  	}
   300  
   301  	// Affinities diff
   302  	affinitiesDiff := primitiveObjectSetDiff(
   303  		interfaceSlice(tg.Affinities),
   304  		interfaceSlice(other.Affinities),
   305  		[]string{"str"},
   306  		"Affinity",
   307  		contextual)
   308  	if affinitiesDiff != nil {
   309  		diff.Objects = append(diff.Objects, affinitiesDiff...)
   310  	}
   311  
   312  	// Restart policy diff
   313  	rDiff := primitiveObjectDiff(tg.RestartPolicy, other.RestartPolicy, nil, "RestartPolicy", contextual)
   314  	if rDiff != nil {
   315  		diff.Objects = append(diff.Objects, rDiff)
   316  	}
   317  
   318  	// Reschedule policy diff
   319  	reschedDiff := primitiveObjectDiff(tg.ReschedulePolicy, other.ReschedulePolicy, nil, "ReschedulePolicy", contextual)
   320  	if reschedDiff != nil {
   321  		diff.Objects = append(diff.Objects, reschedDiff)
   322  	}
   323  
   324  	// EphemeralDisk diff
   325  	diskDiff := primitiveObjectDiff(tg.EphemeralDisk, other.EphemeralDisk, nil, "EphemeralDisk", contextual)
   326  	if diskDiff != nil {
   327  		diff.Objects = append(diff.Objects, diskDiff)
   328  	}
   329  
   330  	consulDiff := primitiveObjectDiff(tg.Consul, other.Consul, nil, "Consul", contextual)
   331  	if consulDiff != nil {
   332  		diff.Objects = append(diff.Objects, consulDiff)
   333  	}
   334  
   335  	// Update diff
   336  	// COMPAT: Remove "Stagger" in 0.7.0.
   337  	if uDiff := primitiveObjectDiff(tg.Update, other.Update, []string{"Stagger"}, "Update", contextual); uDiff != nil {
   338  		diff.Objects = append(diff.Objects, uDiff)
   339  	}
   340  
   341  	// Network Resources diff
   342  	if nDiffs := networkResourceDiffs(tg.Networks, other.Networks, contextual); nDiffs != nil {
   343  		diff.Objects = append(diff.Objects, nDiffs...)
   344  	}
   345  
   346  	// Services diff
   347  	if sDiffs := serviceDiffs(tg.Services, other.Services, contextual); sDiffs != nil {
   348  		diff.Objects = append(diff.Objects, sDiffs...)
   349  	}
   350  
   351  	// Volumes diff
   352  	if vDiffs := volumeDiffs(tg.Volumes, other.Volumes, contextual); vDiffs != nil {
   353  		diff.Objects = append(diff.Objects, vDiffs...)
   354  	}
   355  
   356  	// Tasks diff
   357  	tasks, err := taskDiffs(tg.Tasks, other.Tasks, contextual)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	diff.Tasks = tasks
   362  
   363  	return diff, nil
   364  }
   365  
   366  func (tg *TaskGroupDiff) GoString() string {
   367  	out := fmt.Sprintf("Group %q (%s):\n", tg.Name, tg.Type)
   368  
   369  	if len(tg.Updates) != 0 {
   370  		out += "Updates {\n"
   371  		for update, count := range tg.Updates {
   372  			out += fmt.Sprintf("%d %s\n", count, update)
   373  		}
   374  		out += "}\n"
   375  	}
   376  
   377  	for _, f := range tg.Fields {
   378  		out += fmt.Sprintf("%#v\n", f)
   379  	}
   380  
   381  	for _, o := range tg.Objects {
   382  		out += fmt.Sprintf("%#v\n", o)
   383  	}
   384  
   385  	for _, t := range tg.Tasks {
   386  		out += fmt.Sprintf("%#v\n", t)
   387  	}
   388  
   389  	return out
   390  }
   391  
   392  // TaskGroupDiffs diffs two sets of task groups. If contextual diff is enabled,
   393  // objects' fields will be stored even if no diff occurred as long as one field
   394  // changed.
   395  func taskGroupDiffs(old, new []*TaskGroup, contextual bool) ([]*TaskGroupDiff, error) {
   396  	oldMap := make(map[string]*TaskGroup, len(old))
   397  	newMap := make(map[string]*TaskGroup, len(new))
   398  	for _, o := range old {
   399  		oldMap[o.Name] = o
   400  	}
   401  	for _, n := range new {
   402  		newMap[n.Name] = n
   403  	}
   404  
   405  	var diffs []*TaskGroupDiff
   406  	for name, oldGroup := range oldMap {
   407  		// Diff the same, deleted and edited
   408  		diff, err := oldGroup.Diff(newMap[name], contextual)
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  		diffs = append(diffs, diff)
   413  	}
   414  
   415  	for name, newGroup := range newMap {
   416  		// Diff the added
   417  		if old, ok := oldMap[name]; !ok {
   418  			diff, err := old.Diff(newGroup, contextual)
   419  			if err != nil {
   420  				return nil, err
   421  			}
   422  			diffs = append(diffs, diff)
   423  		}
   424  	}
   425  
   426  	sort.Sort(TaskGroupDiffs(diffs))
   427  	return diffs, nil
   428  }
   429  
   430  // For sorting TaskGroupDiffs
   431  type TaskGroupDiffs []*TaskGroupDiff
   432  
   433  func (tg TaskGroupDiffs) Len() int           { return len(tg) }
   434  func (tg TaskGroupDiffs) Swap(i, j int)      { tg[i], tg[j] = tg[j], tg[i] }
   435  func (tg TaskGroupDiffs) Less(i, j int) bool { return tg[i].Name < tg[j].Name }
   436  
   437  // TaskDiff contains the diff of two Tasks
   438  type TaskDiff struct {
   439  	Type        DiffType
   440  	Name        string
   441  	Fields      []*FieldDiff
   442  	Objects     []*ObjectDiff
   443  	Annotations []string
   444  }
   445  
   446  // Diff returns a diff of two tasks. If contextual diff is enabled, objects
   447  // within the task will contain field information even if unchanged.
   448  func (t *Task) Diff(other *Task, contextual bool) (*TaskDiff, error) {
   449  	diff := &TaskDiff{Type: DiffTypeNone}
   450  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   451  	filter := []string{"Name", "Config"}
   452  
   453  	if t == nil && other == nil {
   454  		return diff, nil
   455  	} else if t == nil {
   456  		t = &Task{}
   457  		diff.Type = DiffTypeAdded
   458  		diff.Name = other.Name
   459  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
   460  	} else if other == nil {
   461  		other = &Task{}
   462  		diff.Type = DiffTypeDeleted
   463  		diff.Name = t.Name
   464  		oldPrimitiveFlat = flatmap.Flatten(t, filter, true)
   465  	} else {
   466  		if !reflect.DeepEqual(t, other) {
   467  			diff.Type = DiffTypeEdited
   468  		}
   469  		if t.Name != other.Name {
   470  			return nil, fmt.Errorf("can not diff tasks with different names: %q and %q", t.Name, other.Name)
   471  		}
   472  		diff.Name = other.Name
   473  		oldPrimitiveFlat = flatmap.Flatten(t, filter, true)
   474  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
   475  	}
   476  
   477  	// Diff the primitive fields.
   478  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
   479  
   480  	// Constraints diff
   481  	conDiff := primitiveObjectSetDiff(
   482  		interfaceSlice(t.Constraints),
   483  		interfaceSlice(other.Constraints),
   484  		[]string{"str"},
   485  		"Constraint",
   486  		contextual)
   487  	if conDiff != nil {
   488  		diff.Objects = append(diff.Objects, conDiff...)
   489  	}
   490  
   491  	// Affinities diff
   492  	affinitiesDiff := primitiveObjectSetDiff(
   493  		interfaceSlice(t.Affinities),
   494  		interfaceSlice(other.Affinities),
   495  		[]string{"str"},
   496  		"Affinity",
   497  		contextual)
   498  	if affinitiesDiff != nil {
   499  		diff.Objects = append(diff.Objects, affinitiesDiff...)
   500  	}
   501  
   502  	// Config diff
   503  	if cDiff := configDiff(t.Config, other.Config, contextual); cDiff != nil {
   504  		diff.Objects = append(diff.Objects, cDiff)
   505  	}
   506  
   507  	// Resources diff
   508  	if rDiff := t.Resources.Diff(other.Resources, contextual); rDiff != nil {
   509  		diff.Objects = append(diff.Objects, rDiff)
   510  	}
   511  
   512  	// LogConfig diff
   513  	lDiff := primitiveObjectDiff(t.LogConfig, other.LogConfig, nil, "LogConfig", contextual)
   514  	if lDiff != nil {
   515  		diff.Objects = append(diff.Objects, lDiff)
   516  	}
   517  
   518  	// Dispatch payload diff
   519  	dDiff := primitiveObjectDiff(t.DispatchPayload, other.DispatchPayload, nil, "DispatchPayload", contextual)
   520  	if dDiff != nil {
   521  		diff.Objects = append(diff.Objects, dDiff)
   522  	}
   523  
   524  	// Artifacts diff
   525  	diffs := primitiveObjectSetDiff(
   526  		interfaceSlice(t.Artifacts),
   527  		interfaceSlice(other.Artifacts),
   528  		nil,
   529  		"Artifact",
   530  		contextual)
   531  	if diffs != nil {
   532  		diff.Objects = append(diff.Objects, diffs...)
   533  	}
   534  
   535  	// Services diff
   536  	if sDiffs := serviceDiffs(t.Services, other.Services, contextual); sDiffs != nil {
   537  		diff.Objects = append(diff.Objects, sDiffs...)
   538  	}
   539  
   540  	// Vault diff
   541  	vDiff := vaultDiff(t.Vault, other.Vault, contextual)
   542  	if vDiff != nil {
   543  		diff.Objects = append(diff.Objects, vDiff)
   544  	}
   545  
   546  	// Template diff
   547  	tmplDiffs := templateDiffs(t.Templates, other.Templates, contextual)
   548  	if tmplDiffs != nil {
   549  		diff.Objects = append(diff.Objects, tmplDiffs...)
   550  	}
   551  
   552  	// Identity diff
   553  	idDiffs := idDiff(t.Identity, other.Identity, contextual)
   554  	if idDiffs != nil {
   555  		diff.Objects = append(diff.Objects, idDiffs)
   556  	}
   557  
   558  	return diff, nil
   559  }
   560  
   561  func (t *TaskDiff) GoString() string {
   562  	var out string
   563  	if len(t.Annotations) == 0 {
   564  		out = fmt.Sprintf("Task %q (%s):\n", t.Name, t.Type)
   565  	} else {
   566  		out = fmt.Sprintf("Task %q (%s) (%s):\n", t.Name, t.Type, strings.Join(t.Annotations, ","))
   567  	}
   568  
   569  	for _, f := range t.Fields {
   570  		out += fmt.Sprintf("%#v\n", f)
   571  	}
   572  
   573  	for _, o := range t.Objects {
   574  		out += fmt.Sprintf("%#v\n", o)
   575  	}
   576  
   577  	return out
   578  }
   579  
   580  // taskDiffs diffs a set of tasks. If contextual diff is enabled, unchanged
   581  // fields within objects nested in the tasks will be returned.
   582  func taskDiffs(old, new []*Task, contextual bool) ([]*TaskDiff, error) {
   583  	oldMap := make(map[string]*Task, len(old))
   584  	newMap := make(map[string]*Task, len(new))
   585  	for _, o := range old {
   586  		oldMap[o.Name] = o
   587  	}
   588  	for _, n := range new {
   589  		newMap[n.Name] = n
   590  	}
   591  
   592  	var diffs []*TaskDiff
   593  	for name, oldGroup := range oldMap {
   594  		// Diff the same, deleted and edited
   595  		diff, err := oldGroup.Diff(newMap[name], contextual)
   596  		if err != nil {
   597  			return nil, err
   598  		}
   599  		diffs = append(diffs, diff)
   600  	}
   601  
   602  	for name, newGroup := range newMap {
   603  		// Diff the added
   604  		if old, ok := oldMap[name]; !ok {
   605  			diff, err := old.Diff(newGroup, contextual)
   606  			if err != nil {
   607  				return nil, err
   608  			}
   609  			diffs = append(diffs, diff)
   610  		}
   611  	}
   612  
   613  	sort.Sort(TaskDiffs(diffs))
   614  	return diffs, nil
   615  }
   616  
   617  // For sorting TaskDiffs
   618  type TaskDiffs []*TaskDiff
   619  
   620  func (t TaskDiffs) Len() int           { return len(t) }
   621  func (t TaskDiffs) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
   622  func (t TaskDiffs) Less(i, j int) bool { return t[i].Name < t[j].Name }
   623  
   624  // serviceDiff returns the diff of two service objects. If contextual diff is
   625  // enabled, all fields will be returned, even if no diff occurred.
   626  func serviceDiff(old, new *Service, contextual bool) *ObjectDiff {
   627  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Service"}
   628  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   629  
   630  	if reflect.DeepEqual(old, new) {
   631  		return nil
   632  	} else if old == nil {
   633  		old = &Service{}
   634  		diff.Type = DiffTypeAdded
   635  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   636  	} else if new == nil {
   637  		new = &Service{}
   638  		diff.Type = DiffTypeDeleted
   639  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   640  	} else {
   641  		diff.Type = DiffTypeEdited
   642  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   643  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   644  	}
   645  
   646  	// Diff the primitive fields.
   647  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
   648  
   649  	if setDiff := stringSetDiff(old.CanaryTags, new.CanaryTags, "CanaryTags", contextual); setDiff != nil {
   650  		diff.Objects = append(diff.Objects, setDiff)
   651  	}
   652  
   653  	// Tag diffs
   654  	if setDiff := stringSetDiff(old.Tags, new.Tags, "Tags", contextual); setDiff != nil {
   655  		diff.Objects = append(diff.Objects, setDiff)
   656  	}
   657  
   658  	// Checks diffs
   659  	if cDiffs := serviceCheckDiffs(old.Checks, new.Checks, contextual); cDiffs != nil {
   660  		diff.Objects = append(diff.Objects, cDiffs...)
   661  	}
   662  
   663  	// Consul Connect diffs
   664  	if conDiffs := connectDiffs(old.Connect, new.Connect, contextual); conDiffs != nil {
   665  		diff.Objects = append(diff.Objects, conDiffs)
   666  	}
   667  
   668  	return diff
   669  }
   670  
   671  // serviceDiffs diffs a set of services. If contextual diff is enabled, unchanged
   672  // fields within objects nested in the tasks will be returned.
   673  func serviceDiffs(old, new []*Service, contextual bool) []*ObjectDiff {
   674  	// Handle trivial case.
   675  	if len(old) == 1 && len(new) == 1 {
   676  		if diff := serviceDiff(old[0], new[0], contextual); diff != nil {
   677  			return []*ObjectDiff{diff}
   678  		}
   679  		return nil
   680  	}
   681  
   682  	// For each service we will try to find a corresponding match in the other
   683  	// service list.
   684  	// The following lists store the index of the matching service for each
   685  	// position of the inputs.
   686  	oldMatches := make([]int, len(old))
   687  	newMatches := make([]int, len(new))
   688  
   689  	// Initialize all services as unmatched.
   690  	for i := range oldMatches {
   691  		oldMatches[i] = -1
   692  	}
   693  	for i := range newMatches {
   694  		newMatches[i] = -1
   695  	}
   696  
   697  	// Find a match in the new services list for each old service and compute
   698  	// their diffs.
   699  	var diffs []*ObjectDiff
   700  	for oldIndex, oldService := range old {
   701  		newIndex := findServiceMatch(oldService, oldIndex, new, newMatches)
   702  
   703  		// Old services that don't have a match were deleted.
   704  		if newIndex < 0 {
   705  			diff := serviceDiff(oldService, nil, contextual)
   706  			diffs = append(diffs, diff)
   707  			continue
   708  		}
   709  
   710  		// If A matches B then B matches A.
   711  		oldMatches[oldIndex] = newIndex
   712  		newMatches[newIndex] = oldIndex
   713  
   714  		newService := new[newIndex]
   715  		if diff := serviceDiff(oldService, newService, contextual); diff != nil {
   716  			diffs = append(diffs, diff)
   717  		}
   718  	}
   719  
   720  	// New services without match were added.
   721  	for i, m := range newMatches {
   722  		if m == -1 {
   723  			diff := serviceDiff(nil, new[i], contextual)
   724  			diffs = append(diffs, diff)
   725  		}
   726  	}
   727  
   728  	sort.Sort(ObjectDiffs(diffs))
   729  	return diffs
   730  }
   731  
   732  // findServiceMatch returns the index of the service in the input services list
   733  // that matches the provided input service.
   734  func findServiceMatch(service *Service, serviceIndex int, services []*Service, matches []int) int {
   735  	// minScoreThreshold can be adjusted to generate more (lower value) or
   736  	// fewer (higher value) matches.
   737  	// More matches result in more Edited diffs, while fewer matches generate
   738  	// more Add/Delete diff pairs.
   739  	minScoreThreshold := 2
   740  
   741  	highestScore := 0
   742  	indexMatch := -1
   743  
   744  	for i, s := range services {
   745  		// Skip service if it's already matched.
   746  		if matches[i] >= 0 {
   747  			continue
   748  		}
   749  
   750  		// Finding a perfect match by just looking at the before and after
   751  		// list of services is impossible since they don't have a stable
   752  		// identifier that can be used to uniquely identify them.
   753  		//
   754  		// Users also have an implicit temporal intuition of which services
   755  		// match each other when editing their jobspec file. If they move the
   756  		// 3rd service to the top, they don't expect their job to change.
   757  		//
   758  		// This intuition could be made explicit by requiring a user-defined
   759  		// unique identifier, but this would cause additional work and the
   760  		// new field would not be intuitive for users to understand how to use
   761  		// it.
   762  		//
   763  		// Using a hash value of the service content will cause any changes to
   764  		// create a delete/add diff pair.
   765  		//
   766  		// There are three main candidates for a service ID:
   767  		//   - name, but they are not unique and can be modified.
   768  		//   - label port, but they have the same problems as name.
   769  		//   - service position within the overall list of services, but if the
   770  		//     service block is moved, it will impact all services that come
   771  		//     after it.
   772  		//
   773  		// None of these values are enough on their own, but they are also too
   774  		// strong when considered all together.
   775  		//
   776  		// So we try to score services by their main candidates with a preference
   777  		// towards name + label over service position.
   778  		score := 0
   779  		if i == serviceIndex {
   780  			score += 1
   781  		}
   782  
   783  		if service.PortLabel == s.PortLabel {
   784  			score += 2
   785  		}
   786  
   787  		if service.Name == s.Name {
   788  			score += 3
   789  		}
   790  
   791  		if score > minScoreThreshold && score > highestScore {
   792  			highestScore = score
   793  			indexMatch = i
   794  		}
   795  	}
   796  
   797  	return indexMatch
   798  }
   799  
   800  // serviceCheckDiff returns the diff of two service check objects. If contextual
   801  // diff is enabled, all fields will be returned, even if no diff occurred.
   802  func serviceCheckDiff(old, new *ServiceCheck, contextual bool) *ObjectDiff {
   803  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Check"}
   804  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   805  
   806  	if reflect.DeepEqual(old, new) {
   807  		return nil
   808  	} else if old == nil {
   809  		old = &ServiceCheck{}
   810  		diff.Type = DiffTypeAdded
   811  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   812  	} else if new == nil {
   813  		new = &ServiceCheck{}
   814  		diff.Type = DiffTypeDeleted
   815  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   816  	} else {
   817  		diff.Type = DiffTypeEdited
   818  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   819  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   820  	}
   821  
   822  	// Diff the primitive fields.
   823  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
   824  
   825  	// Diff Header
   826  	if headerDiff := checkHeaderDiff(old.Header, new.Header, contextual); headerDiff != nil {
   827  		diff.Objects = append(diff.Objects, headerDiff)
   828  	}
   829  
   830  	// Diff check_restart
   831  	if crDiff := checkRestartDiff(old.CheckRestart, new.CheckRestart, contextual); crDiff != nil {
   832  		diff.Objects = append(diff.Objects, crDiff)
   833  	}
   834  
   835  	return diff
   836  }
   837  
   838  // checkHeaderDiff returns the diff of two service check header objects. If
   839  // contextual diff is enabled, all fields will be returned, even if no diff
   840  // occurred.
   841  func checkHeaderDiff(old, new map[string][]string, contextual bool) *ObjectDiff {
   842  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Header"}
   843  	var oldFlat, newFlat map[string]string
   844  
   845  	if reflect.DeepEqual(old, new) {
   846  		return nil
   847  	} else if len(old) == 0 {
   848  		diff.Type = DiffTypeAdded
   849  		newFlat = flatmap.Flatten(new, nil, false)
   850  	} else if len(new) == 0 {
   851  		diff.Type = DiffTypeDeleted
   852  		oldFlat = flatmap.Flatten(old, nil, false)
   853  	} else {
   854  		diff.Type = DiffTypeEdited
   855  		oldFlat = flatmap.Flatten(old, nil, false)
   856  		newFlat = flatmap.Flatten(new, nil, false)
   857  	}
   858  
   859  	diff.Fields = fieldDiffs(oldFlat, newFlat, contextual)
   860  	return diff
   861  }
   862  
   863  // checkRestartDiff returns the diff of two service check check_restart
   864  // objects. If contextual diff is enabled, all fields will be returned, even if
   865  // no diff occurred.
   866  func checkRestartDiff(old, new *CheckRestart, contextual bool) *ObjectDiff {
   867  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "CheckRestart"}
   868  	var oldFlat, newFlat map[string]string
   869  
   870  	if reflect.DeepEqual(old, new) {
   871  		return nil
   872  	} else if old == nil {
   873  		diff.Type = DiffTypeAdded
   874  		newFlat = flatmap.Flatten(new, nil, true)
   875  		diff.Type = DiffTypeAdded
   876  	} else if new == nil {
   877  		diff.Type = DiffTypeDeleted
   878  		oldFlat = flatmap.Flatten(old, nil, true)
   879  	} else {
   880  		diff.Type = DiffTypeEdited
   881  		oldFlat = flatmap.Flatten(old, nil, true)
   882  		newFlat = flatmap.Flatten(new, nil, true)
   883  	}
   884  
   885  	diff.Fields = fieldDiffs(oldFlat, newFlat, contextual)
   886  	return diff
   887  }
   888  
   889  // connectDiffs returns the diff of two Consul connect objects. If contextual
   890  // diff is enabled, all fields will be returned, even if no diff occurred.
   891  func connectDiffs(old, new *ConsulConnect, contextual bool) *ObjectDiff {
   892  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulConnect"}
   893  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   894  
   895  	if reflect.DeepEqual(old, new) {
   896  		return nil
   897  	} else if old == nil {
   898  		old = &ConsulConnect{}
   899  		diff.Type = DiffTypeAdded
   900  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   901  	} else if new == nil {
   902  		new = &ConsulConnect{}
   903  		diff.Type = DiffTypeDeleted
   904  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   905  	} else {
   906  		diff.Type = DiffTypeEdited
   907  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
   908  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
   909  	}
   910  
   911  	// Diff the primitive fields.
   912  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
   913  
   914  	// Diff the object field SidecarService.
   915  	sidecarSvcDiff := connectSidecarServiceDiff(old.SidecarService, new.SidecarService, contextual)
   916  	if sidecarSvcDiff != nil {
   917  		diff.Objects = append(diff.Objects, sidecarSvcDiff)
   918  	}
   919  
   920  	// Diff the object field SidecarTask.
   921  	sidecarTaskDiff := sidecarTaskDiff(old.SidecarTask, new.SidecarTask, contextual)
   922  	if sidecarTaskDiff != nil {
   923  		diff.Objects = append(diff.Objects, sidecarTaskDiff)
   924  	}
   925  
   926  	// Diff the object field ConsulGateway.
   927  	gatewayDiff := connectGatewayDiff(old.Gateway, new.Gateway, contextual)
   928  	if gatewayDiff != nil {
   929  		diff.Objects = append(diff.Objects, gatewayDiff)
   930  	}
   931  
   932  	return diff
   933  }
   934  
   935  func connectGatewayDiff(prev, next *ConsulGateway, contextual bool) *ObjectDiff {
   936  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Gateway"}
   937  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
   938  
   939  	if reflect.DeepEqual(prev, next) {
   940  		return nil
   941  	} else if prev == nil {
   942  		prev = new(ConsulGateway)
   943  		diff.Type = DiffTypeAdded
   944  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
   945  	} else if next == nil {
   946  		next = new(ConsulGateway)
   947  		diff.Type = DiffTypeDeleted
   948  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
   949  	} else {
   950  		diff.Type = DiffTypeEdited
   951  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
   952  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
   953  	}
   954  
   955  	// Diff the primitive fields.
   956  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
   957  
   958  	// Diff the ConsulGatewayProxy fields.
   959  	gatewayProxyDiff := connectGatewayProxyDiff(prev.Proxy, next.Proxy, contextual)
   960  	if gatewayProxyDiff != nil {
   961  		diff.Objects = append(diff.Objects, gatewayProxyDiff)
   962  	}
   963  
   964  	// Diff the ingress gateway fields.
   965  	gatewayIngressDiff := connectGatewayIngressDiff(prev.Ingress, next.Ingress, contextual)
   966  	if gatewayIngressDiff != nil {
   967  		diff.Objects = append(diff.Objects, gatewayIngressDiff)
   968  	}
   969  
   970  	//  Diff the terminating gateway fields.
   971  	gatewayTerminatingDiff := connectGatewayTerminatingDiff(prev.Terminating, next.Terminating, contextual)
   972  	if gatewayTerminatingDiff != nil {
   973  		diff.Objects = append(diff.Objects, gatewayTerminatingDiff)
   974  	}
   975  
   976  	// Diff the mesh gateway fields.
   977  	gatewayMeshDiff := connectGatewayMeshDiff(prev.Mesh, next.Mesh, contextual)
   978  	if gatewayMeshDiff != nil {
   979  		diff.Objects = append(diff.Objects, gatewayMeshDiff)
   980  	}
   981  
   982  	return diff
   983  }
   984  
   985  func connectGatewayMeshDiff(prev, next *ConsulMeshConfigEntry, contextual bool) *ObjectDiff {
   986  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Mesh"}
   987  
   988  	if reflect.DeepEqual(prev, next) {
   989  		return nil
   990  	} else if prev == nil {
   991  		// no fields to further diff
   992  		diff.Type = DiffTypeAdded
   993  	} else if next == nil {
   994  		// no fields to further diff
   995  		diff.Type = DiffTypeDeleted
   996  	} else {
   997  		diff.Type = DiffTypeEdited
   998  	}
   999  
  1000  	// Currently no fields in mesh gateways.
  1001  
  1002  	return diff
  1003  }
  1004  
  1005  func connectGatewayIngressDiff(prev, next *ConsulIngressConfigEntry, contextual bool) *ObjectDiff {
  1006  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Ingress"}
  1007  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1008  
  1009  	if reflect.DeepEqual(prev, next) {
  1010  		return nil
  1011  	} else if prev == nil {
  1012  		prev = new(ConsulIngressConfigEntry)
  1013  		diff.Type = DiffTypeAdded
  1014  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1015  	} else if next == nil {
  1016  		next = new(ConsulIngressConfigEntry)
  1017  		diff.Type = DiffTypeDeleted
  1018  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1019  	} else {
  1020  		diff.Type = DiffTypeEdited
  1021  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1022  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1023  	}
  1024  
  1025  	// Diff the primitive fields.
  1026  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1027  
  1028  	// Diff the ConsulGatewayTLSConfig objects.
  1029  	tlsConfigDiff := connectGatewayTLSConfigDiff(prev.TLS, next.TLS, contextual)
  1030  	if tlsConfigDiff != nil {
  1031  		diff.Objects = append(diff.Objects, tlsConfigDiff)
  1032  	}
  1033  
  1034  	// Diff the Listeners lists.
  1035  	gatewayIngressListenersDiff := connectGatewayIngressListenersDiff(prev.Listeners, next.Listeners, contextual)
  1036  	if gatewayIngressListenersDiff != nil {
  1037  		diff.Objects = append(diff.Objects, gatewayIngressListenersDiff...)
  1038  	}
  1039  
  1040  	return diff
  1041  }
  1042  
  1043  func connectGatewayTerminatingDiff(prev, next *ConsulTerminatingConfigEntry, contextual bool) *ObjectDiff {
  1044  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Terminating"}
  1045  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1046  
  1047  	if reflect.DeepEqual(prev, next) {
  1048  		return nil
  1049  	} else if prev == nil {
  1050  		prev = new(ConsulTerminatingConfigEntry)
  1051  		diff.Type = DiffTypeAdded
  1052  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1053  	} else if next == nil {
  1054  		next = new(ConsulTerminatingConfigEntry)
  1055  		diff.Type = DiffTypeDeleted
  1056  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1057  	} else {
  1058  		diff.Type = DiffTypeEdited
  1059  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1060  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1061  	}
  1062  
  1063  	// Diff the primitive fields.
  1064  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1065  
  1066  	// Diff the Services lists.
  1067  	gatewayLinkedServicesDiff := connectGatewayTerminatingLinkedServicesDiff(prev.Services, next.Services, contextual)
  1068  	if gatewayLinkedServicesDiff != nil {
  1069  		diff.Objects = append(diff.Objects, gatewayLinkedServicesDiff...)
  1070  	}
  1071  
  1072  	return diff
  1073  }
  1074  
  1075  // connectGatewayTerminatingLinkedServicesDiff diffs are a set of services keyed
  1076  // by service name. These objects contain only fields.
  1077  func connectGatewayTerminatingLinkedServicesDiff(prev, next []*ConsulLinkedService, contextual bool) []*ObjectDiff {
  1078  	// create maps, diff the maps, key by linked service name
  1079  
  1080  	prevMap := make(map[string]*ConsulLinkedService, len(prev))
  1081  	nextMap := make(map[string]*ConsulLinkedService, len(next))
  1082  
  1083  	for _, s := range prev {
  1084  		prevMap[s.Name] = s
  1085  	}
  1086  	for _, s := range next {
  1087  		nextMap[s.Name] = s
  1088  	}
  1089  
  1090  	var diffs []*ObjectDiff
  1091  	for k, prevS := range prevMap {
  1092  		// Diff the same, deleted, and edited
  1093  		if diff := connectGatewayTerminatingLinkedServiceDiff(prevS, nextMap[k], contextual); diff != nil {
  1094  			diffs = append(diffs, diff)
  1095  		}
  1096  	}
  1097  	for k, nextS := range nextMap {
  1098  		// Diff the added
  1099  		if old, ok := prevMap[k]; !ok {
  1100  			if diff := connectGatewayTerminatingLinkedServiceDiff(old, nextS, contextual); diff != nil {
  1101  				diffs = append(diffs, diff)
  1102  			}
  1103  		}
  1104  	}
  1105  
  1106  	sort.Sort(ObjectDiffs(diffs))
  1107  	return diffs
  1108  }
  1109  
  1110  func connectGatewayTerminatingLinkedServiceDiff(prev, next *ConsulLinkedService, contextual bool) *ObjectDiff {
  1111  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Service"}
  1112  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1113  
  1114  	if reflect.DeepEqual(prev, next) {
  1115  		return nil
  1116  	} else if prev == nil {
  1117  		diff.Type = DiffTypeAdded
  1118  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1119  	} else if next == nil {
  1120  		diff.Type = DiffTypeDeleted
  1121  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1122  	} else {
  1123  		diff.Type = DiffTypeEdited
  1124  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1125  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1126  	}
  1127  
  1128  	// Diff the primitive fields.
  1129  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1130  
  1131  	// No objects today.
  1132  
  1133  	return diff
  1134  }
  1135  
  1136  func connectGatewayTLSConfigDiff(prev, next *ConsulGatewayTLSConfig, contextual bool) *ObjectDiff {
  1137  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "TLS"}
  1138  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1139  
  1140  	if reflect.DeepEqual(prev, next) {
  1141  		return nil
  1142  	} else if prev == nil {
  1143  		prev = &ConsulGatewayTLSConfig{}
  1144  		diff.Type = DiffTypeAdded
  1145  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1146  	} else if next == nil {
  1147  		next = &ConsulGatewayTLSConfig{}
  1148  		diff.Type = DiffTypeDeleted
  1149  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1150  	} else {
  1151  		diff.Type = DiffTypeEdited
  1152  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1153  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1154  	}
  1155  
  1156  	// CipherSuites diffs
  1157  	if setDiff := stringSetDiff(prev.CipherSuites, next.CipherSuites, "CipherSuites", contextual); setDiff != nil {
  1158  		diff.Objects = append(diff.Objects, setDiff)
  1159  	}
  1160  
  1161  	// Diff the primitive field.
  1162  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1163  
  1164  	return diff
  1165  }
  1166  
  1167  // connectGatewayIngressListenersDiff diffs are a set of listeners keyed by "protocol/port", which is
  1168  // a nifty workaround having slices instead of maps. Presumably such a key will be unique, because if
  1169  // if is not the config entry is not going to work anyway.
  1170  func connectGatewayIngressListenersDiff(prev, next []*ConsulIngressListener, contextual bool) []*ObjectDiff {
  1171  	//  create maps, diff the maps, keys are fields, keys are (port+protocol)
  1172  
  1173  	key := func(l *ConsulIngressListener) string {
  1174  		return fmt.Sprintf("%s/%d", l.Protocol, l.Port)
  1175  	}
  1176  
  1177  	prevMap := make(map[string]*ConsulIngressListener, len(prev))
  1178  	nextMap := make(map[string]*ConsulIngressListener, len(next))
  1179  
  1180  	for _, l := range prev {
  1181  		prevMap[key(l)] = l
  1182  	}
  1183  	for _, l := range next {
  1184  		nextMap[key(l)] = l
  1185  	}
  1186  
  1187  	var diffs []*ObjectDiff
  1188  	for k, prevL := range prevMap {
  1189  		// Diff the same, deleted, and edited
  1190  		if diff := connectGatewayIngressListenerDiff(prevL, nextMap[k], contextual); diff != nil {
  1191  			diffs = append(diffs, diff)
  1192  		}
  1193  	}
  1194  	for k, nextL := range nextMap {
  1195  		// Diff the added
  1196  		if old, ok := prevMap[k]; !ok {
  1197  			if diff := connectGatewayIngressListenerDiff(old, nextL, contextual); diff != nil {
  1198  				diffs = append(diffs, diff)
  1199  			}
  1200  		}
  1201  	}
  1202  
  1203  	sort.Sort(ObjectDiffs(diffs))
  1204  	return diffs
  1205  }
  1206  
  1207  func connectGatewayIngressListenerDiff(prev, next *ConsulIngressListener, contextual bool) *ObjectDiff {
  1208  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Listener"}
  1209  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1210  
  1211  	if reflect.DeepEqual(prev, next) {
  1212  		return nil
  1213  	} else if prev == nil {
  1214  		prev = new(ConsulIngressListener)
  1215  		diff.Type = DiffTypeAdded
  1216  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1217  	} else if next == nil {
  1218  		next = new(ConsulIngressListener)
  1219  		diff.Type = DiffTypeDeleted
  1220  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1221  	} else {
  1222  		diff.Type = DiffTypeEdited
  1223  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1224  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1225  	}
  1226  
  1227  	// Diff the primitive fields.
  1228  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1229  
  1230  	// Diff the Ingress Service objects.
  1231  	if diffs := connectGatewayIngressServicesDiff(prev.Services, next.Services, contextual); diffs != nil {
  1232  		diff.Objects = append(diff.Objects, diffs...)
  1233  	}
  1234  
  1235  	return diff
  1236  }
  1237  
  1238  // connectGatewayIngressServicesDiff diffs are a set of ingress services keyed by their service name, which
  1239  // is a workaround for having slices instead of maps. Presumably the service name is a unique key, because if
  1240  // no the config entry is not going to make sense anyway.
  1241  func connectGatewayIngressServicesDiff(prev, next []*ConsulIngressService, contextual bool) []*ObjectDiff {
  1242  
  1243  	prevMap := make(map[string]*ConsulIngressService, len(prev))
  1244  	nextMap := make(map[string]*ConsulIngressService, len(next))
  1245  
  1246  	for _, s := range prev {
  1247  		prevMap[s.Name] = s
  1248  	}
  1249  	for _, s := range next {
  1250  		nextMap[s.Name] = s
  1251  	}
  1252  
  1253  	var diffs []*ObjectDiff
  1254  	for name, oldIS := range prevMap {
  1255  		// Diff the same, deleted, and edited
  1256  		if diff := connectGatewayIngressServiceDiff(oldIS, nextMap[name], contextual); diff != nil {
  1257  			diffs = append(diffs, diff)
  1258  		}
  1259  	}
  1260  	for name, newIS := range nextMap {
  1261  		// Diff the added
  1262  		if old, ok := prevMap[name]; !ok {
  1263  			if diff := connectGatewayIngressServiceDiff(old, newIS, contextual); diff != nil {
  1264  				diffs = append(diffs, diff)
  1265  			}
  1266  		}
  1267  	}
  1268  
  1269  	sort.Sort(ObjectDiffs(diffs))
  1270  	return diffs
  1271  }
  1272  
  1273  func connectGatewayIngressServiceDiff(prev, next *ConsulIngressService, contextual bool) *ObjectDiff {
  1274  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulIngressService"}
  1275  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1276  
  1277  	if reflect.DeepEqual(prev, next) {
  1278  		return nil
  1279  	} else if prev == nil {
  1280  		prev = new(ConsulIngressService)
  1281  		diff.Type = DiffTypeAdded
  1282  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1283  	} else if next == nil {
  1284  		next = new(ConsulIngressService)
  1285  		diff.Type = DiffTypeDeleted
  1286  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1287  	} else {
  1288  		diff.Type = DiffTypeEdited
  1289  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1290  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1291  	}
  1292  
  1293  	// Diff the primitive fields.
  1294  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1295  
  1296  	// Diff the hosts.
  1297  	if hDiffs := stringSetDiff(prev.Hosts, next.Hosts, "Hosts", contextual); hDiffs != nil {
  1298  		diff.Objects = append(diff.Objects, hDiffs)
  1299  	}
  1300  
  1301  	return diff
  1302  }
  1303  
  1304  func connectGatewayProxyDiff(prev, next *ConsulGatewayProxy, contextual bool) *ObjectDiff {
  1305  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Proxy"}
  1306  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1307  
  1308  	if reflect.DeepEqual(prev, next) {
  1309  		return nil
  1310  	} else if prev == nil {
  1311  		prev = new(ConsulGatewayProxy)
  1312  		diff.Type = DiffTypeAdded
  1313  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1314  	} else if next == nil {
  1315  		next = new(ConsulGatewayProxy)
  1316  		diff.Type = DiffTypeDeleted
  1317  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1318  	} else {
  1319  		diff.Type = DiffTypeEdited
  1320  		oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
  1321  		newPrimitiveFlat = flatmap.Flatten(next, nil, true)
  1322  	}
  1323  
  1324  	// Diff the ConnectTimeout field (dur ptr). (i.e. convert to string for comparison)
  1325  	if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
  1326  		if prev.ConnectTimeout == nil {
  1327  			oldPrimitiveFlat["ConnectTimeout"] = ""
  1328  		} else {
  1329  			oldPrimitiveFlat["ConnectTimeout"] = prev.ConnectTimeout.String()
  1330  		}
  1331  		if next.ConnectTimeout == nil {
  1332  			newPrimitiveFlat["ConnectTimeout"] = ""
  1333  		} else {
  1334  			newPrimitiveFlat["ConnectTimeout"] = next.ConnectTimeout.String()
  1335  		}
  1336  	}
  1337  
  1338  	// Diff the primitive fields.
  1339  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1340  
  1341  	// Diff the EnvoyGatewayBindAddresses map.
  1342  	bindAddrsDiff := connectGatewayProxyEnvoyBindAddrsDiff(prev.EnvoyGatewayBindAddresses, next.EnvoyGatewayBindAddresses, contextual)
  1343  	if bindAddrsDiff != nil {
  1344  		diff.Objects = append(diff.Objects, bindAddrsDiff)
  1345  	}
  1346  
  1347  	// Diff the opaque Config map.
  1348  	if cDiff := configDiff(prev.Config, next.Config, contextual); cDiff != nil {
  1349  		diff.Objects = append(diff.Objects, cDiff)
  1350  	}
  1351  
  1352  	return diff
  1353  }
  1354  
  1355  // connectGatewayProxyEnvoyBindAddrsDiff returns the diff of two maps. If contextual
  1356  // diff is enabled, all fields will be returned, even if no diff occurred.
  1357  func connectGatewayProxyEnvoyBindAddrsDiff(prev, next map[string]*ConsulGatewayBindAddress, contextual bool) *ObjectDiff {
  1358  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "EnvoyGatewayBindAddresses"}
  1359  	if reflect.DeepEqual(prev, next) {
  1360  		return nil
  1361  	} else if len(prev) == 0 {
  1362  		diff.Type = DiffTypeAdded
  1363  	} else if len(next) == 0 {
  1364  		diff.Type = DiffTypeDeleted
  1365  	} else {
  1366  		diff.Type = DiffTypeEdited
  1367  	}
  1368  
  1369  	// convert to string representation
  1370  	prevMap := make(map[string]string, len(prev))
  1371  	nextMap := make(map[string]string, len(next))
  1372  
  1373  	for k, v := range prev {
  1374  		prevMap[k] = net.JoinHostPort(v.Address, strconv.Itoa(v.Port))
  1375  	}
  1376  
  1377  	for k, v := range next {
  1378  		nextMap[k] = net.JoinHostPort(v.Address, strconv.Itoa(v.Port))
  1379  	}
  1380  
  1381  	oldPrimitiveFlat := flatmap.Flatten(prevMap, nil, false)
  1382  	newPrimitiveFlat := flatmap.Flatten(nextMap, nil, false)
  1383  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1384  	return diff
  1385  }
  1386  
  1387  // connectSidecarServiceDiff returns the diff of two ConsulSidecarService objects.
  1388  // If contextual diff is enabled, all fields will be returned, even if no diff occurred.
  1389  func connectSidecarServiceDiff(old, new *ConsulSidecarService, contextual bool) *ObjectDiff {
  1390  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "SidecarService"}
  1391  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1392  
  1393  	if reflect.DeepEqual(old, new) {
  1394  		return nil
  1395  	} else if old == nil {
  1396  		old = &ConsulSidecarService{}
  1397  		diff.Type = DiffTypeAdded
  1398  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1399  	} else if new == nil {
  1400  		new = &ConsulSidecarService{}
  1401  		diff.Type = DiffTypeDeleted
  1402  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1403  	} else {
  1404  		diff.Type = DiffTypeEdited
  1405  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1406  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1407  	}
  1408  
  1409  	// Diff the primitive fields.
  1410  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1411  
  1412  	consulProxyDiff := consulProxyDiff(old.Proxy, new.Proxy, contextual)
  1413  	if consulProxyDiff != nil {
  1414  		diff.Objects = append(diff.Objects, consulProxyDiff)
  1415  	}
  1416  
  1417  	return diff
  1418  }
  1419  
  1420  // sidecarTaskDiff returns the diff of two Task objects.
  1421  // If contextual diff is enabled, all fields will be returned, even if no diff occurred.
  1422  func sidecarTaskDiff(old, new *SidecarTask, contextual bool) *ObjectDiff {
  1423  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "SidecarTask"}
  1424  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1425  
  1426  	if reflect.DeepEqual(old, new) {
  1427  		return nil
  1428  	} else if old == nil {
  1429  		old = &SidecarTask{}
  1430  		diff.Type = DiffTypeAdded
  1431  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1432  	} else if new == nil {
  1433  		new = &SidecarTask{}
  1434  		diff.Type = DiffTypeDeleted
  1435  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1436  	} else {
  1437  		diff.Type = DiffTypeEdited
  1438  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1439  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1440  	}
  1441  
  1442  	// Diff the primitive fields.
  1443  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
  1444  
  1445  	// Config diff
  1446  	if cDiff := configDiff(old.Config, new.Config, contextual); cDiff != nil {
  1447  		diff.Objects = append(diff.Objects, cDiff)
  1448  	}
  1449  
  1450  	// Resources diff
  1451  	if rDiff := old.Resources.Diff(new.Resources, contextual); rDiff != nil {
  1452  		diff.Objects = append(diff.Objects, rDiff)
  1453  	}
  1454  
  1455  	// LogConfig diff
  1456  	lDiff := primitiveObjectDiff(old.LogConfig, new.LogConfig, nil, "LogConfig", contextual)
  1457  	if lDiff != nil {
  1458  		diff.Objects = append(diff.Objects, lDiff)
  1459  	}
  1460  
  1461  	return diff
  1462  }
  1463  
  1464  // consulProxyDiff returns the diff of two ConsulProxy objects.
  1465  // If contextual diff is enabled, all fields will be returned, even if no diff occurred.
  1466  func consulProxyDiff(old, new *ConsulProxy, contextual bool) *ObjectDiff {
  1467  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulProxy"}
  1468  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1469  
  1470  	if reflect.DeepEqual(old, new) {
  1471  		return nil
  1472  	} else if old == nil {
  1473  		old = &ConsulProxy{}
  1474  		diff.Type = DiffTypeAdded
  1475  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1476  	} else if new == nil {
  1477  		new = &ConsulProxy{}
  1478  		diff.Type = DiffTypeDeleted
  1479  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1480  	} else {
  1481  		diff.Type = DiffTypeEdited
  1482  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1483  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1484  	}
  1485  
  1486  	// diff the primitive fields
  1487  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1488  
  1489  	// diff the consul upstream slices
  1490  	if upDiffs := consulProxyUpstreamsDiff(old.Upstreams, new.Upstreams, contextual); upDiffs != nil {
  1491  		diff.Objects = append(diff.Objects, upDiffs...)
  1492  	}
  1493  
  1494  	// diff the config blob
  1495  	if cDiff := configDiff(old.Config, new.Config, contextual); cDiff != nil {
  1496  		diff.Objects = append(diff.Objects, cDiff)
  1497  	}
  1498  
  1499  	return diff
  1500  }
  1501  
  1502  // consulProxyUpstreamsDiff diffs a set of connect upstreams. If contextual diff is
  1503  // enabled, unchanged fields within objects nested in the tasks will be returned.
  1504  func consulProxyUpstreamsDiff(old, new []ConsulUpstream, contextual bool) []*ObjectDiff {
  1505  	oldMap := make(map[string]ConsulUpstream, len(old))
  1506  	newMap := make(map[string]ConsulUpstream, len(new))
  1507  
  1508  	idx := func(up ConsulUpstream) string {
  1509  		return fmt.Sprintf("%s/%s", up.Datacenter, up.DestinationName)
  1510  	}
  1511  
  1512  	for _, o := range old {
  1513  		oldMap[idx(o)] = o
  1514  	}
  1515  	for _, n := range new {
  1516  		newMap[idx(n)] = n
  1517  	}
  1518  
  1519  	var diffs []*ObjectDiff
  1520  	for index, oldUpstream := range oldMap {
  1521  		// Diff the same, deleted, and edited
  1522  		if diff := consulProxyUpstreamDiff(oldUpstream, newMap[index], contextual); diff != nil {
  1523  			diffs = append(diffs, diff)
  1524  		}
  1525  	}
  1526  
  1527  	for index, newUpstream := range newMap {
  1528  		// diff the added
  1529  		if oldUpstream, exists := oldMap[index]; !exists {
  1530  			if diff := consulProxyUpstreamDiff(oldUpstream, newUpstream, contextual); diff != nil {
  1531  				diffs = append(diffs, diff)
  1532  			}
  1533  		}
  1534  	}
  1535  	sort.Sort(ObjectDiffs(diffs))
  1536  	return diffs
  1537  }
  1538  
  1539  func consulProxyUpstreamDiff(prev, next ConsulUpstream, contextual bool) *ObjectDiff {
  1540  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulUpstreams"}
  1541  	var oldPrimFlat, newPrimFlat map[string]string
  1542  
  1543  	if reflect.DeepEqual(prev, next) {
  1544  		return nil
  1545  	} else if prev.Equal(new(ConsulUpstream)) {
  1546  		prev = ConsulUpstream{}
  1547  		diff.Type = DiffTypeAdded
  1548  		newPrimFlat = flatmap.Flatten(next, nil, true)
  1549  	} else if next.Equal(new(ConsulUpstream)) {
  1550  		next = ConsulUpstream{}
  1551  		diff.Type = DiffTypeDeleted
  1552  		oldPrimFlat = flatmap.Flatten(prev, nil, true)
  1553  	} else {
  1554  		diff.Type = DiffTypeEdited
  1555  		oldPrimFlat = flatmap.Flatten(prev, nil, true)
  1556  		newPrimFlat = flatmap.Flatten(next, nil, true)
  1557  	}
  1558  
  1559  	// diff the primitive fields
  1560  	diff.Fields = fieldDiffs(oldPrimFlat, newPrimFlat, contextual)
  1561  
  1562  	// diff the mesh gateway primitive object
  1563  	if mDiff := primitiveObjectDiff(prev.MeshGateway, next.MeshGateway, nil, "MeshGateway", contextual); mDiff != nil {
  1564  		diff.Objects = append(diff.Objects, mDiff)
  1565  	}
  1566  
  1567  	return diff
  1568  }
  1569  
  1570  // serviceCheckDiffs diffs a set of service checks. If contextual diff is
  1571  // enabled, unchanged fields within objects nested in the tasks will be
  1572  // returned.
  1573  func serviceCheckDiffs(old, new []*ServiceCheck, contextual bool) []*ObjectDiff {
  1574  	oldMap := make(map[string]*ServiceCheck, len(old))
  1575  	newMap := make(map[string]*ServiceCheck, len(new))
  1576  	for _, o := range old {
  1577  		oldMap[o.Name] = o
  1578  	}
  1579  	for _, n := range new {
  1580  		newMap[n.Name] = n
  1581  	}
  1582  
  1583  	var diffs []*ObjectDiff
  1584  	for name, oldCheck := range oldMap {
  1585  		// Diff the same, deleted and edited
  1586  		if diff := serviceCheckDiff(oldCheck, newMap[name], contextual); diff != nil {
  1587  			diffs = append(diffs, diff)
  1588  		}
  1589  	}
  1590  
  1591  	for name, newCheck := range newMap {
  1592  		// Diff the added
  1593  		if old, ok := oldMap[name]; !ok {
  1594  			if diff := serviceCheckDiff(old, newCheck, contextual); diff != nil {
  1595  				diffs = append(diffs, diff)
  1596  			}
  1597  		}
  1598  	}
  1599  
  1600  	sort.Sort(ObjectDiffs(diffs))
  1601  	return diffs
  1602  }
  1603  
  1604  // vaultDiff returns the diff of two vault objects. If contextual diff is
  1605  // enabled, all fields will be returned, even if no diff occurred.
  1606  func vaultDiff(old, new *Vault, contextual bool) *ObjectDiff {
  1607  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Vault"}
  1608  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1609  
  1610  	if reflect.DeepEqual(old, new) {
  1611  		return nil
  1612  	} else if old == nil {
  1613  		old = &Vault{}
  1614  		diff.Type = DiffTypeAdded
  1615  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1616  	} else if new == nil {
  1617  		new = &Vault{}
  1618  		diff.Type = DiffTypeDeleted
  1619  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1620  	} else {
  1621  		diff.Type = DiffTypeEdited
  1622  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1623  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1624  	}
  1625  
  1626  	// Diff the primitive fields.
  1627  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1628  
  1629  	// Policies diffs
  1630  	if setDiff := stringSetDiff(old.Policies, new.Policies, "Policies", contextual); setDiff != nil {
  1631  		diff.Objects = append(diff.Objects, setDiff)
  1632  	}
  1633  
  1634  	return diff
  1635  }
  1636  
  1637  // waitConfigDiff returns the diff of two WaitConfig objects. If contextual diff is
  1638  // enabled, all fields will be returned, even if no diff occurred.
  1639  func waitConfigDiff(old, new *WaitConfig, contextual bool) *ObjectDiff {
  1640  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Template"}
  1641  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1642  
  1643  	if reflect.DeepEqual(old, new) {
  1644  		return nil
  1645  	} else if old == nil {
  1646  		diff.Type = DiffTypeAdded
  1647  		newPrimitiveFlat = flatmap.Flatten(new, nil, false)
  1648  	} else if new == nil {
  1649  		diff.Type = DiffTypeDeleted
  1650  		oldPrimitiveFlat = flatmap.Flatten(old, nil, false)
  1651  	} else {
  1652  		diff.Type = DiffTypeEdited
  1653  		oldPrimitiveFlat = flatmap.Flatten(old, nil, false)
  1654  		newPrimitiveFlat = flatmap.Flatten(new, nil, false)
  1655  	}
  1656  
  1657  	// Diff the primitive fields.
  1658  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1659  
  1660  	return diff
  1661  }
  1662  
  1663  // changeScriptDiff returns the diff of two ChangeScript objects. If contextual
  1664  // diff is enabled, all fields will be returned, even if no diff occurred.
  1665  func changeScriptDiff(old, new *ChangeScript, contextual bool) *ObjectDiff {
  1666  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ChangeScript"}
  1667  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1668  
  1669  	if reflect.DeepEqual(old, new) {
  1670  		return nil
  1671  	} else if old == nil {
  1672  		old = &ChangeScript{}
  1673  		diff.Type = DiffTypeAdded
  1674  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1675  	} else if new == nil {
  1676  		new = &ChangeScript{}
  1677  		diff.Type = DiffTypeDeleted
  1678  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1679  	} else {
  1680  		diff.Type = DiffTypeEdited
  1681  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1682  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1683  	}
  1684  
  1685  	// Diff the primitive fields.
  1686  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1687  
  1688  	// Args diffs
  1689  	if setDiff := stringSetDiff(old.Args, new.Args, "Args", contextual); setDiff != nil {
  1690  		diff.Objects = append(diff.Objects, setDiff)
  1691  	}
  1692  
  1693  	return diff
  1694  }
  1695  
  1696  // templateDiff returns the diff of two Consul Template objects. If contextual diff is
  1697  // enabled, all fields will be returned, even if no diff occurred.
  1698  func templateDiff(old, new *Template, contextual bool) *ObjectDiff {
  1699  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Template"}
  1700  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1701  
  1702  	if reflect.DeepEqual(old, new) {
  1703  		return nil
  1704  	} else if old == nil {
  1705  		old = &Template{}
  1706  		diff.Type = DiffTypeAdded
  1707  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1708  	} else if new == nil {
  1709  		new = &Template{}
  1710  		diff.Type = DiffTypeDeleted
  1711  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1712  	} else {
  1713  		diff.Type = DiffTypeEdited
  1714  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1715  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1716  	}
  1717  
  1718  	// Add the pointer primitive fields.
  1719  	if old != nil {
  1720  		if old.Uid != nil {
  1721  			oldPrimitiveFlat["Uid"] = fmt.Sprintf("%v", *old.Uid)
  1722  		}
  1723  		if old.Gid != nil {
  1724  			oldPrimitiveFlat["Gid"] = fmt.Sprintf("%v", *old.Gid)
  1725  		}
  1726  	}
  1727  	if new != nil {
  1728  		if new.Uid != nil {
  1729  			newPrimitiveFlat["Uid"] = fmt.Sprintf("%v", *new.Uid)
  1730  		}
  1731  		if new.Gid != nil {
  1732  			newPrimitiveFlat["Gid"] = fmt.Sprintf("%v", *new.Gid)
  1733  		}
  1734  	}
  1735  
  1736  	// Diff the primitive fields.
  1737  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1738  
  1739  	// WaitConfig diffs
  1740  	if waitDiffs := waitConfigDiff(old.Wait, new.Wait, contextual); waitDiffs != nil {
  1741  		diff.Objects = append(diff.Objects, waitDiffs)
  1742  	}
  1743  
  1744  	// ChangeScript diffs
  1745  	if changeScriptDiffs := changeScriptDiff(
  1746  		old.ChangeScript, new.ChangeScript, contextual,
  1747  	); changeScriptDiffs != nil {
  1748  		diff.Objects = append(diff.Objects, changeScriptDiffs)
  1749  	}
  1750  
  1751  	return diff
  1752  }
  1753  
  1754  // templateDiffs returns the diff of two Consul Template slices. If contextual diff is
  1755  // enabled, all fields will be returned, even if no diff occurred.
  1756  // serviceDiffs diffs a set of services. If contextual diff is enabled, unchanged
  1757  // fields within objects nested in the tasks will be returned.
  1758  func templateDiffs(old, new []*Template, contextual bool) []*ObjectDiff {
  1759  	// Handle trivial case.
  1760  	if len(old) == 1 && len(new) == 1 {
  1761  		if diff := templateDiff(old[0], new[0], contextual); diff != nil {
  1762  			return []*ObjectDiff{diff}
  1763  		}
  1764  		return nil
  1765  	}
  1766  
  1767  	// For each template we will try to find a corresponding match in the other list.
  1768  	// The following lists store the index of the matching template for each
  1769  	// position of the inputs.
  1770  	oldMatches := make([]int, len(old))
  1771  	newMatches := make([]int, len(new))
  1772  
  1773  	// Initialize all templates as unmatched.
  1774  	for i := range oldMatches {
  1775  		oldMatches[i] = -1
  1776  	}
  1777  	for i := range newMatches {
  1778  		newMatches[i] = -1
  1779  	}
  1780  
  1781  	// Find a match in the new templates list for each old template and compute
  1782  	// their diffs.
  1783  	var diffs []*ObjectDiff
  1784  	for oldIndex, oldTemplate := range old {
  1785  		newIndex := findTemplateMatch(oldTemplate, new, newMatches)
  1786  
  1787  		// Old templates that don't have a match were deleted.
  1788  		if newIndex < 0 {
  1789  			diff := templateDiff(oldTemplate, nil, contextual)
  1790  			diffs = append(diffs, diff)
  1791  			continue
  1792  		}
  1793  
  1794  		// If A matches B then B matches A.
  1795  		oldMatches[oldIndex] = newIndex
  1796  		newMatches[newIndex] = oldIndex
  1797  
  1798  		newTemplate := new[newIndex]
  1799  		if diff := templateDiff(oldTemplate, newTemplate, contextual); diff != nil {
  1800  			diffs = append(diffs, diff)
  1801  		}
  1802  	}
  1803  
  1804  	// New templates without match were added.
  1805  	for i, m := range newMatches {
  1806  		if m == -1 {
  1807  			diff := templateDiff(nil, new[i], contextual)
  1808  			diffs = append(diffs, diff)
  1809  		}
  1810  	}
  1811  
  1812  	sort.Sort(ObjectDiffs(diffs))
  1813  	return diffs
  1814  }
  1815  
  1816  func findTemplateMatch(template *Template, newTemplates []*Template, newTemplateMatches []int) int {
  1817  	indexMatch := -1
  1818  
  1819  	for i, newTemplate := range newTemplates {
  1820  		// Skip template if it's already matched.
  1821  		if newTemplateMatches[i] >= 0 {
  1822  			continue
  1823  		}
  1824  
  1825  		if template.DiffID() == newTemplate.DiffID() {
  1826  			indexMatch = i
  1827  			break
  1828  		}
  1829  	}
  1830  
  1831  	return indexMatch
  1832  }
  1833  
  1834  // parameterizedJobDiff returns the diff of two parameterized job objects. If
  1835  // contextual diff is enabled, all fields will be returned, even if no diff
  1836  // occurred.
  1837  func parameterizedJobDiff(old, new *ParameterizedJobConfig, contextual bool) *ObjectDiff {
  1838  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "ParameterizedJob"}
  1839  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1840  
  1841  	if reflect.DeepEqual(old, new) {
  1842  		return nil
  1843  	} else if old == nil {
  1844  		old = &ParameterizedJobConfig{}
  1845  		diff.Type = DiffTypeAdded
  1846  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1847  	} else if new == nil {
  1848  		new = &ParameterizedJobConfig{}
  1849  		diff.Type = DiffTypeDeleted
  1850  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1851  	} else {
  1852  		diff.Type = DiffTypeEdited
  1853  		oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
  1854  		newPrimitiveFlat = flatmap.Flatten(new, nil, true)
  1855  	}
  1856  
  1857  	// Diff the primitive fields.
  1858  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1859  
  1860  	// Meta diffs
  1861  	if optionalDiff := stringSetDiff(old.MetaOptional, new.MetaOptional, "MetaOptional", contextual); optionalDiff != nil {
  1862  		diff.Objects = append(diff.Objects, optionalDiff)
  1863  	}
  1864  
  1865  	if requiredDiff := stringSetDiff(old.MetaRequired, new.MetaRequired, "MetaRequired", contextual); requiredDiff != nil {
  1866  		diff.Objects = append(diff.Objects, requiredDiff)
  1867  	}
  1868  
  1869  	return diff
  1870  }
  1871  
  1872  func multiregionDiff(old, new *Multiregion, contextual bool) *ObjectDiff {
  1873  
  1874  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Multiregion"}
  1875  
  1876  	if reflect.DeepEqual(old, new) {
  1877  		return nil
  1878  	} else if old == nil {
  1879  		old = &Multiregion{}
  1880  		old.Canonicalize()
  1881  		diff.Type = DiffTypeAdded
  1882  	} else if new == nil {
  1883  		new = &Multiregion{}
  1884  		diff.Type = DiffTypeDeleted
  1885  	} else {
  1886  		diff.Type = DiffTypeEdited
  1887  	}
  1888  
  1889  	// strategy diff
  1890  	stratDiff := primitiveObjectDiff(
  1891  		old.Strategy,
  1892  		new.Strategy,
  1893  		[]string{},
  1894  		"Strategy",
  1895  		contextual)
  1896  	if stratDiff != nil {
  1897  		diff.Objects = append(diff.Objects, stratDiff)
  1898  	}
  1899  
  1900  	oldMap := make(map[string]*MultiregionRegion, len(old.Regions))
  1901  	newMap := make(map[string]*MultiregionRegion, len(new.Regions))
  1902  	for _, o := range old.Regions {
  1903  		oldMap[o.Name] = o
  1904  	}
  1905  	for _, n := range new.Regions {
  1906  		newMap[n.Name] = n
  1907  	}
  1908  
  1909  	for name, oldRegion := range oldMap {
  1910  		// Diff the same, deleted and edited
  1911  		newRegion := newMap[name]
  1912  		rdiff := multiregionRegionDiff(oldRegion, newRegion, contextual)
  1913  		if rdiff != nil {
  1914  			diff.Objects = append(diff.Objects, rdiff)
  1915  		}
  1916  	}
  1917  
  1918  	for name, newRegion := range newMap {
  1919  		// Diff the added
  1920  		if oldRegion, ok := oldMap[name]; !ok {
  1921  			rdiff := multiregionRegionDiff(oldRegion, newRegion, contextual)
  1922  			if rdiff != nil {
  1923  				diff.Objects = append(diff.Objects, rdiff)
  1924  			}
  1925  		}
  1926  	}
  1927  	sort.Sort(FieldDiffs(diff.Fields))
  1928  	sort.Sort(ObjectDiffs(diff.Objects))
  1929  	return diff
  1930  }
  1931  
  1932  func multiregionRegionDiff(r, other *MultiregionRegion, contextual bool) *ObjectDiff {
  1933  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Region"}
  1934  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  1935  
  1936  	if reflect.DeepEqual(r, other) {
  1937  		return nil
  1938  	} else if r == nil {
  1939  		r = &MultiregionRegion{}
  1940  		diff.Type = DiffTypeAdded
  1941  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  1942  	} else if other == nil {
  1943  		other = &MultiregionRegion{}
  1944  		diff.Type = DiffTypeDeleted
  1945  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  1946  	} else {
  1947  		diff.Type = DiffTypeEdited
  1948  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  1949  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  1950  	}
  1951  
  1952  	// Diff the primitive fields.
  1953  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  1954  
  1955  	// Datacenters diff
  1956  	setDiff := stringSetDiff(r.Datacenters, other.Datacenters, "Datacenters", contextual)
  1957  	if setDiff != nil && setDiff.Type != DiffTypeNone {
  1958  		diff.Objects = append(diff.Objects, setDiff)
  1959  	}
  1960  
  1961  	sort.Sort(ObjectDiffs(diff.Objects))
  1962  	sort.Sort(FieldDiffs(diff.Fields))
  1963  
  1964  	var added, deleted, edited bool
  1965  Loop:
  1966  	for _, f := range diff.Fields {
  1967  		switch f.Type {
  1968  		case DiffTypeEdited:
  1969  			edited = true
  1970  			break Loop
  1971  		case DiffTypeDeleted:
  1972  			deleted = true
  1973  		case DiffTypeAdded:
  1974  			added = true
  1975  		}
  1976  	}
  1977  
  1978  	if edited || added && deleted {
  1979  		diff.Type = DiffTypeEdited
  1980  	} else if added {
  1981  		diff.Type = DiffTypeAdded
  1982  	} else if deleted {
  1983  		diff.Type = DiffTypeDeleted
  1984  	} else {
  1985  		return nil
  1986  	}
  1987  
  1988  	return diff
  1989  }
  1990  
  1991  // volumeDiffs returns the diff of a group's volume requests. If contextual
  1992  // diff is enabled, all fields will be returned, even if no diff occurred.
  1993  func volumeDiffs(oldVR, newVR map[string]*VolumeRequest, contextual bool) []*ObjectDiff {
  1994  	if reflect.DeepEqual(oldVR, newVR) {
  1995  		return nil
  1996  	}
  1997  
  1998  	diffs := []*ObjectDiff{} //Type: DiffTypeNone, Name: "Volumes"}
  1999  	seen := map[string]bool{}
  2000  	for name, oReq := range oldVR {
  2001  		nReq := newVR[name] // might be nil, that's ok
  2002  		seen[name] = true
  2003  		diff := volumeDiff(oReq, nReq, contextual)
  2004  		if diff != nil {
  2005  			diffs = append(diffs, diff)
  2006  		}
  2007  	}
  2008  	for name, nReq := range newVR {
  2009  		if !seen[name] {
  2010  			// we know old is nil at this point, or we'd have hit it before
  2011  			diff := volumeDiff(nil, nReq, contextual)
  2012  			if diff != nil {
  2013  				diffs = append(diffs, diff)
  2014  			}
  2015  		}
  2016  	}
  2017  	return diffs
  2018  }
  2019  
  2020  // volumeDiff returns the diff between two volume requests. If contextual diff
  2021  // is enabled, all fields will be returned, even if no diff occurred.
  2022  func volumeDiff(oldVR, newVR *VolumeRequest, contextual bool) *ObjectDiff {
  2023  	if reflect.DeepEqual(oldVR, newVR) {
  2024  		return nil
  2025  	}
  2026  
  2027  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Volume"}
  2028  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2029  
  2030  	if oldVR == nil {
  2031  		oldVR = &VolumeRequest{}
  2032  		diff.Type = DiffTypeAdded
  2033  		newPrimitiveFlat = flatmap.Flatten(newVR, nil, true)
  2034  	} else if newVR == nil {
  2035  		newVR = &VolumeRequest{}
  2036  		diff.Type = DiffTypeDeleted
  2037  		oldPrimitiveFlat = flatmap.Flatten(oldVR, nil, true)
  2038  	} else {
  2039  		diff.Type = DiffTypeEdited
  2040  		oldPrimitiveFlat = flatmap.Flatten(oldVR, nil, true)
  2041  		newPrimitiveFlat = flatmap.Flatten(newVR, nil, true)
  2042  	}
  2043  
  2044  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2045  
  2046  	mOptsDiff := volumeCSIMountOptionsDiff(oldVR.MountOptions, newVR.MountOptions, contextual)
  2047  	if mOptsDiff != nil {
  2048  		diff.Objects = append(diff.Objects, mOptsDiff)
  2049  	}
  2050  
  2051  	return diff
  2052  }
  2053  
  2054  // volumeCSIMountOptionsDiff returns the diff between volume mount options. If
  2055  // contextual diff is enabled, all fields will be returned, even if no diff
  2056  // occurred.
  2057  func volumeCSIMountOptionsDiff(oldMO, newMO *CSIMountOptions, contextual bool) *ObjectDiff {
  2058  	if reflect.DeepEqual(oldMO, newMO) {
  2059  		return nil
  2060  	}
  2061  
  2062  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "MountOptions"}
  2063  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2064  
  2065  	if oldMO == nil && newMO != nil {
  2066  		oldMO = &CSIMountOptions{}
  2067  		diff.Type = DiffTypeAdded
  2068  		newPrimitiveFlat = flatmap.Flatten(newMO, nil, true)
  2069  	} else if oldMO != nil && newMO == nil {
  2070  		newMO = &CSIMountOptions{}
  2071  		diff.Type = DiffTypeDeleted
  2072  		oldPrimitiveFlat = flatmap.Flatten(oldMO, nil, true)
  2073  	} else {
  2074  		diff.Type = DiffTypeEdited
  2075  		oldPrimitiveFlat = flatmap.Flatten(oldMO, nil, true)
  2076  		newPrimitiveFlat = flatmap.Flatten(newMO, nil, true)
  2077  	}
  2078  
  2079  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2080  
  2081  	setDiff := stringSetDiff(oldMO.MountFlags, newMO.MountFlags, "MountFlags", contextual)
  2082  	if setDiff != nil {
  2083  		diff.Objects = append(diff.Objects, setDiff)
  2084  	}
  2085  	return diff
  2086  }
  2087  
  2088  // Diff returns a diff of two resource objects. If contextual diff is enabled,
  2089  // non-changed fields will still be returned.
  2090  func (r *Resources) Diff(other *Resources, contextual bool) *ObjectDiff {
  2091  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Resources"}
  2092  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2093  
  2094  	if reflect.DeepEqual(r, other) {
  2095  		return nil
  2096  	} else if r == nil {
  2097  		r = &Resources{}
  2098  		diff.Type = DiffTypeAdded
  2099  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  2100  	} else if other == nil {
  2101  		other = &Resources{}
  2102  		diff.Type = DiffTypeDeleted
  2103  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  2104  	} else {
  2105  		diff.Type = DiffTypeEdited
  2106  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  2107  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  2108  	}
  2109  
  2110  	// Diff the primitive fields.
  2111  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2112  
  2113  	// Network Resources diff
  2114  	if nDiffs := networkResourceDiffs(r.Networks, other.Networks, contextual); nDiffs != nil {
  2115  		diff.Objects = append(diff.Objects, nDiffs...)
  2116  	}
  2117  
  2118  	// Requested Devices diff
  2119  	if nDiffs := requestedDevicesDiffs(r.Devices, other.Devices, contextual); nDiffs != nil {
  2120  		diff.Objects = append(diff.Objects, nDiffs...)
  2121  	}
  2122  
  2123  	return diff
  2124  }
  2125  
  2126  // Diff returns a diff of two network resources. If contextual diff is enabled,
  2127  // non-changed fields will still be returned.
  2128  func (n *NetworkResource) Diff(other *NetworkResource, contextual bool) *ObjectDiff {
  2129  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Network"}
  2130  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2131  	filter := []string{"Device", "CIDR", "IP"}
  2132  
  2133  	if reflect.DeepEqual(n, other) {
  2134  		return nil
  2135  	} else if n == nil {
  2136  		n = &NetworkResource{}
  2137  		diff.Type = DiffTypeAdded
  2138  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
  2139  	} else if other == nil {
  2140  		other = &NetworkResource{}
  2141  		diff.Type = DiffTypeDeleted
  2142  		oldPrimitiveFlat = flatmap.Flatten(n, filter, true)
  2143  	} else {
  2144  		diff.Type = DiffTypeEdited
  2145  		oldPrimitiveFlat = flatmap.Flatten(n, filter, true)
  2146  		newPrimitiveFlat = flatmap.Flatten(other, filter, true)
  2147  	}
  2148  
  2149  	// Diff the primitive fields.
  2150  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2151  
  2152  	// Port diffs
  2153  	resPorts := portDiffs(n.ReservedPorts, other.ReservedPorts, false, contextual)
  2154  	dynPorts := portDiffs(n.DynamicPorts, other.DynamicPorts, true, contextual)
  2155  	if resPorts != nil {
  2156  		diff.Objects = append(diff.Objects, resPorts...)
  2157  	}
  2158  	if dynPorts != nil {
  2159  		diff.Objects = append(diff.Objects, dynPorts...)
  2160  	}
  2161  
  2162  	if dnsDiff := n.DNS.Diff(other.DNS, contextual); dnsDiff != nil {
  2163  		diff.Objects = append(diff.Objects, dnsDiff)
  2164  	}
  2165  
  2166  	return diff
  2167  }
  2168  
  2169  // Diff returns a diff of two DNSConfig structs
  2170  func (d *DNSConfig) Diff(other *DNSConfig, contextual bool) *ObjectDiff {
  2171  	if reflect.DeepEqual(d, other) {
  2172  		return nil
  2173  	}
  2174  
  2175  	flatten := func(conf *DNSConfig) map[string]string {
  2176  		m := map[string]string{}
  2177  		if len(conf.Servers) > 0 {
  2178  			m["Servers"] = strings.Join(conf.Servers, ",")
  2179  		}
  2180  		if len(conf.Searches) > 0 {
  2181  			m["Searches"] = strings.Join(conf.Searches, ",")
  2182  		}
  2183  		if len(conf.Options) > 0 {
  2184  			m["Options"] = strings.Join(conf.Options, ",")
  2185  		}
  2186  		return m
  2187  	}
  2188  
  2189  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "DNS"}
  2190  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2191  	if d == nil {
  2192  		diff.Type = DiffTypeAdded
  2193  		newPrimitiveFlat = flatten(other)
  2194  	} else if other == nil {
  2195  		diff.Type = DiffTypeDeleted
  2196  		oldPrimitiveFlat = flatten(d)
  2197  	} else {
  2198  		diff.Type = DiffTypeEdited
  2199  		oldPrimitiveFlat = flatten(d)
  2200  		newPrimitiveFlat = flatten(other)
  2201  	}
  2202  
  2203  	// Diff the primitive fields.
  2204  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2205  
  2206  	return diff
  2207  }
  2208  
  2209  // networkResourceDiffs diffs a set of NetworkResources. If contextual diff is enabled,
  2210  // non-changed fields will still be returned.
  2211  func networkResourceDiffs(old, new []*NetworkResource, contextual bool) []*ObjectDiff {
  2212  	makeSet := func(objects []*NetworkResource) map[string]*NetworkResource {
  2213  		objMap := make(map[string]*NetworkResource, len(objects))
  2214  		for _, obj := range objects {
  2215  			hash, err := hashstructure.Hash(obj, nil)
  2216  			if err != nil {
  2217  				panic(err)
  2218  			}
  2219  			objMap[fmt.Sprintf("%d", hash)] = obj
  2220  		}
  2221  
  2222  		return objMap
  2223  	}
  2224  
  2225  	oldSet := makeSet(old)
  2226  	newSet := makeSet(new)
  2227  
  2228  	var diffs []*ObjectDiff
  2229  	for k, oldV := range oldSet {
  2230  		if newV, ok := newSet[k]; !ok {
  2231  			if diff := oldV.Diff(newV, contextual); diff != nil {
  2232  				diffs = append(diffs, diff)
  2233  			}
  2234  		}
  2235  	}
  2236  	for k, newV := range newSet {
  2237  		if oldV, ok := oldSet[k]; !ok {
  2238  			if diff := oldV.Diff(newV, contextual); diff != nil {
  2239  				diffs = append(diffs, diff)
  2240  			}
  2241  		}
  2242  	}
  2243  
  2244  	sort.Sort(ObjectDiffs(diffs))
  2245  	return diffs
  2246  
  2247  }
  2248  
  2249  // portDiffs returns the diff of two sets of ports. The dynamic flag marks the
  2250  // set of ports as being Dynamic ports versus Static ports. If contextual diff is enabled,
  2251  // non-changed fields will still be returned.
  2252  func portDiffs(old, new []Port, dynamic bool, contextual bool) []*ObjectDiff {
  2253  	makeSet := func(ports []Port) map[string]Port {
  2254  		portMap := make(map[string]Port, len(ports))
  2255  		for _, port := range ports {
  2256  			portMap[port.Label] = port
  2257  		}
  2258  
  2259  		return portMap
  2260  	}
  2261  
  2262  	oldPorts := makeSet(old)
  2263  	newPorts := makeSet(new)
  2264  
  2265  	var filter []string
  2266  	name := "Static Port"
  2267  	if dynamic {
  2268  		filter = []string{"Value"}
  2269  		name = "Dynamic Port"
  2270  	}
  2271  
  2272  	var diffs []*ObjectDiff
  2273  	for portLabel, oldPort := range oldPorts {
  2274  		// Diff the same, deleted and edited
  2275  		if newPort, ok := newPorts[portLabel]; ok {
  2276  			diff := primitiveObjectDiff(oldPort, newPort, filter, name, contextual)
  2277  			if diff != nil {
  2278  				diffs = append(diffs, diff)
  2279  			}
  2280  		} else {
  2281  			diff := primitiveObjectDiff(oldPort, nil, filter, name, contextual)
  2282  			if diff != nil {
  2283  				diffs = append(diffs, diff)
  2284  			}
  2285  		}
  2286  	}
  2287  	for label, newPort := range newPorts {
  2288  		// Diff the added
  2289  		if _, ok := oldPorts[label]; !ok {
  2290  			diff := primitiveObjectDiff(nil, newPort, filter, name, contextual)
  2291  			if diff != nil {
  2292  				diffs = append(diffs, diff)
  2293  			}
  2294  		}
  2295  	}
  2296  
  2297  	sort.Sort(ObjectDiffs(diffs))
  2298  	return diffs
  2299  
  2300  }
  2301  
  2302  // Diff returns a diff of two requested devices. If contextual diff is enabled,
  2303  // non-changed fields will still be returned.
  2304  func (r *RequestedDevice) Diff(other *RequestedDevice, contextual bool) *ObjectDiff {
  2305  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Device"}
  2306  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2307  
  2308  	if reflect.DeepEqual(r, other) {
  2309  		return nil
  2310  	} else if r == nil {
  2311  		diff.Type = DiffTypeAdded
  2312  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  2313  	} else if other == nil {
  2314  		diff.Type = DiffTypeDeleted
  2315  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  2316  	} else {
  2317  		diff.Type = DiffTypeEdited
  2318  		oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
  2319  		newPrimitiveFlat = flatmap.Flatten(other, nil, true)
  2320  	}
  2321  
  2322  	// Diff the primitive fields.
  2323  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2324  
  2325  	return diff
  2326  }
  2327  
  2328  // requestedDevicesDiffs diffs a set of RequestedDevices. If contextual diff is enabled,
  2329  // non-changed fields will still be returned.
  2330  func requestedDevicesDiffs(old, new []*RequestedDevice, contextual bool) []*ObjectDiff {
  2331  	makeSet := func(devices []*RequestedDevice) map[string]*RequestedDevice {
  2332  		deviceMap := make(map[string]*RequestedDevice, len(devices))
  2333  		for _, d := range devices {
  2334  			deviceMap[d.Name] = d
  2335  		}
  2336  
  2337  		return deviceMap
  2338  	}
  2339  
  2340  	oldSet := makeSet(old)
  2341  	newSet := makeSet(new)
  2342  
  2343  	var diffs []*ObjectDiff
  2344  	for k, oldV := range oldSet {
  2345  		newV := newSet[k]
  2346  		if diff := oldV.Diff(newV, contextual); diff != nil {
  2347  			diffs = append(diffs, diff)
  2348  		}
  2349  	}
  2350  	for k, newV := range newSet {
  2351  		if oldV, ok := oldSet[k]; !ok {
  2352  			if diff := oldV.Diff(newV, contextual); diff != nil {
  2353  				diffs = append(diffs, diff)
  2354  			}
  2355  		}
  2356  	}
  2357  
  2358  	sort.Sort(ObjectDiffs(diffs))
  2359  	return diffs
  2360  
  2361  }
  2362  
  2363  // configDiff returns the diff of two Task Config objects. If contextual diff is
  2364  // enabled, all fields will be returned, even if no diff occurred.
  2365  func configDiff(old, new map[string]interface{}, contextual bool) *ObjectDiff {
  2366  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Config"}
  2367  	if reflect.DeepEqual(old, new) {
  2368  		return nil
  2369  	} else if len(old) == 0 {
  2370  		diff.Type = DiffTypeAdded
  2371  	} else if len(new) == 0 {
  2372  		diff.Type = DiffTypeDeleted
  2373  	} else {
  2374  		diff.Type = DiffTypeEdited
  2375  	}
  2376  
  2377  	// Diff the primitive fields.
  2378  	oldPrimitiveFlat := flatmap.Flatten(old, nil, false)
  2379  	newPrimitiveFlat := flatmap.Flatten(new, nil, false)
  2380  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2381  	return diff
  2382  }
  2383  
  2384  // idDiff returns the diff of two identity objects. If contextual diff is
  2385  // enabled, all fields will be returned, even if no diff occurred.
  2386  func idDiff(oldWI, newWI *WorkloadIdentity, contextual bool) *ObjectDiff {
  2387  	diff := &ObjectDiff{Type: DiffTypeNone, Name: "Identity"}
  2388  	var oldPrimitiveFlat, newPrimitiveFlat map[string]string
  2389  
  2390  	if reflect.DeepEqual(oldWI, newWI) {
  2391  		return nil
  2392  	} else if oldWI == nil {
  2393  		diff.Type = DiffTypeAdded
  2394  		newPrimitiveFlat = flatmap.Flatten(newWI, nil, true)
  2395  	} else if newWI == nil {
  2396  		diff.Type = DiffTypeDeleted
  2397  		oldPrimitiveFlat = flatmap.Flatten(oldWI, nil, true)
  2398  	} else {
  2399  		diff.Type = DiffTypeEdited
  2400  		oldPrimitiveFlat = flatmap.Flatten(oldWI, nil, true)
  2401  		newPrimitiveFlat = flatmap.Flatten(newWI, nil, true)
  2402  	}
  2403  
  2404  	// Diff the primitive fields.
  2405  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2406  
  2407  	return diff
  2408  }
  2409  
  2410  // ObjectDiff contains the diff of two generic objects.
  2411  type ObjectDiff struct {
  2412  	Type    DiffType
  2413  	Name    string
  2414  	Fields  []*FieldDiff
  2415  	Objects []*ObjectDiff
  2416  }
  2417  
  2418  func (o *ObjectDiff) GoString() string {
  2419  	out := fmt.Sprintf("\n%q (%s) {\n", o.Name, o.Type)
  2420  	for _, f := range o.Fields {
  2421  		out += fmt.Sprintf("%#v\n", f)
  2422  	}
  2423  	for _, o := range o.Objects {
  2424  		out += fmt.Sprintf("%#v\n", o)
  2425  	}
  2426  	out += "}"
  2427  	return out
  2428  }
  2429  
  2430  func (o *ObjectDiff) Less(other *ObjectDiff) bool {
  2431  	if reflect.DeepEqual(o, other) {
  2432  		return false
  2433  	} else if other == nil {
  2434  		return false
  2435  	} else if o == nil {
  2436  		return true
  2437  	}
  2438  
  2439  	if o.Name != other.Name {
  2440  		return o.Name < other.Name
  2441  	}
  2442  
  2443  	if o.Type != other.Type {
  2444  		return o.Type.Less(other.Type)
  2445  	}
  2446  
  2447  	if lO, lOther := len(o.Fields), len(other.Fields); lO != lOther {
  2448  		return lO < lOther
  2449  	}
  2450  
  2451  	if lO, lOther := len(o.Objects), len(other.Objects); lO != lOther {
  2452  		return lO < lOther
  2453  	}
  2454  
  2455  	// Check each field
  2456  	sort.Sort(FieldDiffs(o.Fields))
  2457  	sort.Sort(FieldDiffs(other.Fields))
  2458  
  2459  	for i, oV := range o.Fields {
  2460  		if oV.Less(other.Fields[i]) {
  2461  			return true
  2462  		}
  2463  	}
  2464  
  2465  	// Check each object
  2466  	sort.Sort(ObjectDiffs(o.Objects))
  2467  	sort.Sort(ObjectDiffs(other.Objects))
  2468  	for i, oV := range o.Objects {
  2469  		if oV.Less(other.Objects[i]) {
  2470  			return true
  2471  		}
  2472  	}
  2473  
  2474  	return false
  2475  }
  2476  
  2477  // For sorting ObjectDiffs
  2478  type ObjectDiffs []*ObjectDiff
  2479  
  2480  func (o ObjectDiffs) Len() int           { return len(o) }
  2481  func (o ObjectDiffs) Swap(i, j int)      { o[i], o[j] = o[j], o[i] }
  2482  func (o ObjectDiffs) Less(i, j int) bool { return o[i].Less(o[j]) }
  2483  
  2484  type FieldDiff struct {
  2485  	Type        DiffType
  2486  	Name        string
  2487  	Old, New    string
  2488  	Annotations []string
  2489  }
  2490  
  2491  // fieldDiff returns a FieldDiff if old and new are different otherwise, it
  2492  // returns nil. If contextual diff is enabled, even non-changed fields will be
  2493  // returned.
  2494  func fieldDiff(old, new, name string, contextual bool) *FieldDiff {
  2495  	diff := &FieldDiff{Name: name, Type: DiffTypeNone}
  2496  	if old == new {
  2497  		if !contextual {
  2498  			return nil
  2499  		}
  2500  		diff.Old, diff.New = old, new
  2501  		return diff
  2502  	}
  2503  
  2504  	if old == "" {
  2505  		diff.Type = DiffTypeAdded
  2506  		diff.New = new
  2507  	} else if new == "" {
  2508  		diff.Type = DiffTypeDeleted
  2509  		diff.Old = old
  2510  	} else {
  2511  		diff.Type = DiffTypeEdited
  2512  		diff.Old = old
  2513  		diff.New = new
  2514  	}
  2515  	return diff
  2516  }
  2517  
  2518  func (f *FieldDiff) GoString() string {
  2519  	out := fmt.Sprintf("%q (%s): %q => %q", f.Name, f.Type, f.Old, f.New)
  2520  	if len(f.Annotations) != 0 {
  2521  		out += fmt.Sprintf(" (%s)", strings.Join(f.Annotations, ", "))
  2522  	}
  2523  
  2524  	return out
  2525  }
  2526  
  2527  func (f *FieldDiff) Less(other *FieldDiff) bool {
  2528  	if reflect.DeepEqual(f, other) {
  2529  		return false
  2530  	} else if other == nil {
  2531  		return false
  2532  	} else if f == nil {
  2533  		return true
  2534  	}
  2535  
  2536  	if f.Name != other.Name {
  2537  		return f.Name < other.Name
  2538  	} else if f.Old != other.Old {
  2539  		return f.Old < other.Old
  2540  	}
  2541  
  2542  	return f.New < other.New
  2543  }
  2544  
  2545  // For sorting FieldDiffs
  2546  type FieldDiffs []*FieldDiff
  2547  
  2548  func (f FieldDiffs) Len() int           { return len(f) }
  2549  func (f FieldDiffs) Swap(i, j int)      { f[i], f[j] = f[j], f[i] }
  2550  func (f FieldDiffs) Less(i, j int) bool { return f[i].Less(f[j]) }
  2551  
  2552  // fieldDiffs takes a map of field names to their values and returns a set of
  2553  // field diffs. If contextual diff is enabled, even non-changed fields will be
  2554  // returned.
  2555  func fieldDiffs(old, new map[string]string, contextual bool) []*FieldDiff {
  2556  	var diffs []*FieldDiff
  2557  	visited := make(map[string]struct{})
  2558  	for k, oldV := range old {
  2559  		visited[k] = struct{}{}
  2560  		newV := new[k]
  2561  		if diff := fieldDiff(oldV, newV, k, contextual); diff != nil {
  2562  			diffs = append(diffs, diff)
  2563  		}
  2564  	}
  2565  
  2566  	for k, newV := range new {
  2567  		if _, ok := visited[k]; !ok {
  2568  			if diff := fieldDiff("", newV, k, contextual); diff != nil {
  2569  				diffs = append(diffs, diff)
  2570  			}
  2571  		}
  2572  	}
  2573  
  2574  	sort.Sort(FieldDiffs(diffs))
  2575  	return diffs
  2576  }
  2577  
  2578  // stringSetDiff diffs two sets of strings with the given name.
  2579  func stringSetDiff(old, new []string, name string, contextual bool) *ObjectDiff {
  2580  	oldMap := make(map[string]struct{}, len(old))
  2581  	newMap := make(map[string]struct{}, len(new))
  2582  	for _, o := range old {
  2583  		oldMap[o] = struct{}{}
  2584  	}
  2585  	for _, n := range new {
  2586  		newMap[n] = struct{}{}
  2587  	}
  2588  	if reflect.DeepEqual(oldMap, newMap) && !contextual {
  2589  		return nil
  2590  	}
  2591  
  2592  	diff := &ObjectDiff{Name: name}
  2593  	var added, removed bool
  2594  	for k := range oldMap {
  2595  		if _, ok := newMap[k]; !ok {
  2596  			diff.Fields = append(diff.Fields, fieldDiff(k, "", name, contextual))
  2597  			removed = true
  2598  		} else if contextual {
  2599  			diff.Fields = append(diff.Fields, fieldDiff(k, k, name, contextual))
  2600  		}
  2601  	}
  2602  
  2603  	for k := range newMap {
  2604  		if _, ok := oldMap[k]; !ok {
  2605  			diff.Fields = append(diff.Fields, fieldDiff("", k, name, contextual))
  2606  			added = true
  2607  		}
  2608  	}
  2609  
  2610  	sort.Sort(FieldDiffs(diff.Fields))
  2611  
  2612  	// Determine the type
  2613  	if added && removed {
  2614  		diff.Type = DiffTypeEdited
  2615  	} else if added {
  2616  		diff.Type = DiffTypeAdded
  2617  	} else if removed {
  2618  		diff.Type = DiffTypeDeleted
  2619  	} else {
  2620  		// Diff of an empty set
  2621  		if len(diff.Fields) == 0 {
  2622  			return nil
  2623  		}
  2624  
  2625  		diff.Type = DiffTypeNone
  2626  	}
  2627  
  2628  	return diff
  2629  }
  2630  
  2631  // primitiveObjectDiff returns a diff of the passed objects' primitive fields.
  2632  // The filter field can be used to exclude fields from the diff. The name is the
  2633  // name of the objects. If contextual is set, non-changed fields will also be
  2634  // stored in the object diff.
  2635  func primitiveObjectDiff(old, new interface{}, filter []string, name string, contextual bool) *ObjectDiff {
  2636  	oldPrimitiveFlat := flatmap.Flatten(old, filter, true)
  2637  	newPrimitiveFlat := flatmap.Flatten(new, filter, true)
  2638  	delete(oldPrimitiveFlat, "")
  2639  	delete(newPrimitiveFlat, "")
  2640  
  2641  	diff := &ObjectDiff{Name: name}
  2642  	diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
  2643  
  2644  	var added, deleted, edited bool
  2645  Loop:
  2646  	for _, f := range diff.Fields {
  2647  		switch f.Type {
  2648  		case DiffTypeEdited:
  2649  			edited = true
  2650  			break Loop
  2651  		case DiffTypeDeleted:
  2652  			deleted = true
  2653  		case DiffTypeAdded:
  2654  			added = true
  2655  		}
  2656  	}
  2657  
  2658  	if edited || added && deleted {
  2659  		diff.Type = DiffTypeEdited
  2660  	} else if added {
  2661  		diff.Type = DiffTypeAdded
  2662  	} else if deleted {
  2663  		diff.Type = DiffTypeDeleted
  2664  	} else {
  2665  		return nil
  2666  	}
  2667  
  2668  	return diff
  2669  }
  2670  
  2671  // primitiveObjectSetDiff does a set difference of the old and new sets. The
  2672  // filter parameter can be used to filter a set of primitive fields in the
  2673  // passed structs. The name corresponds to the name of the passed objects. If
  2674  // contextual diff is enabled, objects' primitive fields will be returned even if
  2675  // no diff exists.
  2676  func primitiveObjectSetDiff(old, new []interface{}, filter []string, name string, contextual bool) []*ObjectDiff {
  2677  	makeSet := func(objects []interface{}) map[string]interface{} {
  2678  		objMap := make(map[string]interface{}, len(objects))
  2679  		for _, obj := range objects {
  2680  			var key string
  2681  
  2682  			if diffable, ok := obj.(DiffableWithID); ok {
  2683  				key = diffable.DiffID()
  2684  			}
  2685  
  2686  			if key == "" {
  2687  				hash, err := hashstructure.Hash(obj, nil)
  2688  				if err != nil {
  2689  					panic(err)
  2690  				}
  2691  				key = fmt.Sprintf("%d", hash)
  2692  			}
  2693  			objMap[key] = obj
  2694  		}
  2695  
  2696  		return objMap
  2697  	}
  2698  
  2699  	oldSet := makeSet(old)
  2700  	newSet := makeSet(new)
  2701  
  2702  	var diffs []*ObjectDiff
  2703  	for k, oldObj := range oldSet {
  2704  		newObj := newSet[k]
  2705  		diff := primitiveObjectDiff(oldObj, newObj, filter, name, contextual)
  2706  		if diff != nil {
  2707  			diffs = append(diffs, diff)
  2708  		}
  2709  	}
  2710  	for k, v := range newSet {
  2711  		// Added
  2712  		if _, ok := oldSet[k]; !ok {
  2713  			diffs = append(diffs, primitiveObjectDiff(nil, v, filter, name, contextual))
  2714  		}
  2715  	}
  2716  
  2717  	sort.Sort(ObjectDiffs(diffs))
  2718  	return diffs
  2719  }
  2720  
  2721  // interfaceSlice is a helper method that takes a slice of typed elements and
  2722  // returns a slice of interface. This method will panic if given a non-slice
  2723  // input.
  2724  func interfaceSlice(slice interface{}) []interface{} {
  2725  	s := reflect.ValueOf(slice)
  2726  	if s.Kind() != reflect.Slice {
  2727  		panic("InterfaceSlice() given a non-slice type")
  2728  	}
  2729  
  2730  	ret := make([]interface{}, s.Len())
  2731  
  2732  	for i := 0; i < s.Len(); i++ {
  2733  		ret[i] = s.Index(i).Interface()
  2734  	}
  2735  
  2736  	return ret
  2737  }