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 } // 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 // 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 diff := &JobDiff{Type: DiffTypeNone} var oldPrimitiveFlat, newPrimitiveFlat map[string]string filter := []string{"ID", "Status", "StatusDescription", "Version", "Stable", "CreateIndex", "ModifyIndex", "JobModifyIndex", "Update", "SubmitTime", "NomadTokenID", "VaultToken"} 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. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false) // Datacenters diff if setDiff := stringSetDiff(j.Datacenters, other.Datacenters, "Datacenters", contextual); setDiff != nil && setDiff.Type != DiffTypeNone { diff.Objects = append(diff.Objects, setDiff) } // Constraints diff conDiff := primitiveObjectSetDiff( interfaceSlice(j.Constraints), interfaceSlice(other.Constraints), []string{"str"}, "Constraint", contextual) 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...) } // Task groups diff tgs, err := taskGroupDiffs(j.TaskGroups, other.TaskGroups, contextual) if err != nil { return nil, err } diff.TaskGroups = tgs // Periodic diff if pDiff := primitiveObjectDiff(j.Periodic, other.Periodic, nil, "Periodic", contextual); pDiff != nil { diff.Objects = append(diff.Objects, pDiff) } // ParameterizedJob diff if cDiff := parameterizedJobDiff(j.ParameterizedJob, other.ParameterizedJob, contextual); cDiff != nil { diff.Objects = append(diff.Objects, cDiff) } // Multiregion diff if mrDiff := multiregionDiff(j.Multiregion, other.Multiregion, contextual); mrDiff != nil { diff.Objects = append(diff.Objects, mrDiff) } // 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 } } } 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 Updates map[string]uint64 } // Diff returns a diff of two task groups. If contextual diff is enabled, // objects' fields will be stored even if no diff occurred as long as one field // changed. func (tg *TaskGroup) Diff(other *TaskGroup, contextual bool) (*TaskGroupDiff, error) { 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 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) } } // Diff the primitive fields. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false) // Constraints diff conDiff := primitiveObjectSetDiff( interfaceSlice(tg.Constraints), interfaceSlice(other.Constraints), []string{"str"}, "Constraint", contextual) if conDiff != nil { diff.Objects = append(diff.Objects, conDiff...) } // Affinities diff affinitiesDiff := primitiveObjectSetDiff( interfaceSlice(tg.Affinities), interfaceSlice(other.Affinities), []string{"str"}, "Affinity", contextual) if affinitiesDiff != nil { diff.Objects = append(diff.Objects, affinitiesDiff...) } // Restart policy diff rDiff := primitiveObjectDiff(tg.RestartPolicy, other.RestartPolicy, nil, "RestartPolicy", contextual) if rDiff != nil { 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) 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) } // 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...) } // Tasks diff tasks, err := taskDiffs(tg.Tasks, other.Tasks, contextual) 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 } // TaskGroupDiffs diffs two sets of task groups. If contextual diff is enabled, // objects' fields will be stored even if no diff occurred as long as one field // changed. func taskGroupDiffs(old, new []*TaskGroup, contextual bool) ([]*TaskGroupDiff, error) { 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 diff, err := oldGroup.Diff(newMap[name], contextual) if err != nil { return nil, err } diffs = append(diffs, diff) } for name, newGroup := range newMap { // Diff the added if old, ok := oldMap[name]; !ok { diff, err := old.Diff(newGroup, contextual) 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 } // 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) { 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. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, false) // Constraints diff conDiff := primitiveObjectSetDiff( interfaceSlice(t.Constraints), interfaceSlice(other.Constraints), []string{"str"}, "Constraint", contextual) 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...) } // Config diff if cDiff := configDiff(t.Config, other.Config, contextual); cDiff != nil { diff.Objects = append(diff.Objects, cDiff) } // Resources diff if rDiff := t.Resources.Diff(other.Resources, contextual); rDiff != nil { diff.Objects = append(diff.Objects, rDiff) } // LogConfig diff lDiff := primitiveObjectDiff(t.LogConfig, other.LogConfig, nil, "LogConfig", contextual) if lDiff != nil { diff.Objects = append(diff.Objects, lDiff) } // Dispatch payload diff dDiff := primitiveObjectDiff(t.DispatchPayload, other.DispatchPayload, nil, "DispatchPayload", contextual) if dDiff != nil { diff.Objects = append(diff.Objects, dDiff) } // Artifacts diff diffs := primitiveObjectSetDiff( interfaceSlice(t.Artifacts), interfaceSlice(other.Artifacts), nil, "Artifact", contextual) if diffs != nil { diff.Objects = append(diff.Objects, diffs...) } // Services diff if sDiffs := serviceDiffs(t.Services, other.Services, contextual); sDiffs != nil { diff.Objects = append(diff.Objects, sDiffs...) } // Vault diff vDiff := vaultDiff(t.Vault, other.Vault, contextual) if vDiff != nil { diff.Objects = append(diff.Objects, vDiff) } // Template diff tmplDiffs := templateDiffs(t.Templates, other.Templates, contextual) if tmplDiffs != nil { diff.Objects = append(diff.Objects, tmplDiffs...) } // Identity diff idDiffs := idDiff(t.Identity, other.Identity, contextual) if idDiffs != nil { diff.Objects = append(diff.Objects, idDiffs) } 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 } // 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) { 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 diff, err := oldGroup.Diff(newMap[name], contextual) if err != nil { return nil, err } diffs = append(diffs, diff) } for name, newGroup := range newMap { // Diff the added if old, ok := oldMap[name]; !ok { diff, err := old.Diff(newGroup, contextual) 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 } // serviceDiff returns the diff of two service objects. If contextual diff is // enabled, all fields will be returned, even if no diff occurred. func serviceDiff(old, new *Service, contextual bool) *ObjectDiff { diff := &ObjectDiff{Type: DiffTypeNone, Name: "Service"} var oldPrimitiveFlat, newPrimitiveFlat map[string]string if reflect.DeepEqual(old, new) { return nil } else if old == nil { old = &Service{} diff.Type = DiffTypeAdded newPrimitiveFlat = flatmap.Flatten(new, nil, true) } else if new == nil { new = &Service{} 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) 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) } // 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) } return diff } // serviceDiffs diffs a set of services. If contextual diff is enabled, unchanged // fields within objects nested in the tasks will be returned. 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 } // 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 } // Find a match in the new services list for each old service and compute // their diffs. 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 { 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) } } 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 } // serviceCheckDiff returns the diff of two service check objects. If contextual // diff is enabled, all fields will be returned, even if no diff occurred. 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 if headerDiff := checkHeaderDiff(old.Header, new.Header, contextual); headerDiff != nil { diff.Objects = append(diff.Objects, headerDiff) } // Diff check_restart if crDiff := checkRestartDiff(old.CheckRestart, new.CheckRestart, contextual); crDiff != nil { diff.Objects = append(diff.Objects, crDiff) } 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"} var oldFlat, newFlat map[string]string if reflect.DeepEqual(old, new) { return nil } else if len(old) == 0 { diff.Type = DiffTypeAdded newFlat = flatmap.Flatten(new, nil, false) } else if len(new) == 0 { diff.Type = DiffTypeDeleted oldFlat = flatmap.Flatten(old, nil, false) } else { diff.Type = DiffTypeEdited oldFlat = flatmap.Flatten(old, nil, false) newFlat = flatmap.Flatten(new, nil, false) } diff.Fields = fieldDiffs(oldFlat, newFlat, contextual) return diff } // 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"} var oldFlat, newFlat map[string]string if reflect.DeepEqual(old, new) { return nil } else if old == nil { diff.Type = DiffTypeAdded newFlat = flatmap.Flatten(new, nil, true) diff.Type = DiffTypeAdded } else if new == nil { diff.Type = DiffTypeDeleted oldFlat = flatmap.Flatten(old, nil, true) } else { diff.Type = DiffTypeEdited oldFlat = flatmap.Flatten(old, nil, true) newFlat = flatmap.Flatten(new, nil, true) } 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) } // 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 { oldPrimitiveFlat["ConnectTimeout"] = prev.ConnectTimeout.String() } if next.ConnectTimeout == nil { newPrimitiveFlat["ConnectTimeout"] = "" } else { 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) } // diff the primitive fields diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) // diff the consul upstream slices if upDiffs := consulProxyUpstreamsDiff(old.Upstreams, new.Upstreams, contextual); upDiffs != nil { diff.Objects = append(diff.Objects, upDiffs...) } // diff the config blob if cDiff := configDiff(old.Config, new.Config, contextual); cDiff != nil { diff.Objects = append(diff.Objects, cDiff) } return diff } // 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)) { prev = ConsulUpstream{} diff.Type = DiffTypeAdded newPrimFlat = flatmap.Flatten(next, nil, true) } else if next.Equal(new(ConsulUpstream)) { 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 } // 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 { // Diff the same, deleted and edited if diff := serviceCheckDiff(oldCheck, newMap[name], contextual); diff != nil { diffs = append(diffs, diff) } } for name, newCheck := range newMap { // Diff the added if old, ok := oldMap[name]; !ok { if diff := serviceCheckDiff(old, newCheck, contextual); diff != nil { diffs = append(diffs, diff) } } } sort.Sort(ObjectDiffs(diffs)) return diffs } // 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"} var oldPrimitiveFlat, newPrimitiveFlat map[string]string if reflect.DeepEqual(old, new) { return nil } else if old == nil { old = &ParameterizedJobConfig{} diff.Type = DiffTypeAdded newPrimitiveFlat = flatmap.Flatten(new, nil, true) } else if new == nil { new = &ParameterizedJobConfig{} 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 if optionalDiff := stringSetDiff(old.MetaOptional, new.MetaOptional, "MetaOptional", contextual); optionalDiff != nil { diff.Objects = append(diff.Objects, optionalDiff) } if requiredDiff := stringSetDiff(old.MetaRequired, new.MetaRequired, "MetaRequired", contextual); requiredDiff != nil { 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 Loop: for _, f := range diff.Fields { switch f.Type { case DiffTypeEdited: edited = true 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) } 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 } // 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 { 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. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) // Network Resources diff if nDiffs := networkResourceDiffs(r.Networks, other.Networks, contextual); nDiffs != nil { diff.Objects = append(diff.Objects, nDiffs...) } // Requested Devices diff if nDiffs := requestedDevicesDiffs(r.Devices, other.Devices, contextual); nDiffs != nil { diff.Objects = append(diff.Objects, nDiffs...) } return diff } // 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 { diff := &ObjectDiff{Type: DiffTypeNone, Name: "Network"} var oldPrimitiveFlat, newPrimitiveFlat map[string]string filter := []string{"Device", "CIDR", "IP"} if reflect.DeepEqual(n, other) { return nil } else if n == nil { n = &NetworkResource{} diff.Type = DiffTypeAdded newPrimitiveFlat = flatmap.Flatten(other, filter, true) } else if other == nil { other = &NetworkResource{} diff.Type = DiffTypeDeleted oldPrimitiveFlat = flatmap.Flatten(n, filter, true) } else { diff.Type = DiffTypeEdited oldPrimitiveFlat = flatmap.Flatten(n, filter, true) newPrimitiveFlat = flatmap.Flatten(other, filter, true) } // Diff the primitive fields. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) // Port diffs resPorts := portDiffs(n.ReservedPorts, other.ReservedPorts, false, contextual) dynPorts := portDiffs(n.DynamicPorts, other.DynamicPorts, true, contextual) if resPorts != nil { diff.Objects = append(diff.Objects, resPorts...) } if dynPorts != nil { 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) return diff } // 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 { 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 { 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 } // portDiffs returns the diff of two sets of ports. The dynamic flag marks the // 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 { 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 { diff := primitiveObjectDiff(oldPort, newPort, filter, name, contextual) if diff != nil { diffs = append(diffs, diff) } } else { diff := primitiveObjectDiff(oldPort, nil, filter, name, contextual) if diff != nil { diffs = append(diffs, diff) } } } for label, newPort := range newPorts { // Diff the added if _, ok := oldPorts[label]; !ok { diff := primitiveObjectDiff(nil, newPort, filter, name, contextual) if diff != nil { diffs = append(diffs, diff) } } } sort.Sort(ObjectDiffs(diffs)) return diffs } // 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 } // configDiff returns the diff of two Task Config objects. If contextual diff is // enabled, all fields will be returned, even if no diff occurred. func configDiff(old, new map[string]interface{}, contextual bool) *ObjectDiff { 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) diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) return diff } // idDiff returns the diff of two identity objects. If contextual diff is // enabled, all fields will be returned, even if no diff occurred. func idDiff(oldWI, newWI *WorkloadIdentity, contextual bool) *ObjectDiff { diff := &ObjectDiff{Type: DiffTypeNone, Name: "Identity"} var oldPrimitiveFlat, newPrimitiveFlat map[string]string if reflect.DeepEqual(oldWI, newWI) { return nil } else if oldWI == nil { diff.Type = DiffTypeAdded newPrimitiveFlat = flatmap.Flatten(newWI, nil, true) } else if newWI == nil { diff.Type = DiffTypeDeleted oldPrimitiveFlat = flatmap.Flatten(oldWI, nil, true) } else { diff.Type = DiffTypeEdited oldPrimitiveFlat = flatmap.Flatten(oldWI, nil, true) newPrimitiveFlat = flatmap.Flatten(newWI, nil, true) } // Diff the primitive fields. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) 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 { Type DiffType Name string Old, New string Annotations []string } // 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} if old == new { if !contextual { return nil } diff.Old, diff.New = old, new return diff } 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 { 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 } 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 // field diffs. If contextual diff is enabled, even non-changed fields will be // returned. func fieldDiffs(old, new map[string]string, contextual bool) []*FieldDiff { var diffs []*FieldDiff visited := make(map[string]struct{}) for k, oldV := range old { visited[k] = struct{}{} newV := new[k] if diff := fieldDiff(oldV, newV, k, contextual); diff != nil { diffs = append(diffs, diff) } } for k, newV := range new { if _, ok := visited[k]; !ok { if diff := fieldDiff("", newV, k, contextual); diff != nil { diffs = append(diffs, diff) } } } sort.Sort(FieldDiffs(diffs)) return diffs } // stringSetDiff diffs two sets of strings with the given name. func stringSetDiff(old, new []string, name string, contextual bool) *ObjectDiff { 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{}{} } if reflect.DeepEqual(oldMap, newMap) && !contextual { return nil } diff := &ObjectDiff{Name: name} var added, removed bool for k := range oldMap { if _, ok := newMap[k]; !ok { diff.Fields = append(diff.Fields, fieldDiff(k, "", name, contextual)) removed = true } else if contextual { diff.Fields = append(diff.Fields, fieldDiff(k, k, name, contextual)) } } for k := range newMap { if _, ok := oldMap[k]; !ok { diff.Fields = append(diff.Fields, fieldDiff("", k, name, contextual)) added = true } } sort.Sort(FieldDiffs(diff.Fields)) // Determine the type if added && removed { diff.Type = DiffTypeEdited } else if added { diff.Type = DiffTypeAdded } else if removed { diff.Type = DiffTypeDeleted } else { // Diff of an empty set if len(diff.Fields) == 0 { return nil } diff.Type = DiffTypeNone } 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 // 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 { oldPrimitiveFlat := flatmap.Flatten(old, filter, true) newPrimitiveFlat := flatmap.Flatten(new, filter, true) delete(oldPrimitiveFlat, "") delete(newPrimitiveFlat, "") diff := &ObjectDiff{Name: name} diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) var added, deleted, edited bool Loop: for _, f := range diff.Fields { switch f.Type { case DiffTypeEdited: edited = true 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 } // 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 // passed structs. The name corresponds to the name of the passed objects. If // contextual diff is enabled, objects' primitive fields will be returned even if // no diff exists. func primitiveObjectSetDiff(old, new []interface{}, filter []string, name string, contextual bool) []*ObjectDiff { 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() } if key == "" { hash, err := hashstructure.Hash(obj, nil) if err != nil { panic(err) } key = fmt.Sprintf("%d", hash) } objMap[key] = obj } 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) } } for k, v := range newSet { // Added if _, ok := oldSet[k]; !ok { diffs = append(diffs, primitiveObjectDiff(nil, v, filter, name, contextual)) } } 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 }