open-nomad/nomad/structs/diff.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2701 lines
77 KiB
Go
Raw Normal View History

2016-05-11 05:23:34 +00:00
package structs
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/hashicorp/nomad/helper/flatmap"
"github.com/mitchellh/hashstructure"
)
// DiffableWithID defines an object that has a unique and stable value that can
// be used as an identifier when generating a diff.
type DiffableWithID interface {
// DiffID returns the value to use to match entities between the old and
// the new input.
DiffID() string
}
2016-05-11 05:23:34 +00:00
// DiffType denotes the type of a diff object.
type DiffType string
var (
DiffTypeNone DiffType = "None"
DiffTypeAdded DiffType = "Added"
DiffTypeDeleted DiffType = "Deleted"
DiffTypeEdited DiffType = "Edited"
)
func (d DiffType) Less(other DiffType) bool {
// Edited > Added > Deleted > None
// But we do a reverse sort
if d == other {
return false
}
if d == DiffTypeEdited {
return true
} else if other == DiffTypeEdited {
return false
} else if d == DiffTypeAdded {
return true
} else if other == DiffTypeAdded {
return false
} else if d == DiffTypeDeleted {
return true
} else if other == DiffTypeDeleted {
return false
}
return true
}
// JobDiff contains the diff of two jobs.
type JobDiff struct {
Type DiffType
ID string
Fields []*FieldDiff
Objects []*ObjectDiff
TaskGroups []*TaskGroupDiff
}
// Diff returns a diff of two jobs and a potential error if the Jobs are not
2016-05-11 18:10:09 +00:00
// diffable. If contextual diff is enabled, objects within the job will contain
// field information even if unchanged.
func (j *Job) Diff(other *Job, contextual bool) (*JobDiff, error) {
// See agent.ApiJobToStructJob Update is a default for TaskGroups
2016-05-11 05:23:34 +00:00
diff := &JobDiff{Type: DiffTypeNone}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
2017-04-16 23:54:02 +00:00
filter := []string{"ID", "Status", "StatusDescription", "Version", "Stable", "CreateIndex",
"ModifyIndex", "JobModifyIndex", "Update", "SubmitTime", "NomadTokenID", "VaultToken"}
2016-05-13 18:53:11 +00:00
2016-05-11 05:23:34 +00:00
if j == nil && other == nil {
return diff, nil
} else if j == nil {
j = &Job{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
diff.ID = other.ID
} else if other == nil {
other = &Job{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(j, filter, true)
diff.ID = j.ID
} else {
if j.ID != other.ID {
return nil, fmt.Errorf("can not diff jobs with different IDs: %q and %q", j.ID, other.ID)
}
oldPrimitiveFlat = flatmap.Flatten(j, filter, true)
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
diff.ID = other.ID
}
// Diff the primitive fields.
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
2016-05-11 05:23:34 +00:00
// Datacenters diff
Fix diff alignment and remove no change DC Old Output: ``` +/- Job: "example" Datacenters { Datacenters: "dc1" } +/- Task Group: "cache" (1 create/destroy update) +/- RestartPolicy { +/- Attempts: "10" => "9" Delay: "25000000000" Interval: "300000000000" Mode: "delay" } +/- EphemeralDisk { Migrate: "false" +/- SizeMB: "300" => "301" Sticky: "false" } +/- Task: "redis" (forces create/destroy update) + Meta[key]: "value" +/- Config { image: "redis:3.2" +/- port_map[0][db]: "6379" => "6380" } +/- Resources { CPU: "500" DiskMB: "0" IOPS: "0" +/- MemoryMB: "256" => "257" } +/- Service { Name: "global-redis-check" PortLabel: "db" +/- Check { Command: "" InitialStatus: "" Interval: "10000000000" Name: "alive" Path: "" PortLabel: "" Protocol: "" +/- Timeout: "2000000000" => "3000000000" Type: "tcp" } } ``` New Output: ``` +/- Job: "example" +/- Task Group: "cache" (1 create/destroy update) +/- RestartPolicy { +/- Attempts: "10" => "9" Delay: "25000000000" Interval: "300000000000" Mode: "delay" } +/- EphemeralDisk { Migrate: "false" +/- SizeMB: "300" => "301" Sticky: "false" } +/- Task: "redis" (forces create/destroy update) + Meta[key]: "value" +/- Config { image: "redis:3.2" +/- port_map[0][db]: "6379" => "6380" } +/- Resources { CPU: "500" DiskMB: "0" IOPS: "0" +/- MemoryMB: "256" => "257" } +/- Service { Name: "global-redis-check" PortLabel: "db" +/- Check { Command: "" InitialStatus: "" Interval: "10000000000" Name: "alive" Path: "" PortLabel: "" Protocol: "" +/- Timeout: "2000000000" => "3000000000" Type: "tcp" } } ```
2017-03-21 18:42:10 +00:00
if setDiff := stringSetDiff(j.Datacenters, other.Datacenters, "Datacenters", contextual); setDiff != nil && setDiff.Type != DiffTypeNone {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, setDiff)
}
// Constraints diff
conDiff := primitiveObjectSetDiff(
interfaceSlice(j.Constraints),
interfaceSlice(other.Constraints),
[]string{"str"},
2016-05-11 18:10:09 +00:00
"Constraint",
contextual)
2016-05-11 05:23:34 +00:00
if conDiff != nil {
diff.Objects = append(diff.Objects, conDiff...)
}
// Affinities diff
affinitiesDiff := primitiveObjectSetDiff(
interfaceSlice(j.Affinities),
interfaceSlice(other.Affinities),
[]string{"str"},
"Affinity",
contextual)
if affinitiesDiff != nil {
diff.Objects = append(diff.Objects, affinitiesDiff...)
}
2016-05-11 05:23:34 +00:00
// Task groups diff
2016-05-11 18:10:09 +00:00
tgs, err := taskGroupDiffs(j.TaskGroups, other.TaskGroups, contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diff.TaskGroups = tgs
// Periodic diff
2016-05-11 18:10:09 +00:00
if pDiff := primitiveObjectDiff(j.Periodic, other.Periodic, nil, "Periodic", contextual); pDiff != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, pDiff)
}
// ParameterizedJob diff
if cDiff := parameterizedJobDiff(j.ParameterizedJob, other.ParameterizedJob, contextual); cDiff != nil {
2016-12-15 23:40:18 +00:00
diff.Objects = append(diff.Objects, cDiff)
}
// Multiregion diff
if mrDiff := multiregionDiff(j.Multiregion, other.Multiregion, contextual); mrDiff != nil {
diff.Objects = append(diff.Objects, mrDiff)
}
2017-04-16 23:54:02 +00:00
// Check to see if there is a diff. We don't use reflect because we are
// filtering quite a few fields that will change on each diff.
if diff.Type == DiffTypeNone {
for _, fd := range diff.Fields {
if fd.Type != DiffTypeNone {
diff.Type = DiffTypeEdited
break
}
}
}
if diff.Type == DiffTypeNone {
for _, od := range diff.Objects {
if od.Type != DiffTypeNone {
diff.Type = DiffTypeEdited
break
}
}
}
if diff.Type == DiffTypeNone {
for _, tg := range diff.TaskGroups {
if tg.Type != DiffTypeNone {
diff.Type = DiffTypeEdited
break
}
}
}
2016-05-11 05:23:34 +00:00
return diff, nil
}
func (j *JobDiff) GoString() string {
out := fmt.Sprintf("Job %q (%s):\n", j.ID, j.Type)
for _, f := range j.Fields {
out += fmt.Sprintf("%#v\n", f)
}
for _, o := range j.Objects {
out += fmt.Sprintf("%#v\n", o)
}
for _, tg := range j.TaskGroups {
out += fmt.Sprintf("%#v\n", tg)
}
return out
}
// TaskGroupDiff contains the diff of two task groups.
type TaskGroupDiff struct {
Type DiffType
Name string
Fields []*FieldDiff
Objects []*ObjectDiff
Tasks []*TaskDiff
2016-05-11 22:36:28 +00:00
Updates map[string]uint64
2016-05-11 05:23:34 +00:00
}
2016-05-11 18:10:09 +00:00
// Diff returns a diff of two task groups. If contextual diff is enabled,
2016-05-15 16:41:34 +00:00
// objects' fields will be stored even if no diff occurred as long as one field
2016-05-11 18:10:09 +00:00
// changed.
func (tg *TaskGroup) Diff(other *TaskGroup, contextual bool) (*TaskGroupDiff, error) {
2016-05-11 05:23:34 +00:00
diff := &TaskGroupDiff{Type: DiffTypeNone}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
filter := []string{"Name"}
if tg == nil && other == nil {
return diff, nil
} else if tg == nil {
tg = &TaskGroup{}
diff.Type = DiffTypeAdded
diff.Name = other.Name
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
} else if other == nil {
other = &TaskGroup{}
diff.Type = DiffTypeDeleted
diff.Name = tg.Name
oldPrimitiveFlat = flatmap.Flatten(tg, filter, true)
} else {
if !reflect.DeepEqual(tg, other) {
diff.Type = DiffTypeEdited
}
if tg.Name != other.Name {
return nil, fmt.Errorf("can not diff task groups with different names: %q and %q", tg.Name, other.Name)
}
diff.Name = other.Name
oldPrimitiveFlat = flatmap.Flatten(tg, filter, true)
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
}
// ShutdownDelay diff
2020-04-06 16:25:50 +00:00
if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
if tg.ShutdownDelay == nil {
oldPrimitiveFlat["ShutdownDelay"] = ""
} else {
oldPrimitiveFlat["ShutdownDelay"] = fmt.Sprintf("%d", *tg.ShutdownDelay)
}
if other.ShutdownDelay == nil {
newPrimitiveFlat["ShutdownDelay"] = ""
} else {
newPrimitiveFlat["ShutdownDelay"] = fmt.Sprintf("%d", *other.ShutdownDelay)
}
}
// StopAfterClientDisconnect diff
if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
if tg.StopAfterClientDisconnect == nil {
oldPrimitiveFlat["StopAfterClientDisconnect"] = ""
} else {
oldPrimitiveFlat["StopAfterClientDisconnect"] = fmt.Sprintf("%d", *tg.StopAfterClientDisconnect)
}
if other.StopAfterClientDisconnect == nil {
newPrimitiveFlat["StopAfterClientDisconnect"] = ""
} else {
newPrimitiveFlat["StopAfterClientDisconnect"] = fmt.Sprintf("%d", *other.StopAfterClientDisconnect)
}
}
// MaxClientDisconnect diff
if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
if tg.MaxClientDisconnect == nil {
oldPrimitiveFlat["MaxClientDisconnect"] = ""
} else {
oldPrimitiveFlat["MaxClientDisconnect"] = fmt.Sprintf("%d", *tg.MaxClientDisconnect)
}
if other.MaxClientDisconnect == nil {
newPrimitiveFlat["MaxClientDisconnect"] = ""
} else {
newPrimitiveFlat["MaxClientDisconnect"] = fmt.Sprintf("%d", *other.MaxClientDisconnect)
}
}
2016-05-11 05:23:34 +00:00
// Diff the primitive fields.
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
2016-05-11 05:23:34 +00:00
// Constraints diff
conDiff := primitiveObjectSetDiff(
interfaceSlice(tg.Constraints),
interfaceSlice(other.Constraints),
[]string{"str"},
2016-05-11 18:10:09 +00:00
"Constraint",
contextual)
2016-05-11 05:23:34 +00:00
if conDiff != nil {
diff.Objects = append(diff.Objects, conDiff...)
}
2018-07-16 13:30:58 +00:00
// Affinities diff
affinitiesDiff := primitiveObjectSetDiff(
interfaceSlice(tg.Affinities),
interfaceSlice(other.Affinities),
[]string{"str"},
"Affinity",
contextual)
if affinitiesDiff != nil {
diff.Objects = append(diff.Objects, affinitiesDiff...)
}
2016-05-11 05:23:34 +00:00
// Restart policy diff
2016-05-11 18:10:09 +00:00
rDiff := primitiveObjectDiff(tg.RestartPolicy, other.RestartPolicy, nil, "RestartPolicy", contextual)
if rDiff != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, rDiff)
}
// Reschedule policy diff
reschedDiff := primitiveObjectDiff(tg.ReschedulePolicy, other.ReschedulePolicy, nil, "ReschedulePolicy", contextual)
if reschedDiff != nil {
diff.Objects = append(diff.Objects, reschedDiff)
}
// EphemeralDisk diff
diskDiff := primitiveObjectDiff(tg.EphemeralDisk, other.EphemeralDisk, nil, "EphemeralDisk", contextual)
2016-08-27 03:38:50 +00:00
if diskDiff != nil {
diff.Objects = append(diff.Objects, diskDiff)
}
consulDiff := primitiveObjectDiff(tg.Consul, other.Consul, nil, "Consul", contextual)
if consulDiff != nil {
diff.Objects = append(diff.Objects, consulDiff)
2016-08-27 03:38:50 +00:00
}
// Update diff
// COMPAT: Remove "Stagger" in 0.7.0.
if uDiff := primitiveObjectDiff(tg.Update, other.Update, []string{"Stagger"}, "Update", contextual); uDiff != nil {
diff.Objects = append(diff.Objects, uDiff)
}
// Network Resources diff
if nDiffs := networkResourceDiffs(tg.Networks, other.Networks, contextual); nDiffs != nil {
diff.Objects = append(diff.Objects, nDiffs...)
}
// Services diff
if sDiffs := serviceDiffs(tg.Services, other.Services, contextual); sDiffs != nil {
diff.Objects = append(diff.Objects, sDiffs...)
}
// Volumes diff
if vDiffs := volumeDiffs(tg.Volumes, other.Volumes, contextual); vDiffs != nil {
diff.Objects = append(diff.Objects, vDiffs...)
}
2016-05-11 05:23:34 +00:00
// Tasks diff
2016-05-11 18:10:09 +00:00
tasks, err := taskDiffs(tg.Tasks, other.Tasks, contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diff.Tasks = tasks
return diff, nil
}
func (tg *TaskGroupDiff) GoString() string {
out := fmt.Sprintf("Group %q (%s):\n", tg.Name, tg.Type)
if len(tg.Updates) != 0 {
out += "Updates {\n"
for update, count := range tg.Updates {
out += fmt.Sprintf("%d %s\n", count, update)
}
out += "}\n"
}
for _, f := range tg.Fields {
out += fmt.Sprintf("%#v\n", f)
}
for _, o := range tg.Objects {
out += fmt.Sprintf("%#v\n", o)
}
for _, t := range tg.Tasks {
out += fmt.Sprintf("%#v\n", t)
}
return out
}
2016-05-11 18:10:09 +00:00
// TaskGroupDiffs diffs two sets of task groups. If contextual diff is enabled,
2016-05-15 16:41:34 +00:00
// objects' fields will be stored even if no diff occurred as long as one field
2016-05-11 18:10:09 +00:00
// changed.
func taskGroupDiffs(old, new []*TaskGroup, contextual bool) ([]*TaskGroupDiff, error) {
2016-05-11 05:23:34 +00:00
oldMap := make(map[string]*TaskGroup, len(old))
newMap := make(map[string]*TaskGroup, len(new))
for _, o := range old {
oldMap[o.Name] = o
}
for _, n := range new {
newMap[n.Name] = n
}
var diffs []*TaskGroupDiff
for name, oldGroup := range oldMap {
// Diff the same, deleted and edited
2016-05-11 18:10:09 +00:00
diff, err := oldGroup.Diff(newMap[name], contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diffs = append(diffs, diff)
}
for name, newGroup := range newMap {
// Diff the added
if old, ok := oldMap[name]; !ok {
2016-05-11 18:10:09 +00:00
diff, err := old.Diff(newGroup, contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diffs = append(diffs, diff)
}
}
sort.Sort(TaskGroupDiffs(diffs))
return diffs, nil
}
// For sorting TaskGroupDiffs
type TaskGroupDiffs []*TaskGroupDiff
func (tg TaskGroupDiffs) Len() int { return len(tg) }
func (tg TaskGroupDiffs) Swap(i, j int) { tg[i], tg[j] = tg[j], tg[i] }
func (tg TaskGroupDiffs) Less(i, j int) bool { return tg[i].Name < tg[j].Name }
// TaskDiff contains the diff of two Tasks
type TaskDiff struct {
Type DiffType
Name string
Fields []*FieldDiff
Objects []*ObjectDiff
Annotations []string
}
2016-05-11 18:10:09 +00:00
// Diff returns a diff of two tasks. If contextual diff is enabled, objects
// within the task will contain field information even if unchanged.
func (t *Task) Diff(other *Task, contextual bool) (*TaskDiff, error) {
2016-05-11 05:23:34 +00:00
diff := &TaskDiff{Type: DiffTypeNone}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
filter := []string{"Name", "Config"}
if t == nil && other == nil {
return diff, nil
} else if t == nil {
t = &Task{}
diff.Type = DiffTypeAdded
diff.Name = other.Name
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
} else if other == nil {
other = &Task{}
diff.Type = DiffTypeDeleted
diff.Name = t.Name
oldPrimitiveFlat = flatmap.Flatten(t, filter, true)
} else {
if !reflect.DeepEqual(t, other) {
diff.Type = DiffTypeEdited
}
if t.Name != other.Name {
return nil, fmt.Errorf("can not diff tasks with different names: %q and %q", t.Name, other.Name)
}
diff.Name = other.Name
oldPrimitiveFlat = flatmap.Flatten(t, filter, true)
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
}
// Diff the primitive fields.
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
2016-05-11 05:23:34 +00:00
// Constraints diff
conDiff := primitiveObjectSetDiff(
interfaceSlice(t.Constraints),
interfaceSlice(other.Constraints),
[]string{"str"},
2016-05-11 18:10:09 +00:00
"Constraint",
contextual)
2016-05-11 05:23:34 +00:00
if conDiff != nil {
diff.Objects = append(diff.Objects, conDiff...)
}
// Affinities diff
affinitiesDiff := primitiveObjectSetDiff(
interfaceSlice(t.Affinities),
interfaceSlice(other.Affinities),
[]string{"str"},
"Affinity",
contextual)
if affinitiesDiff != nil {
diff.Objects = append(diff.Objects, affinitiesDiff...)
}
2016-05-11 05:23:34 +00:00
// Config diff
2016-05-11 18:10:09 +00:00
if cDiff := configDiff(t.Config, other.Config, contextual); cDiff != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, cDiff)
}
// Resources diff
2016-05-11 18:10:09 +00:00
if rDiff := t.Resources.Diff(other.Resources, contextual); rDiff != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, rDiff)
}
// LogConfig diff
2016-05-11 18:10:09 +00:00
lDiff := primitiveObjectDiff(t.LogConfig, other.LogConfig, nil, "LogConfig", contextual)
if lDiff != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, lDiff)
}
// Dispatch payload diff
dDiff := primitiveObjectDiff(t.DispatchPayload, other.DispatchPayload, nil, "DispatchPayload", contextual)
2016-12-15 23:40:18 +00:00
if dDiff != nil {
diff.Objects = append(diff.Objects, dDiff)
}
2016-05-11 05:23:34 +00:00
// Artifacts diff
diffs := primitiveObjectSetDiff(
interfaceSlice(t.Artifacts),
interfaceSlice(other.Artifacts),
nil,
2016-05-11 18:10:09 +00:00
"Artifact",
contextual)
2016-05-11 05:23:34 +00:00
if diffs != nil {
diff.Objects = append(diff.Objects, diffs...)
}
2016-05-11 22:25:59 +00:00
// Services diff
2016-06-12 23:36:49 +00:00
if sDiffs := serviceDiffs(t.Services, other.Services, contextual); sDiffs != nil {
2016-05-11 22:25:59 +00:00
diff.Objects = append(diff.Objects, sDiffs...)
}
2016-09-21 20:49:34 +00:00
// Vault diff
vDiff := vaultDiff(t.Vault, other.Vault, contextual)
if vDiff != nil {
diff.Objects = append(diff.Objects, vDiff)
}
2017-02-11 00:57:47 +00:00
// Template diff
tmplDiffs := templateDiffs(t.Templates, other.Templates, contextual)
2016-09-23 23:14:20 +00:00
if tmplDiffs != nil {
diff.Objects = append(diff.Objects, tmplDiffs...)
}
2016-05-11 05:23:34 +00:00
return diff, nil
}
func (t *TaskDiff) GoString() string {
var out string
if len(t.Annotations) == 0 {
out = fmt.Sprintf("Task %q (%s):\n", t.Name, t.Type)
} else {
out = fmt.Sprintf("Task %q (%s) (%s):\n", t.Name, t.Type, strings.Join(t.Annotations, ","))
}
for _, f := range t.Fields {
out += fmt.Sprintf("%#v\n", f)
}
for _, o := range t.Objects {
out += fmt.Sprintf("%#v\n", o)
}
return out
}
2016-05-11 18:10:09 +00:00
// taskDiffs diffs a set of tasks. If contextual diff is enabled, unchanged
// fields within objects nested in the tasks will be returned.
func taskDiffs(old, new []*Task, contextual bool) ([]*TaskDiff, error) {
2016-05-11 05:23:34 +00:00
oldMap := make(map[string]*Task, len(old))
newMap := make(map[string]*Task, len(new))
for _, o := range old {
oldMap[o.Name] = o
}
for _, n := range new {
newMap[n.Name] = n
}
var diffs []*TaskDiff
for name, oldGroup := range oldMap {
// Diff the same, deleted and edited
2016-05-11 18:10:09 +00:00
diff, err := oldGroup.Diff(newMap[name], contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diffs = append(diffs, diff)
}
for name, newGroup := range newMap {
// Diff the added
if old, ok := oldMap[name]; !ok {
2016-05-11 18:10:09 +00:00
diff, err := old.Diff(newGroup, contextual)
2016-05-11 05:23:34 +00:00
if err != nil {
return nil, err
}
diffs = append(diffs, diff)
}
}
sort.Sort(TaskDiffs(diffs))
return diffs, nil
}
// For sorting TaskDiffs
type TaskDiffs []*TaskDiff
func (t TaskDiffs) Len() int { return len(t) }
func (t TaskDiffs) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t TaskDiffs) Less(i, j int) bool { return t[i].Name < t[j].Name }
2016-05-11 22:25:59 +00:00
// serviceDiff returns the diff of two service objects. If contextual diff is
2016-05-15 16:41:34 +00:00
// enabled, all fields will be returned, even if no diff occurred.
2016-06-12 23:36:49 +00:00
func serviceDiff(old, new *Service, contextual bool) *ObjectDiff {
2016-05-11 22:25:59 +00:00
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Service"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
2016-06-12 23:36:49 +00:00
old = &Service{}
2016-05-11 22:25:59 +00:00
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
2016-06-12 23:36:49 +00:00
new = &Service{}
2016-05-11 22:25:59 +00:00
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2018-04-19 22:12:23 +00:00
if setDiff := stringSetDiff(old.CanaryTags, new.CanaryTags, "CanaryTags", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
// Tag diffs
if setDiff := stringSetDiff(old.Tags, new.Tags, "Tags", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
2016-05-11 22:25:59 +00:00
// Checks diffs
if cDiffs := serviceCheckDiffs(old.Checks, new.Checks, contextual); cDiffs != nil {
diff.Objects = append(diff.Objects, cDiffs...)
}
// Consul Connect diffs
if conDiffs := connectDiffs(old.Connect, new.Connect, contextual); conDiffs != nil {
diff.Objects = append(diff.Objects, conDiffs)
}
2016-05-11 22:25:59 +00:00
return diff
}
// serviceDiffs diffs a set of services. If contextual diff is enabled, unchanged
// fields within objects nested in the tasks will be returned.
2016-06-12 23:36:49 +00:00
func serviceDiffs(old, new []*Service, contextual bool) []*ObjectDiff {
// Handle trivial case.
if len(old) == 1 && len(new) == 1 {
if diff := serviceDiff(old[0], new[0], contextual); diff != nil {
return []*ObjectDiff{diff}
}
return nil
2016-05-11 22:25:59 +00:00
}
// For each service we will try to find a corresponding match in the other
// service list.
// The following lists store the index of the matching service for each
// position of the inputs.
oldMatches := make([]int, len(old))
newMatches := make([]int, len(new))
// Initialize all services as unmatched.
for i := range oldMatches {
oldMatches[i] = -1
}
for i := range newMatches {
newMatches[i] = -1
2016-05-11 22:25:59 +00:00
}
// Find a match in the new services list for each old service and compute
// their diffs.
2016-05-11 22:25:59 +00:00
var diffs []*ObjectDiff
for oldIndex, oldService := range old {
newIndex := findServiceMatch(oldService, oldIndex, new, newMatches)
// Old services that don't have a match were deleted.
if newIndex < 0 {
diff := serviceDiff(oldService, nil, contextual)
diffs = append(diffs, diff)
continue
}
// If A matches B then B matches A.
oldMatches[oldIndex] = newIndex
newMatches[newIndex] = oldIndex
newService := new[newIndex]
if diff := serviceDiff(oldService, newService, contextual); diff != nil {
2016-05-11 22:25:59 +00:00
diffs = append(diffs, diff)
}
}
// New services without match were added.
for i, m := range newMatches {
if m == -1 {
diff := serviceDiff(nil, new[i], contextual)
diffs = append(diffs, diff)
2016-05-11 22:25:59 +00:00
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
// findServiceMatch returns the index of the service in the input services list
// that matches the provided input service.
func findServiceMatch(service *Service, serviceIndex int, services []*Service, matches []int) int {
// minScoreThreshold can be adjusted to generate more (lower value) or
// fewer (higher value) matches.
// More matches result in more Edited diffs, while fewer matches generate
// more Add/Delete diff pairs.
minScoreThreshold := 2
highestScore := 0
indexMatch := -1
for i, s := range services {
// Skip service if it's already matched.
if matches[i] >= 0 {
continue
}
// Finding a perfect match by just looking at the before and after
// list of services is impossible since they don't have a stable
// identifier that can be used to uniquely identify them.
//
// Users also have an implicit temporal intuition of which services
// match each other when editing their jobspec file. If they move the
// 3rd service to the top, they don't expect their job to change.
//
// This intuition could be made explicit by requiring a user-defined
// unique identifier, but this would cause additional work and the
// new field would not be intuitive for users to understand how to use
// it.
//
// Using a hash value of the service content will cause any changes to
// create a delete/add diff pair.
//
// There are three main candidates for a service ID:
// - name, but they are not unique and can be modified.
// - label port, but they have the same problems as name.
// - service position within the overall list of services, but if the
// service block is moved, it will impact all services that come
// after it.
//
// None of these values are enough on their own, but they are also too
// strong when considered all together.
//
// So we try to score services by their main candidates with a preference
// towards name + label over service position.
score := 0
if i == serviceIndex {
score += 1
}
if service.PortLabel == s.PortLabel {
score += 2
}
if service.Name == s.Name {
score += 3
}
if score > minScoreThreshold && score > highestScore {
highestScore = score
indexMatch = i
}
}
return indexMatch
}
2016-05-11 22:25:59 +00:00
// serviceCheckDiff returns the diff of two service check objects. If contextual
2016-05-15 16:41:34 +00:00
// diff is enabled, all fields will be returned, even if no diff occurred.
2016-05-11 22:25:59 +00:00
func serviceCheckDiff(old, new *ServiceCheck, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Check"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ServiceCheck{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ServiceCheck{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff Header
2017-08-17 22:46:35 +00:00
if headerDiff := checkHeaderDiff(old.Header, new.Header, contextual); headerDiff != nil {
diff.Objects = append(diff.Objects, headerDiff)
}
2017-09-28 21:06:18 +00:00
// Diff check_restart
if crDiff := checkRestartDiff(old.CheckRestart, new.CheckRestart, contextual); crDiff != nil {
diff.Objects = append(diff.Objects, crDiff)
}
2017-08-17 22:46:35 +00:00
return diff
}
// checkHeaderDiff returns the diff of two service check header objects. If
// contextual diff is enabled, all fields will be returned, even if no diff
// occurred.
func checkHeaderDiff(old, new map[string][]string, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Header"}
2017-09-28 23:15:04 +00:00
var oldFlat, newFlat map[string]string
2017-08-17 22:46:35 +00:00
if reflect.DeepEqual(old, new) {
return nil
} else if len(old) == 0 {
diff.Type = DiffTypeAdded
2017-09-28 23:15:04 +00:00
newFlat = flatmap.Flatten(new, nil, false)
2017-08-17 22:46:35 +00:00
} else if len(new) == 0 {
diff.Type = DiffTypeDeleted
2017-09-28 23:15:04 +00:00
oldFlat = flatmap.Flatten(old, nil, false)
2017-08-17 22:46:35 +00:00
} else {
diff.Type = DiffTypeEdited
2017-09-28 23:15:04 +00:00
oldFlat = flatmap.Flatten(old, nil, false)
newFlat = flatmap.Flatten(new, nil, false)
2017-08-17 22:46:35 +00:00
}
2017-09-28 23:15:04 +00:00
2017-08-17 22:46:35 +00:00
diff.Fields = fieldDiffs(oldFlat, newFlat, contextual)
2016-05-11 22:25:59 +00:00
return diff
}
2017-09-28 21:06:18 +00:00
// checkRestartDiff returns the diff of two service check check_restart
// objects. If contextual diff is enabled, all fields will be returned, even if
// no diff occurred.
func checkRestartDiff(old, new *CheckRestart, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "CheckRestart"}
2017-09-28 23:15:04 +00:00
var oldFlat, newFlat map[string]string
2017-09-28 21:06:18 +00:00
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
diff.Type = DiffTypeAdded
2017-09-28 23:15:04 +00:00
newFlat = flatmap.Flatten(new, nil, true)
diff.Type = DiffTypeAdded
2017-09-28 21:06:18 +00:00
} else if new == nil {
diff.Type = DiffTypeDeleted
2017-09-28 23:15:04 +00:00
oldFlat = flatmap.Flatten(old, nil, true)
2017-09-28 21:06:18 +00:00
} else {
diff.Type = DiffTypeEdited
2017-09-28 23:15:04 +00:00
oldFlat = flatmap.Flatten(old, nil, true)
newFlat = flatmap.Flatten(new, nil, true)
2017-09-28 21:06:18 +00:00
}
2017-09-28 23:15:04 +00:00
2017-09-28 21:06:18 +00:00
diff.Fields = fieldDiffs(oldFlat, newFlat, contextual)
return diff
}
// connectDiffs returns the diff of two Consul connect objects. If contextual
// diff is enabled, all fields will be returned, even if no diff occurred.
func connectDiffs(old, new *ConsulConnect, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulConnect"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ConsulConnect{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ConsulConnect{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the object field SidecarService.
sidecarSvcDiff := connectSidecarServiceDiff(old.SidecarService, new.SidecarService, contextual)
if sidecarSvcDiff != nil {
diff.Objects = append(diff.Objects, sidecarSvcDiff)
}
// Diff the object field SidecarTask.
sidecarTaskDiff := sidecarTaskDiff(old.SidecarTask, new.SidecarTask, contextual)
if sidecarTaskDiff != nil {
diff.Objects = append(diff.Objects, sidecarTaskDiff)
}
// Diff the object field ConsulGateway.
gatewayDiff := connectGatewayDiff(old.Gateway, new.Gateway, contextual)
if gatewayDiff != nil {
diff.Objects = append(diff.Objects, gatewayDiff)
}
return diff
}
func connectGatewayDiff(prev, next *ConsulGateway, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Gateway"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulGateway)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulGateway)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the ConsulGatewayProxy fields.
gatewayProxyDiff := connectGatewayProxyDiff(prev.Proxy, next.Proxy, contextual)
if gatewayProxyDiff != nil {
diff.Objects = append(diff.Objects, gatewayProxyDiff)
}
// Diff the ingress gateway fields.
gatewayIngressDiff := connectGatewayIngressDiff(prev.Ingress, next.Ingress, contextual)
if gatewayIngressDiff != nil {
diff.Objects = append(diff.Objects, gatewayIngressDiff)
}
// Diff the terminating gateway fields.
gatewayTerminatingDiff := connectGatewayTerminatingDiff(prev.Terminating, next.Terminating, contextual)
if gatewayTerminatingDiff != nil {
diff.Objects = append(diff.Objects, gatewayTerminatingDiff)
}
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
// Diff the mesh gateway fields.
gatewayMeshDiff := connectGatewayMeshDiff(prev.Mesh, next.Mesh, contextual)
if gatewayMeshDiff != nil {
diff.Objects = append(diff.Objects, gatewayMeshDiff)
}
return diff
}
func connectGatewayMeshDiff(prev, next *ConsulMeshConfigEntry, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Mesh"}
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
// no fields to further diff
diff.Type = DiffTypeAdded
} else if next == nil {
// no fields to further diff
diff.Type = DiffTypeDeleted
} else {
diff.Type = DiffTypeEdited
}
// Currently no fields in mesh gateways.
return diff
}
func connectGatewayIngressDiff(prev, next *ConsulIngressConfigEntry, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Ingress"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulIngressConfigEntry)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulIngressConfigEntry)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the ConsulGatewayTLSConfig objects.
tlsConfigDiff := connectGatewayTLSConfigDiff(prev.TLS, next.TLS, contextual)
if tlsConfigDiff != nil {
diff.Objects = append(diff.Objects, tlsConfigDiff)
}
// Diff the Listeners lists.
gatewayIngressListenersDiff := connectGatewayIngressListenersDiff(prev.Listeners, next.Listeners, contextual)
if gatewayIngressListenersDiff != nil {
diff.Objects = append(diff.Objects, gatewayIngressListenersDiff...)
}
return diff
}
func connectGatewayTerminatingDiff(prev, next *ConsulTerminatingConfigEntry, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Terminating"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulTerminatingConfigEntry)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulTerminatingConfigEntry)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the Services lists.
gatewayLinkedServicesDiff := connectGatewayTerminatingLinkedServicesDiff(prev.Services, next.Services, contextual)
if gatewayLinkedServicesDiff != nil {
diff.Objects = append(diff.Objects, gatewayLinkedServicesDiff...)
}
return diff
}
// connectGatewayTerminatingLinkedServicesDiff diffs are a set of services keyed
// by service name. These objects contain only fields.
func connectGatewayTerminatingLinkedServicesDiff(prev, next []*ConsulLinkedService, contextual bool) []*ObjectDiff {
// create maps, diff the maps, key by linked service name
prevMap := make(map[string]*ConsulLinkedService, len(prev))
nextMap := make(map[string]*ConsulLinkedService, len(next))
for _, s := range prev {
prevMap[s.Name] = s
}
for _, s := range next {
nextMap[s.Name] = s
}
var diffs []*ObjectDiff
for k, prevS := range prevMap {
// Diff the same, deleted, and edited
if diff := connectGatewayTerminatingLinkedServiceDiff(prevS, nextMap[k], contextual); diff != nil {
diffs = append(diffs, diff)
}
}
for k, nextS := range nextMap {
// Diff the added
if old, ok := prevMap[k]; !ok {
if diff := connectGatewayTerminatingLinkedServiceDiff(old, nextS, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
func connectGatewayTerminatingLinkedServiceDiff(prev, next *ConsulLinkedService, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Service"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// No objects today.
return diff
}
func connectGatewayTLSConfigDiff(prev, next *ConsulGatewayTLSConfig, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "TLS"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = &ConsulGatewayTLSConfig{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = &ConsulGatewayTLSConfig{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// CipherSuites diffs
if setDiff := stringSetDiff(prev.CipherSuites, next.CipherSuites, "CipherSuites", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
// Diff the primitive field.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
return diff
}
// connectGatewayIngressListenersDiff diffs are a set of listeners keyed by "protocol/port", which is
// a nifty workaround having slices instead of maps. Presumably such a key will be unique, because if
// if is not the config entry is not going to work anyway.
func connectGatewayIngressListenersDiff(prev, next []*ConsulIngressListener, contextual bool) []*ObjectDiff {
// create maps, diff the maps, keys are fields, keys are (port+protocol)
key := func(l *ConsulIngressListener) string {
return fmt.Sprintf("%s/%d", l.Protocol, l.Port)
}
prevMap := make(map[string]*ConsulIngressListener, len(prev))
nextMap := make(map[string]*ConsulIngressListener, len(next))
for _, l := range prev {
prevMap[key(l)] = l
}
for _, l := range next {
nextMap[key(l)] = l
}
var diffs []*ObjectDiff
for k, prevL := range prevMap {
// Diff the same, deleted, and edited
if diff := connectGatewayIngressListenerDiff(prevL, nextMap[k], contextual); diff != nil {
diffs = append(diffs, diff)
}
}
for k, nextL := range nextMap {
// Diff the added
if old, ok := prevMap[k]; !ok {
if diff := connectGatewayIngressListenerDiff(old, nextL, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
func connectGatewayIngressListenerDiff(prev, next *ConsulIngressListener, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Listener"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulIngressListener)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulIngressListener)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the Ingress Service objects.
if diffs := connectGatewayIngressServicesDiff(prev.Services, next.Services, contextual); diffs != nil {
diff.Objects = append(diff.Objects, diffs...)
}
return diff
}
// connectGatewayIngressServicesDiff diffs are a set of ingress services keyed by their service name, which
// is a workaround for having slices instead of maps. Presumably the service name is a unique key, because if
// no the config entry is not going to make sense anyway.
func connectGatewayIngressServicesDiff(prev, next []*ConsulIngressService, contextual bool) []*ObjectDiff {
prevMap := make(map[string]*ConsulIngressService, len(prev))
nextMap := make(map[string]*ConsulIngressService, len(next))
for _, s := range prev {
prevMap[s.Name] = s
}
for _, s := range next {
nextMap[s.Name] = s
}
var diffs []*ObjectDiff
for name, oldIS := range prevMap {
// Diff the same, deleted, and edited
if diff := connectGatewayIngressServiceDiff(oldIS, nextMap[name], contextual); diff != nil {
diffs = append(diffs, diff)
}
}
for name, newIS := range nextMap {
// Diff the added
if old, ok := prevMap[name]; !ok {
if diff := connectGatewayIngressServiceDiff(old, newIS, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
func connectGatewayIngressServiceDiff(prev, next *ConsulIngressService, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulIngressService"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulIngressService)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulIngressService)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the hosts.
if hDiffs := stringSetDiff(prev.Hosts, next.Hosts, "Hosts", contextual); hDiffs != nil {
diff.Objects = append(diff.Objects, hDiffs)
}
return diff
}
func connectGatewayProxyDiff(prev, next *ConsulGatewayProxy, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Proxy"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev == nil {
prev = new(ConsulGatewayProxy)
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
} else if next == nil {
next = new(ConsulGatewayProxy)
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(prev, nil, true)
newPrimitiveFlat = flatmap.Flatten(next, nil, true)
}
// Diff the ConnectTimeout field (dur ptr). (i.e. convert to string for comparison)
if oldPrimitiveFlat != nil && newPrimitiveFlat != nil {
if prev.ConnectTimeout == nil {
oldPrimitiveFlat["ConnectTimeout"] = ""
} else {
2020-12-09 19:05:18 +00:00
oldPrimitiveFlat["ConnectTimeout"] = prev.ConnectTimeout.String()
}
if next.ConnectTimeout == nil {
newPrimitiveFlat["ConnectTimeout"] = ""
} else {
2020-12-09 19:05:18 +00:00
newPrimitiveFlat["ConnectTimeout"] = next.ConnectTimeout.String()
}
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Diff the EnvoyGatewayBindAddresses map.
bindAddrsDiff := connectGatewayProxyEnvoyBindAddrsDiff(prev.EnvoyGatewayBindAddresses, next.EnvoyGatewayBindAddresses, contextual)
if bindAddrsDiff != nil {
diff.Objects = append(diff.Objects, bindAddrsDiff)
}
// Diff the opaque Config map.
if cDiff := configDiff(prev.Config, next.Config, contextual); cDiff != nil {
diff.Objects = append(diff.Objects, cDiff)
}
return diff
}
// connectGatewayProxyEnvoyBindAddrsDiff returns the diff of two maps. If contextual
// diff is enabled, all fields will be returned, even if no diff occurred.
func connectGatewayProxyEnvoyBindAddrsDiff(prev, next map[string]*ConsulGatewayBindAddress, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "EnvoyGatewayBindAddresses"}
if reflect.DeepEqual(prev, next) {
return nil
} else if len(prev) == 0 {
diff.Type = DiffTypeAdded
} else if len(next) == 0 {
diff.Type = DiffTypeDeleted
} else {
diff.Type = DiffTypeEdited
}
// convert to string representation
prevMap := make(map[string]string, len(prev))
nextMap := make(map[string]string, len(next))
for k, v := range prev {
prevMap[k] = fmt.Sprintf("%s:%d", v.Address, v.Port)
}
for k, v := range next {
nextMap[k] = fmt.Sprintf("%s:%d", v.Address, v.Port)
}
oldPrimitiveFlat := flatmap.Flatten(prevMap, nil, false)
newPrimitiveFlat := flatmap.Flatten(nextMap, nil, false)
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
return diff
}
// connectSidecarServiceDiff returns the diff of two ConsulSidecarService objects.
// If contextual diff is enabled, all fields will be returned, even if no diff occurred.
func connectSidecarServiceDiff(old, new *ConsulSidecarService, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "SidecarService"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ConsulSidecarService{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ConsulSidecarService{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
consulProxyDiff := consulProxyDiff(old.Proxy, new.Proxy, contextual)
if consulProxyDiff != nil {
diff.Objects = append(diff.Objects, consulProxyDiff)
}
return diff
}
// sidecarTaskDiff returns the diff of two Task objects.
// If contextual diff is enabled, all fields will be returned, even if no diff occurred.
func sidecarTaskDiff(old, new *SidecarTask, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "SidecarTask"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &SidecarTask{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &SidecarTask{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false)
// Config diff
if cDiff := configDiff(old.Config, new.Config, contextual); cDiff != nil {
diff.Objects = append(diff.Objects, cDiff)
}
// Resources diff
if rDiff := old.Resources.Diff(new.Resources, contextual); rDiff != nil {
diff.Objects = append(diff.Objects, rDiff)
}
// LogConfig diff
lDiff := primitiveObjectDiff(old.LogConfig, new.LogConfig, nil, "LogConfig", contextual)
if lDiff != nil {
diff.Objects = append(diff.Objects, lDiff)
}
return diff
}
// consulProxyDiff returns the diff of two ConsulProxy objects.
// If contextual diff is enabled, all fields will be returned, even if no diff occurred.
func consulProxyDiff(old, new *ConsulProxy, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulProxy"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ConsulProxy{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ConsulProxy{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
// diff the primitive fields
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
// diff the consul upstream slices
if upDiffs := consulProxyUpstreamsDiff(old.Upstreams, new.Upstreams, contextual); upDiffs != nil {
diff.Objects = append(diff.Objects, upDiffs...)
}
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
// diff the config blob
if cDiff := configDiff(old.Config, new.Config, contextual); cDiff != nil {
diff.Objects = append(diff.Objects, cDiff)
}
return diff
}
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
// consulProxyUpstreamsDiff diffs a set of connect upstreams. If contextual diff is
// enabled, unchanged fields within objects nested in the tasks will be returned.
func consulProxyUpstreamsDiff(old, new []ConsulUpstream, contextual bool) []*ObjectDiff {
oldMap := make(map[string]ConsulUpstream, len(old))
newMap := make(map[string]ConsulUpstream, len(new))
idx := func(up ConsulUpstream) string {
return fmt.Sprintf("%s/%s", up.Datacenter, up.DestinationName)
}
for _, o := range old {
oldMap[idx(o)] = o
}
for _, n := range new {
newMap[idx(n)] = n
}
var diffs []*ObjectDiff
for index, oldUpstream := range oldMap {
// Diff the same, deleted, and edited
if diff := consulProxyUpstreamDiff(oldUpstream, newMap[index], contextual); diff != nil {
diffs = append(diffs, diff)
}
}
for index, newUpstream := range newMap {
// diff the added
if oldUpstream, exists := oldMap[index]; !exists {
if diff := consulProxyUpstreamDiff(oldUpstream, newUpstream, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
func consulProxyUpstreamDiff(prev, next ConsulUpstream, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ConsulUpstreams"}
var oldPrimFlat, newPrimFlat map[string]string
if reflect.DeepEqual(prev, next) {
return nil
} else if prev.Equal(new(ConsulUpstream)) {
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
prev = ConsulUpstream{}
diff.Type = DiffTypeAdded
newPrimFlat = flatmap.Flatten(next, nil, true)
} else if next.Equal(new(ConsulUpstream)) {
consul/connect: add support for connect mesh gateways This PR implements first-class support for Nomad running Consul Connect Mesh Gateways. Mesh gateways enable services in the Connect mesh to make cross-DC connections via gateways, where each datacenter may not have full node interconnectivity. Consul docs with more information: https://www.consul.io/docs/connect/gateways/mesh-gateway The following group level service block can be used to establish a Connect mesh gateway. service { connect { gateway { mesh { // no configuration } } } } Services can make use of a mesh gateway by configuring so in their upstream blocks, e.g. service { connect { sidecar_service { proxy { upstreams { destination_name = "<service>" local_bind_port = <port> datacenter = "<datacenter>" mesh_gateway { mode = "<mode>" } } } } } } Typical use of a mesh gateway is to create a bridge between datacenters. A mesh gateway should then be configured with a service port that is mapped from a host_network configured on a WAN interface in Nomad agent config, e.g. client { host_network "public" { interface = "eth1" } } Create a port mapping in the group.network block for use by the mesh gateway service from the public host_network, e.g. network { mode = "bridge" port "mesh_wan" { host_network = "public" } } Use this port label for the service.port of the mesh gateway, e.g. service { name = "mesh-gateway" port = "mesh_wan" connect { gateway { mesh {} } } } Currently Envoy is the only supported gateway implementation in Consul. By default Nomad client will run the latest official Envoy docker image supported by the local Consul agent. The Envoy task can be customized by setting `meta.connect.gateway_image` in agent config or by setting the `connect.sidecar_task` block. Gateways require Consul 1.8.0+, enforced by the Nomad scheduler. Closes #9446
2021-04-12 19:10:10 +00:00
next = ConsulUpstream{}
diff.Type = DiffTypeDeleted
oldPrimFlat = flatmap.Flatten(prev, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimFlat = flatmap.Flatten(prev, nil, true)
newPrimFlat = flatmap.Flatten(next, nil, true)
}
// diff the primitive fields
diff.Fields = fieldDiffs(oldPrimFlat, newPrimFlat, contextual)
// diff the mesh gateway primitive object
if mDiff := primitiveObjectDiff(prev.MeshGateway, next.MeshGateway, nil, "MeshGateway", contextual); mDiff != nil {
diff.Objects = append(diff.Objects, mDiff)
}
return diff
}
2016-05-11 22:25:59 +00:00
// serviceCheckDiffs diffs a set of service checks. If contextual diff is
// enabled, unchanged fields within objects nested in the tasks will be
// returned.
func serviceCheckDiffs(old, new []*ServiceCheck, contextual bool) []*ObjectDiff {
oldMap := make(map[string]*ServiceCheck, len(old))
newMap := make(map[string]*ServiceCheck, len(new))
for _, o := range old {
oldMap[o.Name] = o
}
for _, n := range new {
newMap[n.Name] = n
}
var diffs []*ObjectDiff
for name, oldCheck := range oldMap {
2016-05-11 22:25:59 +00:00
// Diff the same, deleted and edited
if diff := serviceCheckDiff(oldCheck, newMap[name], contextual); diff != nil {
2016-05-11 22:25:59 +00:00
diffs = append(diffs, diff)
}
}
for name, newCheck := range newMap {
2016-05-11 22:25:59 +00:00
// Diff the added
if old, ok := oldMap[name]; !ok {
if diff := serviceCheckDiff(old, newCheck, contextual); diff != nil {
2016-05-11 22:25:59 +00:00
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
2016-09-21 20:49:34 +00:00
// vaultDiff returns the diff of two vault objects. If contextual diff is
// enabled, all fields will be returned, even if no diff occurred.
func vaultDiff(old, new *Vault, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Vault"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &Vault{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &Vault{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Policies diffs
if setDiff := stringSetDiff(old.Policies, new.Policies, "Policies", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
return diff
}
// waitConfigDiff returns the diff of two WaitConfig objects. If contextual diff is
// enabled, all fields will be returned, even if no diff occurred.
func waitConfigDiff(old, new *WaitConfig, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Template"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, false)
} else if new == nil {
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, false)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, false)
newPrimitiveFlat = flatmap.Flatten(new, nil, false)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
return diff
}
// changeScriptDiff returns the diff of two ChangeScript objects. If contextual
// diff is enabled, all fields will be returned, even if no diff occurred.
func changeScriptDiff(old, new *ChangeScript, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ChangeScript"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ChangeScript{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ChangeScript{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Args diffs
if setDiff := stringSetDiff(old.Args, new.Args, "Args", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
return diff
}
// templateDiff returns the diff of two Consul Template objects. If contextual diff is
// enabled, all fields will be returned, even if no diff occurred.
func templateDiff(old, new *Template, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Template"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &Template{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &Template{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Add the pointer primitive fields.
if old != nil {
if old.Uid != nil {
oldPrimitiveFlat["Uid"] = fmt.Sprintf("%v", *old.Uid)
}
if old.Gid != nil {
oldPrimitiveFlat["Gid"] = fmt.Sprintf("%v", *old.Gid)
}
}
if new != nil {
if new.Uid != nil {
newPrimitiveFlat["Uid"] = fmt.Sprintf("%v", *new.Uid)
}
if new.Gid != nil {
newPrimitiveFlat["Gid"] = fmt.Sprintf("%v", *new.Gid)
}
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// WaitConfig diffs
if waitDiffs := waitConfigDiff(old.Wait, new.Wait, contextual); waitDiffs != nil {
diff.Objects = append(diff.Objects, waitDiffs)
}
// ChangeScript diffs
if changeScriptDiffs := changeScriptDiff(
old.ChangeScript, new.ChangeScript, contextual,
); changeScriptDiffs != nil {
diff.Objects = append(diff.Objects, changeScriptDiffs)
}
return diff
}
// templateDiffs returns the diff of two Consul Template slices. If contextual diff is
// enabled, all fields will be returned, even if no diff occurred.
// serviceDiffs diffs a set of services. If contextual diff is enabled, unchanged
// fields within objects nested in the tasks will be returned.
func templateDiffs(old, new []*Template, contextual bool) []*ObjectDiff {
// Handle trivial case.
if len(old) == 1 && len(new) == 1 {
if diff := templateDiff(old[0], new[0], contextual); diff != nil {
return []*ObjectDiff{diff}
}
return nil
}
// For each template we will try to find a corresponding match in the other list.
// The following lists store the index of the matching template for each
// position of the inputs.
oldMatches := make([]int, len(old))
newMatches := make([]int, len(new))
// Initialize all templates as unmatched.
for i := range oldMatches {
oldMatches[i] = -1
}
for i := range newMatches {
newMatches[i] = -1
}
// Find a match in the new templates list for each old template and compute
// their diffs.
var diffs []*ObjectDiff
for oldIndex, oldTemplate := range old {
newIndex := findTemplateMatch(oldTemplate, new, newMatches)
// Old templates that don't have a match were deleted.
if newIndex < 0 {
diff := templateDiff(oldTemplate, nil, contextual)
diffs = append(diffs, diff)
continue
}
// If A matches B then B matches A.
oldMatches[oldIndex] = newIndex
newMatches[newIndex] = oldIndex
newTemplate := new[newIndex]
if diff := templateDiff(oldTemplate, newTemplate, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
// New templates without match were added.
for i, m := range newMatches {
if m == -1 {
diff := templateDiff(nil, new[i], contextual)
diffs = append(diffs, diff)
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
func findTemplateMatch(template *Template, newTemplates []*Template, newTemplateMatches []int) int {
indexMatch := -1
for i, newTemplate := range newTemplates {
// Skip template if it's already matched.
if newTemplateMatches[i] >= 0 {
continue
}
if template.DiffID() == newTemplate.DiffID() {
indexMatch = i
break
}
}
return indexMatch
}
// parameterizedJobDiff returns the diff of two parameterized job objects. If
// contextual diff is enabled, all fields will be returned, even if no diff
// occurred.
func parameterizedJobDiff(old, new *ParameterizedJobConfig, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "ParameterizedJob"}
2016-12-15 23:40:18 +00:00
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &ParameterizedJobConfig{}
2016-12-15 23:40:18 +00:00
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &ParameterizedJobConfig{}
2016-12-15 23:40:18 +00:00
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Meta diffs
2017-01-26 05:20:50 +00:00
if optionalDiff := stringSetDiff(old.MetaOptional, new.MetaOptional, "MetaOptional", contextual); optionalDiff != nil {
2016-12-15 23:40:18 +00:00
diff.Objects = append(diff.Objects, optionalDiff)
}
2017-01-26 05:20:50 +00:00
if requiredDiff := stringSetDiff(old.MetaRequired, new.MetaRequired, "MetaRequired", contextual); requiredDiff != nil {
2016-12-15 23:40:18 +00:00
diff.Objects = append(diff.Objects, requiredDiff)
}
return diff
}
func multiregionDiff(old, new *Multiregion, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Multiregion"}
if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &Multiregion{}
old.Canonicalize()
diff.Type = DiffTypeAdded
} else if new == nil {
new = &Multiregion{}
diff.Type = DiffTypeDeleted
} else {
diff.Type = DiffTypeEdited
}
// strategy diff
stratDiff := primitiveObjectDiff(
old.Strategy,
new.Strategy,
[]string{},
"Strategy",
contextual)
if stratDiff != nil {
diff.Objects = append(diff.Objects, stratDiff)
}
oldMap := make(map[string]*MultiregionRegion, len(old.Regions))
newMap := make(map[string]*MultiregionRegion, len(new.Regions))
for _, o := range old.Regions {
oldMap[o.Name] = o
}
for _, n := range new.Regions {
newMap[n.Name] = n
}
for name, oldRegion := range oldMap {
// Diff the same, deleted and edited
newRegion := newMap[name]
rdiff := multiregionRegionDiff(oldRegion, newRegion, contextual)
if rdiff != nil {
diff.Objects = append(diff.Objects, rdiff)
}
}
for name, newRegion := range newMap {
// Diff the added
if oldRegion, ok := oldMap[name]; !ok {
rdiff := multiregionRegionDiff(oldRegion, newRegion, contextual)
if rdiff != nil {
diff.Objects = append(diff.Objects, rdiff)
}
}
}
sort.Sort(FieldDiffs(diff.Fields))
sort.Sort(ObjectDiffs(diff.Objects))
return diff
}
func multiregionRegionDiff(r, other *MultiregionRegion, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Region"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(r, other) {
return nil
} else if r == nil {
r = &MultiregionRegion{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
} else if other == nil {
other = &MultiregionRegion{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
// Datacenters diff
setDiff := stringSetDiff(r.Datacenters, other.Datacenters, "Datacenters", contextual)
if setDiff != nil && setDiff.Type != DiffTypeNone {
diff.Objects = append(diff.Objects, setDiff)
}
sort.Sort(ObjectDiffs(diff.Objects))
sort.Sort(FieldDiffs(diff.Fields))
var added, deleted, edited bool
2020-11-13 13:28:11 +00:00
Loop:
for _, f := range diff.Fields {
switch f.Type {
case DiffTypeEdited:
edited = true
2020-11-13 13:28:11 +00:00
break Loop
case DiffTypeDeleted:
deleted = true
case DiffTypeAdded:
added = true
}
}
if edited || added && deleted {
diff.Type = DiffTypeEdited
} else if added {
diff.Type = DiffTypeAdded
} else if deleted {
diff.Type = DiffTypeDeleted
} else {
return nil
}
return diff
}
// volumeDiffs returns the diff of a group's volume requests. If contextual
// diff is enabled, all fields will be returned, even if no diff occurred.
func volumeDiffs(oldVR, newVR map[string]*VolumeRequest, contextual bool) []*ObjectDiff {
if reflect.DeepEqual(oldVR, newVR) {
return nil
}
diffs := []*ObjectDiff{} //Type: DiffTypeNone, Name: "Volumes"}
seen := map[string]bool{}
for name, oReq := range oldVR {
nReq := newVR[name] // might be nil, that's ok
seen[name] = true
diff := volumeDiff(oReq, nReq, contextual)
if diff != nil {
diffs = append(diffs, diff)
}
}
for name, nReq := range newVR {
if !seen[name] {
// we know old is nil at this point, or we'd have hit it before
diff := volumeDiff(nil, nReq, contextual)
if diff != nil {
diffs = append(diffs, diff)
}
}
}
return diffs
}
// volumeDiff returns the diff between two volume requests. If contextual diff
// is enabled, all fields will be returned, even if no diff occurred.
func volumeDiff(oldVR, newVR *VolumeRequest, contextual bool) *ObjectDiff {
if reflect.DeepEqual(oldVR, newVR) {
return nil
}
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Volume"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if oldVR == nil {
oldVR = &VolumeRequest{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(newVR, nil, true)
} else if newVR == nil {
newVR = &VolumeRequest{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(oldVR, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(oldVR, nil, true)
newPrimitiveFlat = flatmap.Flatten(newVR, nil, true)
}
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
mOptsDiff := volumeCSIMountOptionsDiff(oldVR.MountOptions, newVR.MountOptions, contextual)
if mOptsDiff != nil {
diff.Objects = append(diff.Objects, mOptsDiff)
}
return diff
}
// volumeCSIMountOptionsDiff returns the diff between volume mount options. If
// contextual diff is enabled, all fields will be returned, even if no diff
// occurred.
func volumeCSIMountOptionsDiff(oldMO, newMO *CSIMountOptions, contextual bool) *ObjectDiff {
if reflect.DeepEqual(oldMO, newMO) {
return nil
}
diff := &ObjectDiff{Type: DiffTypeNone, Name: "MountOptions"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if oldMO == nil && newMO != nil {
oldMO = &CSIMountOptions{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(newMO, nil, true)
2021-04-09 02:06:41 +00:00
} else if oldMO != nil && newMO == nil {
newMO = &CSIMountOptions{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(oldMO, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(oldMO, nil, true)
newPrimitiveFlat = flatmap.Flatten(newMO, nil, true)
}
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
setDiff := stringSetDiff(oldMO.MountFlags, newMO.MountFlags, "MountFlags", contextual)
if setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}
return diff
}
2016-05-11 18:10:09 +00:00
// Diff returns a diff of two resource objects. If contextual diff is enabled,
// non-changed fields will still be returned.
func (r *Resources) Diff(other *Resources, contextual bool) *ObjectDiff {
2016-05-11 05:23:34 +00:00
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Resources"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(r, other) {
return nil
} else if r == nil {
r = &Resources{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
} else if other == nil {
other = &Resources{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
}
// Diff the primitive fields.
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2016-05-11 05:23:34 +00:00
// Network Resources diff
2016-05-11 18:10:09 +00:00
if nDiffs := networkResourceDiffs(r.Networks, other.Networks, contextual); nDiffs != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, nDiffs...)
}
2018-10-08 23:58:19 +00:00
// Requested Devices diff
if nDiffs := requestedDevicesDiffs(r.Devices, other.Devices, contextual); nDiffs != nil {
diff.Objects = append(diff.Objects, nDiffs...)
}
2016-05-11 05:23:34 +00:00
return diff
}
2016-05-11 18:10:09 +00:00
// Diff returns a diff of two network resources. If contextual diff is enabled,
// non-changed fields will still be returned.
func (n *NetworkResource) Diff(other *NetworkResource, contextual bool) *ObjectDiff {
2016-05-11 05:23:34 +00:00
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Network"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
filter := []string{"Device", "CIDR", "IP"}
if reflect.DeepEqual(n, other) {
2016-05-11 05:23:34 +00:00
return nil
} else if n == nil {
n = &NetworkResource{}
2016-05-11 05:23:34 +00:00
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
} else if other == nil {
other = &NetworkResource{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(n, filter, true)
2016-05-11 05:23:34 +00:00
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(n, filter, true)
2016-05-11 05:23:34 +00:00
newPrimitiveFlat = flatmap.Flatten(other, filter, true)
}
// Diff the primitive fields.
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2016-05-11 05:23:34 +00:00
// Port diffs
resPorts := portDiffs(n.ReservedPorts, other.ReservedPorts, false, contextual)
dynPorts := portDiffs(n.DynamicPorts, other.DynamicPorts, true, contextual)
2016-05-11 18:10:09 +00:00
if resPorts != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, resPorts...)
}
2016-05-11 18:10:09 +00:00
if dynPorts != nil {
2016-05-11 05:23:34 +00:00
diff.Objects = append(diff.Objects, dynPorts...)
}
if dnsDiff := n.DNS.Diff(other.DNS, contextual); dnsDiff != nil {
diff.Objects = append(diff.Objects, dnsDiff)
}
return diff
}
// Diff returns a diff of two DNSConfig structs
func (d *DNSConfig) Diff(other *DNSConfig, contextual bool) *ObjectDiff {
if reflect.DeepEqual(d, other) {
return nil
}
flatten := func(conf *DNSConfig) map[string]string {
m := map[string]string{}
if len(conf.Servers) > 0 {
m["Servers"] = strings.Join(conf.Servers, ",")
}
if len(conf.Searches) > 0 {
m["Searches"] = strings.Join(conf.Searches, ",")
}
if len(conf.Options) > 0 {
m["Options"] = strings.Join(conf.Options, ",")
}
return m
}
diff := &ObjectDiff{Type: DiffTypeNone, Name: "DNS"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if d == nil {
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatten(other)
} else if other == nil {
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatten(d)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatten(d)
newPrimitiveFlat = flatten(other)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2016-05-11 05:23:34 +00:00
return diff
}
2016-05-11 18:10:09 +00:00
// networkResourceDiffs diffs a set of NetworkResources. If contextual diff is enabled,
// non-changed fields will still be returned.
func networkResourceDiffs(old, new []*NetworkResource, contextual bool) []*ObjectDiff {
2016-05-11 05:23:34 +00:00
makeSet := func(objects []*NetworkResource) map[string]*NetworkResource {
objMap := make(map[string]*NetworkResource, len(objects))
for _, obj := range objects {
hash, err := hashstructure.Hash(obj, nil)
if err != nil {
panic(err)
}
objMap[fmt.Sprintf("%d", hash)] = obj
}
return objMap
}
oldSet := makeSet(old)
newSet := makeSet(new)
var diffs []*ObjectDiff
for k, oldV := range oldSet {
if newV, ok := newSet[k]; !ok {
2016-05-11 18:10:09 +00:00
if diff := oldV.Diff(newV, contextual); diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
}
for k, newV := range newSet {
if oldV, ok := oldSet[k]; !ok {
2016-05-11 18:10:09 +00:00
if diff := oldV.Diff(newV, contextual); diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
// portDiffs returns the diff of two sets of ports. The dynamic flag marks the
2016-05-11 18:10:09 +00:00
// set of ports as being Dynamic ports versus Static ports. If contextual diff is enabled,
// non-changed fields will still be returned.
func portDiffs(old, new []Port, dynamic bool, contextual bool) []*ObjectDiff {
2016-05-11 05:23:34 +00:00
makeSet := func(ports []Port) map[string]Port {
portMap := make(map[string]Port, len(ports))
for _, port := range ports {
portMap[port.Label] = port
}
return portMap
}
oldPorts := makeSet(old)
newPorts := makeSet(new)
var filter []string
name := "Static Port"
if dynamic {
filter = []string{"Value"}
name = "Dynamic Port"
}
var diffs []*ObjectDiff
for portLabel, oldPort := range oldPorts {
// Diff the same, deleted and edited
if newPort, ok := newPorts[portLabel]; ok {
2016-05-11 18:10:09 +00:00
diff := primitiveObjectDiff(oldPort, newPort, filter, name, contextual)
if diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
} else {
2016-05-11 18:10:09 +00:00
diff := primitiveObjectDiff(oldPort, nil, filter, name, contextual)
if diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
}
for label, newPort := range newPorts {
// Diff the added
if _, ok := oldPorts[label]; !ok {
2016-05-11 18:10:09 +00:00
diff := primitiveObjectDiff(nil, newPort, filter, name, contextual)
if diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
2018-10-08 23:58:19 +00:00
// Diff returns a diff of two requested devices. If contextual diff is enabled,
// non-changed fields will still be returned.
func (r *RequestedDevice) Diff(other *RequestedDevice, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Device"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string
if reflect.DeepEqual(r, other) {
return nil
} else if r == nil {
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
} else if other == nil {
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(r, nil, true)
newPrimitiveFlat = flatmap.Flatten(other, nil, true)
}
// Diff the primitive fields.
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
return diff
}
// requestedDevicesDiffs diffs a set of RequestedDevices. If contextual diff is enabled,
// non-changed fields will still be returned.
func requestedDevicesDiffs(old, new []*RequestedDevice, contextual bool) []*ObjectDiff {
makeSet := func(devices []*RequestedDevice) map[string]*RequestedDevice {
deviceMap := make(map[string]*RequestedDevice, len(devices))
for _, d := range devices {
deviceMap[d.Name] = d
}
return deviceMap
}
oldSet := makeSet(old)
newSet := makeSet(new)
var diffs []*ObjectDiff
for k, oldV := range oldSet {
newV := newSet[k]
if diff := oldV.Diff(newV, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
for k, newV := range newSet {
if oldV, ok := oldSet[k]; !ok {
if diff := oldV.Diff(newV, contextual); diff != nil {
diffs = append(diffs, diff)
}
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
2016-05-11 18:10:09 +00:00
// configDiff returns the diff of two Task Config objects. If contextual diff is
2016-05-15 16:41:34 +00:00
// enabled, all fields will be returned, even if no diff occurred.
2016-05-11 18:10:09 +00:00
func configDiff(old, new map[string]interface{}, contextual bool) *ObjectDiff {
2016-05-11 05:23:34 +00:00
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Config"}
if reflect.DeepEqual(old, new) {
return nil
} else if len(old) == 0 {
diff.Type = DiffTypeAdded
} else if len(new) == 0 {
diff.Type = DiffTypeDeleted
} else {
diff.Type = DiffTypeEdited
}
// Diff the primitive fields.
oldPrimitiveFlat := flatmap.Flatten(old, nil, false)
newPrimitiveFlat := flatmap.Flatten(new, nil, false)
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2016-05-11 05:23:34 +00:00
return diff
}
// ObjectDiff contains the diff of two generic objects.
type ObjectDiff struct {
Type DiffType
Name string
Fields []*FieldDiff
Objects []*ObjectDiff
}
func (o *ObjectDiff) GoString() string {
out := fmt.Sprintf("\n%q (%s) {\n", o.Name, o.Type)
for _, f := range o.Fields {
out += fmt.Sprintf("%#v\n", f)
}
for _, o := range o.Objects {
out += fmt.Sprintf("%#v\n", o)
}
out += "}"
return out
}
func (o *ObjectDiff) Less(other *ObjectDiff) bool {
if reflect.DeepEqual(o, other) {
return false
} else if other == nil {
return false
} else if o == nil {
return true
}
if o.Name != other.Name {
return o.Name < other.Name
}
if o.Type != other.Type {
return o.Type.Less(other.Type)
}
if lO, lOther := len(o.Fields), len(other.Fields); lO != lOther {
return lO < lOther
}
if lO, lOther := len(o.Objects), len(other.Objects); lO != lOther {
return lO < lOther
}
// Check each field
sort.Sort(FieldDiffs(o.Fields))
sort.Sort(FieldDiffs(other.Fields))
for i, oV := range o.Fields {
if oV.Less(other.Fields[i]) {
return true
}
}
// Check each object
sort.Sort(ObjectDiffs(o.Objects))
sort.Sort(ObjectDiffs(other.Objects))
for i, oV := range o.Objects {
if oV.Less(other.Objects[i]) {
return true
}
}
return false
}
// For sorting ObjectDiffs
type ObjectDiffs []*ObjectDiff
func (o ObjectDiffs) Len() int { return len(o) }
func (o ObjectDiffs) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o ObjectDiffs) Less(i, j int) bool { return o[i].Less(o[j]) }
type FieldDiff struct {
2016-05-11 22:36:28 +00:00
Type DiffType
Name string
Old, New string
Annotations []string
2016-05-11 05:23:34 +00:00
}
2016-05-11 18:10:09 +00:00
// fieldDiff returns a FieldDiff if old and new are different otherwise, it
// returns nil. If contextual diff is enabled, even non-changed fields will be
// returned.
func fieldDiff(old, new, name string, contextual bool) *FieldDiff {
diff := &FieldDiff{Name: name, Type: DiffTypeNone}
2016-05-11 05:23:34 +00:00
if old == new {
2016-05-11 18:10:09 +00:00
if !contextual {
return nil
}
diff.Old, diff.New = old, new
return diff
2016-05-11 05:23:34 +00:00
}
if old == "" {
diff.Type = DiffTypeAdded
diff.New = new
} else if new == "" {
diff.Type = DiffTypeDeleted
diff.Old = old
} else {
diff.Type = DiffTypeEdited
diff.Old = old
diff.New = new
}
return diff
}
func (f *FieldDiff) GoString() string {
2016-05-11 22:36:28 +00:00
out := fmt.Sprintf("%q (%s): %q => %q", f.Name, f.Type, f.Old, f.New)
if len(f.Annotations) != 0 {
out += fmt.Sprintf(" (%s)", strings.Join(f.Annotations, ", "))
}
return out
2016-05-11 05:23:34 +00:00
}
func (f *FieldDiff) Less(other *FieldDiff) bool {
if reflect.DeepEqual(f, other) {
return false
} else if other == nil {
return false
} else if f == nil {
return true
}
if f.Name != other.Name {
return f.Name < other.Name
} else if f.Old != other.Old {
return f.Old < other.Old
}
return f.New < other.New
}
// For sorting FieldDiffs
type FieldDiffs []*FieldDiff
func (f FieldDiffs) Len() int { return len(f) }
func (f FieldDiffs) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f FieldDiffs) Less(i, j int) bool { return f[i].Less(f[j]) }
// fieldDiffs takes a map of field names to their values and returns a set of
2016-05-11 18:10:09 +00:00
// field diffs. If contextual diff is enabled, even non-changed fields will be
// returned.
func fieldDiffs(old, new map[string]string, contextual bool) []*FieldDiff {
2016-05-11 05:23:34 +00:00
var diffs []*FieldDiff
visited := make(map[string]struct{})
for k, oldV := range old {
visited[k] = struct{}{}
newV := new[k]
2016-05-11 18:10:09 +00:00
if diff := fieldDiff(oldV, newV, k, contextual); diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
for k, newV := range new {
if _, ok := visited[k]; !ok {
2016-05-11 18:10:09 +00:00
if diff := fieldDiff("", newV, k, contextual); diff != nil {
2016-05-11 05:23:34 +00:00
diffs = append(diffs, diff)
}
}
}
sort.Sort(FieldDiffs(diffs))
return diffs
}
// stringSetDiff diffs two sets of strings with the given name.
2016-09-21 20:49:34 +00:00
func stringSetDiff(old, new []string, name string, contextual bool) *ObjectDiff {
2016-05-11 05:23:34 +00:00
oldMap := make(map[string]struct{}, len(old))
newMap := make(map[string]struct{}, len(new))
for _, o := range old {
oldMap[o] = struct{}{}
}
for _, n := range new {
newMap[n] = struct{}{}
}
2016-09-21 20:49:34 +00:00
if reflect.DeepEqual(oldMap, newMap) && !contextual {
2016-05-11 05:23:34 +00:00
return nil
}
diff := &ObjectDiff{Name: name}
var added, removed bool
for k := range oldMap {
if _, ok := newMap[k]; !ok {
2016-09-21 20:49:34 +00:00
diff.Fields = append(diff.Fields, fieldDiff(k, "", name, contextual))
2016-05-11 05:23:34 +00:00
removed = true
2016-09-21 20:49:34 +00:00
} else if contextual {
diff.Fields = append(diff.Fields, fieldDiff(k, k, name, contextual))
2016-05-11 05:23:34 +00:00
}
}
for k := range newMap {
if _, ok := oldMap[k]; !ok {
2016-09-21 20:49:34 +00:00
diff.Fields = append(diff.Fields, fieldDiff("", k, name, contextual))
2016-05-11 05:23:34 +00:00
added = true
}
}
sort.Sort(FieldDiffs(diff.Fields))
// Determine the type
if added && removed {
diff.Type = DiffTypeEdited
} else if added {
diff.Type = DiffTypeAdded
2016-09-21 20:49:34 +00:00
} else if removed {
2016-05-11 05:23:34 +00:00
diff.Type = DiffTypeDeleted
2016-09-21 20:49:34 +00:00
} else {
// Diff of an empty set
if len(diff.Fields) == 0 {
return nil
}
diff.Type = DiffTypeNone
2016-05-11 05:23:34 +00:00
}
return diff
}
// primitiveObjectDiff returns a diff of the passed objects' primitive fields.
// The filter field can be used to exclude fields from the diff. The name is the
2016-05-11 18:10:09 +00:00
// name of the objects. If contextual is set, non-changed fields will also be
// stored in the object diff.
func primitiveObjectDiff(old, new interface{}, filter []string, name string, contextual bool) *ObjectDiff {
2016-05-11 05:23:34 +00:00
oldPrimitiveFlat := flatmap.Flatten(old, filter, true)
newPrimitiveFlat := flatmap.Flatten(new, filter, true)
delete(oldPrimitiveFlat, "")
delete(newPrimitiveFlat, "")
diff := &ObjectDiff{Name: name}
2016-05-11 18:10:09 +00:00
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)
2016-05-11 05:23:34 +00:00
var added, deleted, edited bool
2020-11-13 13:28:11 +00:00
Loop:
2016-05-11 05:23:34 +00:00
for _, f := range diff.Fields {
switch f.Type {
case DiffTypeEdited:
edited = true
2020-11-13 13:28:11 +00:00
break Loop
2016-05-11 05:23:34 +00:00
case DiffTypeDeleted:
deleted = true
case DiffTypeAdded:
added = true
}
}
if edited || added && deleted {
diff.Type = DiffTypeEdited
} else if added {
diff.Type = DiffTypeAdded
} else if deleted {
diff.Type = DiffTypeDeleted
} else {
return nil
}
return diff
}
// primitiveObjectSetDiff does a set difference of the old and new sets. The
// filter parameter can be used to filter a set of primitive fields in the
2016-05-11 18:10:09 +00:00
// passed structs. The name corresponds to the name of the passed objects. If
2018-03-11 18:38:46 +00:00
// contextual diff is enabled, objects' primitive fields will be returned even if
2016-05-11 18:10:09 +00:00
// no diff exists.
func primitiveObjectSetDiff(old, new []interface{}, filter []string, name string, contextual bool) []*ObjectDiff {
2016-05-11 05:23:34 +00:00
makeSet := func(objects []interface{}) map[string]interface{} {
objMap := make(map[string]interface{}, len(objects))
for _, obj := range objects {
var key string
if diffable, ok := obj.(DiffableWithID); ok {
key = diffable.DiffID()
2016-05-11 05:23:34 +00:00
}
if key == "" {
hash, err := hashstructure.Hash(obj, nil)
if err != nil {
panic(err)
}
key = fmt.Sprintf("%d", hash)
}
objMap[key] = obj
2016-05-11 05:23:34 +00:00
}
return objMap
}
oldSet := makeSet(old)
newSet := makeSet(new)
var diffs []*ObjectDiff
for k, oldObj := range oldSet {
newObj := newSet[k]
diff := primitiveObjectDiff(oldObj, newObj, filter, name, contextual)
if diff != nil {
diffs = append(diffs, diff)
2016-05-11 05:23:34 +00:00
}
}
for k, v := range newSet {
2016-05-11 18:10:09 +00:00
// Added
2016-05-11 05:23:34 +00:00
if _, ok := oldSet[k]; !ok {
2016-05-11 18:10:09 +00:00
diffs = append(diffs, primitiveObjectDiff(nil, v, filter, name, contextual))
2016-05-11 05:23:34 +00:00
}
}
sort.Sort(ObjectDiffs(diffs))
return diffs
}
// interfaceSlice is a helper method that takes a slice of typed elements and
// returns a slice of interface. This method will panic if given a non-slice
// input.
func interfaceSlice(slice interface{}) []interface{} {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
panic("InterfaceSlice() given a non-slice type")
}
ret := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
ret[i] = s.Index(i).Interface()
}
return ret
}