From b0d3f1ba28673929b3e68fa04783cd363222c672 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 11 May 2016 15:25:59 -0700 Subject: [PATCH] Add service diff --- nomad/structs/diff.go | 132 +++++++++++ nomad/structs/diff_test.go | 446 +++++++++++++++++++++++++++++++++++++ 2 files changed, 578 insertions(+) diff --git a/nomad/structs/diff.go b/nomad/structs/diff.go index ece8b4c9b..6cd8a5be2 100644 --- a/nomad/structs/diff.go +++ b/nomad/structs/diff.go @@ -388,6 +388,11 @@ func (t *Task) Diff(other *Task, contextual bool) (*TaskDiff, error) { diff.Objects = append(diff.Objects, diffs...) } + // Services diff + if sDiffs := serviceDiffs(t.Services, other.Services, contextual); sDiffs != nil { + diff.Objects = append(diff.Objects, sDiffs...) + } + return diff, nil } @@ -454,6 +459,133 @@ 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 occured. +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) + + // Checks diffs + if cDiffs := serviceCheckDiffs(old.Checks, new.Checks, contextual); cDiffs != nil { + diff.Objects = append(diff.Objects, cDiffs...) + } + + 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 { + oldMap := make(map[string]*Service, len(old)) + newMap := make(map[string]*Service, len(new)) + for _, o := range old { + oldMap[o.Name] = o + } + for _, n := range new { + newMap[n.Name] = n + } + + var diffs []*ObjectDiff + for name, oldService := range oldMap { + // Diff the same, deleted and edited + if diff := serviceDiff(oldService, newMap[name], contextual); diff != nil { + diffs = append(diffs, diff) + } + } + + for name, newService := range newMap { + // Diff the added + if old, ok := oldMap[name]; !ok { + if diff := serviceDiff(old, newService, contextual); diff != nil { + diffs = append(diffs, diff) + } + } + } + + sort.Sort(ObjectDiffs(diffs)) + return diffs +} + +// serviceCheckDiff returns the diff of two service check objects. If contextual +// diff is enabled, all fields will be returned, even if no diff occured. +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) + 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, oldService := range oldMap { + // Diff the same, deleted and edited + if diff := serviceCheckDiff(oldService, newMap[name], contextual); diff != nil { + diffs = append(diffs, diff) + } + } + + for name, newService := range newMap { + // Diff the added + if old, ok := oldMap[name]; !ok { + if diff := serviceCheckDiff(old, newService, contextual); diff != nil { + diffs = append(diffs, diff) + } + } + } + + sort.Sort(ObjectDiffs(diffs)) + return diffs +} + // 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 --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go index a41b67491..45b7c3835 100644 --- a/nomad/structs/diff_test.go +++ b/nomad/structs/diff_test.go @@ -2263,6 +2263,452 @@ func TestTaskDiff(t *testing.T) { }, }, }, + { + // Services edited (no checks) + Old: &Task{ + Services: []*Service{ + { + Name: "foo", + PortLabel: "foo", + }, + { + Name: "bar", + PortLabel: "bar", + }, + { + Name: "baz", + PortLabel: "baz", + }, + }, + }, + New: &Task{ + Services: []*Service{ + { + Name: "bar", + PortLabel: "bar", + }, + { + Name: "baz", + PortLabel: "baz2", + }, + { + Name: "bam", + PortLabel: "bam", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Service", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "PortLabel", + Old: "baz", + New: "baz2", + }, + }, + }, + { + Type: DiffTypeAdded, + Name: "Service", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Name", + Old: "", + New: "bam", + }, + { + Type: DiffTypeAdded, + Name: "PortLabel", + Old: "", + New: "bam", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Service", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Name", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "PortLabel", + Old: "foo", + New: "", + }, + }, + }, + }, + }, + }, + { + // Services edited (no checks) with context + Contextual: true, + Old: &Task{ + Services: []*Service{ + { + Name: "foo", + PortLabel: "foo", + }, + }, + }, + New: &Task{ + Services: []*Service{ + { + Name: "foo", + PortLabel: "bar", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Service", + Fields: []*FieldDiff{ + { + Type: DiffTypeNone, + Name: "Name", + Old: "foo", + New: "foo", + }, + { + Type: DiffTypeEdited, + Name: "PortLabel", + Old: "foo", + New: "bar", + }, + }, + }, + }, + }, + }, + { + // Service Checks edited + Old: &Task{ + Services: []*Service{ + { + Name: "foo", + Checks: []*ServiceCheck{ + { + Name: "foo", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + { + Name: "bar", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + { + Name: "baz", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + }, + }, + }, + }, + New: &Task{ + Services: []*Service{ + { + Name: "foo", + Checks: []*ServiceCheck{ + { + Name: "bar", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + { + Name: "baz", + Type: "tcp", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + { + Name: "bam", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + }, + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Service", + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Check", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Type", + Old: "http", + New: "tcp", + }, + }, + }, + { + Type: DiffTypeAdded, + Name: "Check", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Command", + Old: "", + New: "foo", + }, + { + Type: DiffTypeAdded, + Name: "Interval", + Old: "", + New: "1000000000", + }, + { + Type: DiffTypeAdded, + Name: "Name", + Old: "", + New: "bam", + }, + { + Type: DiffTypeAdded, + Name: "Path", + Old: "", + New: "foo", + }, + { + Type: DiffTypeAdded, + Name: "Protocol", + Old: "", + New: "http", + }, + { + Type: DiffTypeAdded, + Name: "Timeout", + Old: "", + New: "1000000000", + }, + { + Type: DiffTypeAdded, + Name: "Type", + Old: "", + New: "http", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Check", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Command", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Interval", + Old: "1000000000", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Name", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Path", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Protocol", + Old: "http", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Timeout", + Old: "1000000000", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Type", + Old: "http", + New: "", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Service Checks edited with context + Contextual: true, + Old: &Task{ + Services: []*Service{ + { + Name: "foo", + Checks: []*ServiceCheck{ + { + Name: "foo", + Type: "http", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + }, + }, + }, + }, + New: &Task{ + Services: []*Service{ + { + Name: "foo", + Checks: []*ServiceCheck{ + { + Name: "foo", + Type: "tcp", + Command: "foo", + Args: []string{"foo"}, + Path: "foo", + Protocol: "http", + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + }, + }, + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Service", + Fields: []*FieldDiff{ + { + Type: DiffTypeNone, + Name: "Name", + Old: "foo", + New: "foo", + }, + { + Type: DiffTypeNone, + Name: "PortLabel", + Old: "", + New: "", + }, + }, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Check", + Fields: []*FieldDiff{ + { + Type: DiffTypeNone, + Name: "Command", + Old: "foo", + New: "foo", + }, + { + Type: DiffTypeNone, + Name: "Interval", + Old: "1000000000", + New: "1000000000", + }, + { + Type: DiffTypeNone, + Name: "Name", + Old: "foo", + New: "foo", + }, + { + Type: DiffTypeNone, + Name: "Path", + Old: "foo", + New: "foo", + }, + { + Type: DiffTypeNone, + Name: "Protocol", + Old: "http", + New: "http", + }, + { + Type: DiffTypeNone, + Name: "Timeout", + Old: "1000000000", + New: "1000000000", + }, + { + Type: DiffTypeEdited, + Name: "Type", + Old: "http", + New: "tcp", + }, + }, + }, + }, + }, + }, + }, + }, } for i, c := range cases {