From fefc55906e572ba5debdfe3a076b0d16ac040b6a Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 10 May 2016 22:23:34 -0700 Subject: [PATCH] Job diff using generic structures --- helper/flatmap/flatmap.go | 135 +++ helper/flatmap/flatmap_test.go | 232 ++++ nomad/structs/diff.go | 915 +++++++++++++++ nomad/structs/diff_test.go | 1942 ++++++++++++++++++++++++++++++++ 4 files changed, 3224 insertions(+) create mode 100644 helper/flatmap/flatmap.go create mode 100644 helper/flatmap/flatmap_test.go create mode 100644 nomad/structs/diff.go create mode 100644 nomad/structs/diff_test.go diff --git a/helper/flatmap/flatmap.go b/helper/flatmap/flatmap.go new file mode 100644 index 000000000..622ef7658 --- /dev/null +++ b/helper/flatmap/flatmap.go @@ -0,0 +1,135 @@ +package flatmap + +import ( + "fmt" + "reflect" +) + +// Flatten takes an object and returns a flat map of the object. The keys of the +// map is the path of the field names until a primitive field is reached and the +// value is a string representation of the terminal field. +func Flatten(obj interface{}, filter []string, primitiveOnly bool) map[string]string { + flat := make(map[string]string) + v := reflect.ValueOf(obj) + if !v.IsValid() { + return nil + } + + flatten("", v, primitiveOnly, false, flat) + for _, f := range filter { + if _, ok := flat[f]; ok { + delete(flat, f) + } + } + return flat +} + +// flatten recursively calls itself to create a flatmap representation of the +// passed value. The results are stored into the output map and the keys are +// the fields prepended with the passed prefix. +// XXX: A current restriction is that maps only support string keys. +func flatten(prefix string, v reflect.Value, primitiveOnly, enteredStruct bool, output map[string]string) { + switch v.Kind() { + case reflect.Bool: + output[prefix] = fmt.Sprintf("%v", v.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + output[prefix] = fmt.Sprintf("%v", v.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + output[prefix] = fmt.Sprintf("%v", v.Uint()) + case reflect.Float32, reflect.Float64: + output[prefix] = fmt.Sprintf("%v", v.Float()) + case reflect.Complex64, reflect.Complex128: + output[prefix] = fmt.Sprintf("%v", v.Complex()) + case reflect.String: + output[prefix] = fmt.Sprintf("%v", v.String()) + case reflect.Invalid: + output[prefix] = "nil" + case reflect.Ptr: + if primitiveOnly && enteredStruct { + return + } + + e := v.Elem() + if !e.IsValid() { + output[prefix] = "nil" + } + flatten(prefix, e, primitiveOnly, enteredStruct, output) + case reflect.Map: + for _, k := range v.MapKeys() { + if k.Kind() == reflect.Interface { + k = k.Elem() + } + + if k.Kind() != reflect.String { + panic(fmt.Sprintf("%q: map key is not string: %s", prefix, k)) + } + + flatten(getSubKeyPrefix(prefix, k.String()), v.MapIndex(k), primitiveOnly, enteredStruct, output) + } + case reflect.Struct: + if primitiveOnly && enteredStruct { + return + } + enteredStruct = true + + t := v.Type() + for i := 0; i < v.NumField(); i++ { + name := t.Field(i).Name + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + + flatten(getSubPrefix(prefix, name), val, primitiveOnly, enteredStruct, output) + } + case reflect.Interface: + if primitiveOnly { + return + } + + e := v.Elem() + if !e.IsValid() { + output[prefix] = "nil" + return + } + flatten(prefix, e, primitiveOnly, enteredStruct, output) + case reflect.Array, reflect.Slice: + if primitiveOnly { + return + } + + if v.Kind() == reflect.Slice && v.IsNil() { + output[prefix] = "nil" + return + } + for i := 0; i < v.Len(); i++ { + flatten(fmt.Sprintf("%s[%d]", prefix, i), v.Index(i), primitiveOnly, enteredStruct, output) + } + default: + panic(fmt.Sprintf("prefix %q; unsupported type %v", prefix, v.Kind())) + } +} + +// getSubPrefix takes the current prefix and the next subfield and returns an +// appropriate prefix. +func getSubPrefix(curPrefix, subField string) string { + newPrefix := "" + if curPrefix != "" { + newPrefix = fmt.Sprintf("%s.%s", curPrefix, subField) + } else { + newPrefix = fmt.Sprintf("%s", subField) + } + return newPrefix +} + +// getSubKeyPrefix takes the current prefix and the next subfield and returns an +// appropriate prefix for a map field. +func getSubKeyPrefix(curPrefix, subField string) string { + newPrefix := "" + if curPrefix != "" { + newPrefix = fmt.Sprintf("%s[%s]", curPrefix, subField) + } else { + newPrefix = fmt.Sprintf("%s", subField) + } + return newPrefix +} diff --git a/helper/flatmap/flatmap_test.go b/helper/flatmap/flatmap_test.go new file mode 100644 index 000000000..125155152 --- /dev/null +++ b/helper/flatmap/flatmap_test.go @@ -0,0 +1,232 @@ +package flatmap + +import ( + "reflect" + "testing" +) + +type simpleTypes struct { + b bool + i int + i8 int8 + i16 int16 + i32 int32 + i64 int64 + ui uint + ui8 uint8 + ui16 uint16 + ui32 uint32 + ui64 uint64 + f32 float32 + f64 float64 + c64 complex64 + c128 complex128 + s string +} + +type linkedList struct { + value string + next *linkedList +} + +type containers struct { + myslice []int + mymap map[string]linkedList +} + +type interfaceHolder struct { + value interface{} +} + +func TestFlatMap(t *testing.T) { + cases := []struct { + Input interface{} + Expected map[string]string + Filter []string + PrimitiveOnly bool + }{ + { + Input: nil, + Expected: nil, + }, + { + Input: &simpleTypes{ + b: true, + i: -10, + i8: 88, + i16: 1616, + i32: 3232, + i64: 6464, + ui: 10, + ui8: 88, + ui16: 1616, + ui32: 3232, + ui64: 6464, + f32: 3232, + f64: 6464, + c64: 64, + c128: 128, + s: "foobar", + }, + Expected: map[string]string{ + "b": "true", + "i": "-10", + "i8": "88", + "i16": "1616", + "i32": "3232", + "i64": "6464", + "ui": "10", + "ui8": "88", + "ui16": "1616", + "ui32": "3232", + "ui64": "6464", + "f32": "3232", + "f64": "6464", + "c64": "(64+0i)", + "c128": "(128+0i)", + "s": "foobar", + }, + }, + { + Input: &simpleTypes{ + b: true, + i: -10, + i8: 88, + i16: 1616, + i32: 3232, + i64: 6464, + ui: 10, + ui8: 88, + ui16: 1616, + ui32: 3232, + ui64: 6464, + f32: 3232, + f64: 6464, + c64: 64, + c128: 128, + s: "foobar", + }, + Filter: []string{"i", "i8", "i16"}, + Expected: map[string]string{ + "b": "true", + "i32": "3232", + "i64": "6464", + "ui": "10", + "ui8": "88", + "ui16": "1616", + "ui32": "3232", + "ui64": "6464", + "f32": "3232", + "f64": "6464", + "c64": "(64+0i)", + "c128": "(128+0i)", + "s": "foobar", + }, + }, + { + Input: &linkedList{ + value: "foo", + next: &linkedList{ + value: "bar", + next: nil, + }, + }, + Expected: map[string]string{ + "value": "foo", + "next.value": "bar", + "next.next": "nil", + }, + }, + { + Input: &linkedList{ + value: "foo", + next: &linkedList{ + value: "bar", + next: nil, + }, + }, + PrimitiveOnly: true, + Expected: map[string]string{ + "value": "foo", + }, + }, + { + Input: linkedList{ + value: "foo", + next: &linkedList{ + value: "bar", + next: nil, + }, + }, + PrimitiveOnly: true, + Expected: map[string]string{ + "value": "foo", + }, + }, + { + Input: &containers{ + myslice: []int{1, 2}, + mymap: map[string]linkedList{ + "foo": linkedList{ + value: "l1", + }, + "bar": linkedList{ + value: "l2", + }, + }, + }, + Expected: map[string]string{ + "myslice[0]": "1", + "myslice[1]": "2", + "mymap[foo].value": "l1", + "mymap[foo].next": "nil", + "mymap[bar].value": "l2", + "mymap[bar].next": "nil", + }, + }, + { + Input: &containers{ + myslice: []int{1, 2}, + mymap: map[string]linkedList{ + "foo": linkedList{ + value: "l1", + }, + "bar": linkedList{ + value: "l2", + }, + }, + }, + PrimitiveOnly: true, + Expected: map[string]string{}, + }, + { + Input: &interfaceHolder{ + value: &linkedList{ + value: "foo", + next: nil, + }, + }, + Expected: map[string]string{ + "value.value": "foo", + "value.next": "nil", + }, + }, + { + Input: &interfaceHolder{ + value: &linkedList{ + value: "foo", + next: nil, + }, + }, + PrimitiveOnly: true, + Expected: map[string]string{}, + }, + } + + for i, c := range cases { + act := Flatten(c.Input, c.Filter, c.PrimitiveOnly) + if !reflect.DeepEqual(act, c.Expected) { + t.Fatalf("case %d: got %#v; want %#v", i+1, act, c.Expected) + } + } +} diff --git a/nomad/structs/diff.go b/nomad/structs/diff.go new file mode 100644 index 000000000..1f3d35794 --- /dev/null +++ b/nomad/structs/diff.go @@ -0,0 +1,915 @@ +package structs + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/hashicorp/nomad/helper/flatmap" + "github.com/mitchellh/hashstructure" +) + +// TODO: Support contextual diff + +const ( + // AnnotationForcesDestructiveUpdate marks a diff as causing a destructive + // update. + AnnotationForcesDestructiveUpdate = "forces create/destroy update" + + // AnnotationForcesInplaceUpdate marks a diff as causing an in-place + // update. + AnnotationForcesInplaceUpdate = "forces in-place update" +) + +// UpdateTypes denote the type of update to occur against the task group. +const ( + UpdateTypeIgnore = "ignore" + UpdateTypeCreate = "create" + UpdateTypeDestroy = "destroy" + UpdateTypeMigrate = "migrate" + UpdateTypeInplaceUpdate = "in-place update" + UpdateTypeDestructiveUpdate = "create/destroy update" +) + +// 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. +func (j *Job) Diff(other *Job) (*JobDiff, error) { + diff := &JobDiff{Type: DiffTypeNone} + var oldPrimitiveFlat, newPrimitiveFlat map[string]string + filter := []string{"ID", "Status", "StatusDescription", "CreateIndex", "ModifyIndex", "JobModifyIndex"} + + // TODO This logic is too complicated + 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 !reflect.DeepEqual(j, other) { + diff.Type = DiffTypeEdited + } + + 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) + + // Datacenters diff + if setDiff := stringSetDiff(j.Datacenters, other.Datacenters, "Datacenters"); setDiff != nil { + diff.Objects = append(diff.Objects, setDiff) + } + + // Constraints diff + conDiff := primitiveObjectSetDiff( + interfaceSlice(j.Constraints), + interfaceSlice(other.Constraints), + []string{"str"}, + "Constraint") + if conDiff != nil { + diff.Objects = append(diff.Objects, conDiff...) + } + + // Task groups diff + tgs, err := taskGroupDiffs(j.TaskGroups, other.TaskGroups) + if err != nil { + return nil, err + } + diff.TaskGroups = tgs + + // Update diff + if uDiff := primitiveObjectDiff(j.Update, other.Update, nil, "Update"); uDiff != nil { + diff.Objects = append(diff.Objects, uDiff) + } + + // Periodic diff + if pDiff := primitiveObjectDiff(j.Periodic, other.Periodic, nil, "Periodic"); pDiff != nil { + diff.Objects = append(diff.Objects, pDiff) + } + + 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]int +} + +// Diff returns a diff of two task groups. +func (tg *TaskGroup) Diff(other *TaskGroup) (*TaskGroupDiff, error) { + diff := &TaskGroupDiff{Type: DiffTypeNone} + var oldPrimitiveFlat, newPrimitiveFlat map[string]string + filter := []string{"Name"} + + // TODO This logic is too complicated + 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) + } + + // Diff the primitive fields. + diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat) + + // Constraints diff + conDiff := primitiveObjectSetDiff( + interfaceSlice(tg.Constraints), + interfaceSlice(other.Constraints), + []string{"str"}, + "Constraint") + if conDiff != nil { + diff.Objects = append(diff.Objects, conDiff...) + } + + // Restart policy diff + if rDiff := primitiveObjectDiff(tg.RestartPolicy, other.RestartPolicy, nil, "RestartPolicy"); rDiff != nil { + diff.Objects = append(diff.Objects, rDiff) + } + + // Tasks diff + tasks, err := taskDiffs(tg.Tasks, other.Tasks) + 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. +func taskGroupDiffs(old, new []*TaskGroup) ([]*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]) + 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) + 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. +func (t *Task) Diff(other *Task) (*TaskDiff, error) { + diff := &TaskDiff{Type: DiffTypeNone} + var oldPrimitiveFlat, newPrimitiveFlat map[string]string + filter := []string{"Name", "Config"} + + // TODO This logic is too complicated + 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) + + // Constraints diff + conDiff := primitiveObjectSetDiff( + interfaceSlice(t.Constraints), + interfaceSlice(other.Constraints), + []string{"str"}, + "Constraint") + if conDiff != nil { + diff.Objects = append(diff.Objects, conDiff...) + } + + // Config diff + if cDiff := configDiff(t.Config, other.Config); cDiff != nil { + diff.Objects = append(diff.Objects, cDiff) + } + + // Resources diff + if rDiff := t.Resources.Diff(other.Resources); rDiff != nil { + diff.Objects = append(diff.Objects, rDiff) + } + + // LogConfig diff + if lDiff := primitiveObjectDiff(t.LogConfig, other.LogConfig, nil, "LogConfig"); lDiff != nil { + diff.Objects = append(diff.Objects, lDiff) + } + + // Artifacts diff + diffs := primitiveObjectSetDiff( + interfaceSlice(t.Artifacts), + interfaceSlice(other.Artifacts), + nil, + "Artifact") + if diffs != nil { + diff.Objects = append(diff.Objects, diffs...) + } + + 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. +func taskDiffs(old, new []*Task) ([]*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]) + 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) + 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 } + +// Diff returns a diff of two resource objects. +func (r *Resources) Diff(other *Resources) *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) + + // Network Resources diff + if nDiffs := networkResourceDiffs(r.Networks, other.Networks); nDiffs != nil { + diff.Objects = append(diff.Objects, nDiffs...) + } + + return diff +} + +// Diff returns a diff of two network resources. +func (r *NetworkResource) Diff(other *NetworkResource) *ObjectDiff { + diff := &ObjectDiff{Type: DiffTypeNone, Name: "Network"} + var oldPrimitiveFlat, newPrimitiveFlat map[string]string + filter := []string{"Device", "CIDR", "IP"} + + if reflect.DeepEqual(r, other) { + return nil + } else if r == nil { + r = &NetworkResource{} + diff.Type = DiffTypeAdded + newPrimitiveFlat = flatmap.Flatten(other, filter, true) + } else if other == nil { + other = &NetworkResource{} + diff.Type = DiffTypeDeleted + oldPrimitiveFlat = flatmap.Flatten(r, filter, true) + } else { + diff.Type = DiffTypeEdited + oldPrimitiveFlat = flatmap.Flatten(r, filter, true) + newPrimitiveFlat = flatmap.Flatten(other, filter, true) + } + + // Diff the primitive fields. + diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat) + + // Port diffs + if resPorts := portDiffs(r.ReservedPorts, other.ReservedPorts, false); resPorts != nil { + diff.Objects = append(diff.Objects, resPorts...) + } + if dynPorts := portDiffs(r.DynamicPorts, other.DynamicPorts, true); dynPorts != nil { + diff.Objects = append(diff.Objects, dynPorts...) + } + + return diff +} + +// networkResourceDiffs diffs a set of NetworkResources. +func networkResourceDiffs(old, new []*NetworkResource) []*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); diff != nil { + diffs = append(diffs, diff) + } + } + } + for k, newV := range newSet { + if oldV, ok := oldSet[k]; !ok { + if diff := oldV.Diff(newV); 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. +func portDiffs(old, new []Port, dynamic 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 { + if diff := primitiveObjectDiff(oldPort, newPort, filter, name); diff != nil { + diffs = append(diffs, diff) + } + } else { + if diff := primitiveObjectDiff(oldPort, nil, filter, name); diff != nil { + diffs = append(diffs, diff) + } + } + } + for label, newPort := range newPorts { + // Diff the added + if _, ok := oldPorts[label]; !ok { + if diff := primitiveObjectDiff(nil, newPort, filter, name); diff != nil { + diffs = append(diffs, diff) + } + } + } + + sort.Sort(ObjectDiffs(diffs)) + return diffs + +} + +// configDiff returns the diff of two Task Config objects. +func configDiff(old, new map[string]interface{}) *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) + 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 +} + +// NewFieldDiff returns a FieldDiff if old and new are different otherwise, it +// returns nil. +func NewFieldDiff(old, new, name string) *FieldDiff { + if old == new { + return nil + } + + diff := &FieldDiff{Name: name} + 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 { + return fmt.Sprintf("%q (%s): %q => %q", f.Name, f.Type, f.Old, f.New) +} + +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. +func fieldDiffs(old, new map[string]string) []*FieldDiff { + var diffs []*FieldDiff + visited := make(map[string]struct{}) + for k, oldV := range old { + visited[k] = struct{}{} + newV := new[k] + if diff := NewFieldDiff(oldV, newV, k); diff != nil { + diffs = append(diffs, diff) + } + } + + for k, newV := range new { + if _, ok := visited[k]; !ok { + if diff := NewFieldDiff("", newV, k); 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) *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) { + return nil + } + + diff := &ObjectDiff{Name: name} + var added, removed bool + for k := range oldMap { + if _, ok := newMap[k]; !ok { + diff.Fields = append(diff.Fields, NewFieldDiff(k, "", name)) + removed = true + } + } + + for k := range newMap { + if _, ok := oldMap[k]; !ok { + diff.Fields = append(diff.Fields, NewFieldDiff("", k, name)) + added = true + } + } + + sort.Sort(FieldDiffs(diff.Fields)) + + // Determine the type + if added && removed { + diff.Type = DiffTypeEdited + } else if added { + diff.Type = DiffTypeAdded + } else { + diff.Type = DiffTypeDeleted + } + + 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. +func primitiveObjectDiff(old, new interface{}, filter []string, name string) *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) + + var added, deleted, edited bool + for _, f := range diff.Fields { + switch f.Type { + case DiffTypeEdited: + edited = true + break + 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. +func primitiveObjectSetDiff(old, new []interface{}, filter []string, name string) []*ObjectDiff { + makeSet := func(objects []interface{}) map[string]interface{} { + objMap := make(map[string]interface{}, 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, v := range oldSet { + if _, ok := newSet[k]; !ok { + diffs = append(diffs, primitiveObjectDiff(v, nil, filter, name)) + } + } + for k, v := range newSet { + if _, ok := oldSet[k]; !ok { + diffs = append(diffs, primitiveObjectDiff(nil, v, filter, name)) + } + } + + 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 +} diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go new file mode 100644 index 000000000..c47d415d2 --- /dev/null +++ b/nomad/structs/diff_test.go @@ -0,0 +1,1942 @@ +package structs + +import ( + "reflect" + "testing" + "time" +) + +func TestJobDiff(t *testing.T) { + cases := []struct { + Old, New *Job + Expected *JobDiff + Error bool + }{ + { + Old: nil, + New: nil, + Expected: &JobDiff{ + Type: DiffTypeNone, + }, + }, + { + // Different IDs + Old: &Job{ + ID: "foo", + }, + New: &Job{ + ID: "bar", + }, + Error: true, + }, + { + // Primitive only that is the same + Old: &Job{ + Region: "foo", + ID: "foo", + Name: "foo", + Type: "batch", + Priority: 10, + AllAtOnce: true, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &Job{ + Region: "foo", + ID: "foo", + Name: "foo", + Type: "batch", + Priority: 10, + AllAtOnce: true, + Meta: map[string]string{ + "foo": "bar", + }, + }, + Expected: &JobDiff{ + Type: DiffTypeNone, + ID: "foo", + }, + }, + { + // Primitive only that is has diffs + Old: &Job{ + Region: "foo", + ID: "foo", + Name: "foo", + Type: "batch", + Priority: 10, + AllAtOnce: true, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &Job{ + Region: "bar", + ID: "foo", + Name: "bar", + Type: "system", + Priority: 100, + AllAtOnce: false, + Meta: map[string]string{ + "foo": "baz", + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + ID: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "AllAtOnce", + Old: "true", + New: "false", + }, + { + Type: DiffTypeEdited, + Name: "Meta[foo]", + Old: "bar", + New: "baz", + }, + { + Type: DiffTypeEdited, + Name: "Name", + Old: "foo", + New: "bar", + }, + { + Type: DiffTypeEdited, + Name: "Priority", + Old: "10", + New: "100", + }, + { + Type: DiffTypeEdited, + Name: "Region", + Old: "foo", + New: "bar", + }, + { + Type: DiffTypeEdited, + Name: "Type", + Old: "batch", + New: "system", + }, + }, + }, + }, + { + // Primitive only deleted job + Old: &Job{ + Region: "foo", + ID: "foo", + Name: "foo", + Type: "batch", + Priority: 10, + AllAtOnce: true, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: nil, + Expected: &JobDiff{ + Type: DiffTypeDeleted, + ID: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "AllAtOnce", + Old: "true", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Meta[foo]", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Name", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Priority", + Old: "10", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Region", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Type", + Old: "batch", + New: "", + }, + }, + }, + }, + { + // Map diff + Old: &Job{ + Meta: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + New: &Job{ + Meta: map[string]string{ + "bar": "bar", + "baz": "baz", + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Meta[baz]", + Old: "", + New: "baz", + }, + { + Type: DiffTypeDeleted, + Name: "Meta[foo]", + Old: "foo", + New: "", + }, + }, + }, + }, + { + // Datacenter diff both added and removed + Old: &Job{ + Datacenters: []string{"foo", "bar"}, + }, + New: &Job{ + Datacenters: []string{"baz", "bar"}, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Datacenters", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Datacenters", + Old: "", + New: "baz", + }, + { + Type: DiffTypeDeleted, + Name: "Datacenters", + Old: "foo", + New: "", + }, + }, + }, + }, + }, + }, + { + // Datacenter diff just added + Old: &Job{ + Datacenters: []string{"foo", "bar"}, + }, + New: &Job{ + Datacenters: []string{"foo", "bar", "baz"}, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Datacenters", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Datacenters", + Old: "", + New: "baz", + }, + }, + }, + }, + }, + }, + { + // Datacenter diff just deleted + Old: &Job{ + Datacenters: []string{"foo", "bar"}, + }, + New: &Job{ + Datacenters: []string{"foo"}, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeDeleted, + Name: "Datacenters", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Datacenters", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + { + // Update strategy edited + Old: &Job{ + Update: UpdateStrategy{ + Stagger: 10 * time.Second, + MaxParallel: 5, + }, + }, + New: &Job{ + Update: UpdateStrategy{ + Stagger: 60 * time.Second, + MaxParallel: 10, + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Update", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "MaxParallel", + Old: "5", + New: "10", + }, + { + Type: DiffTypeEdited, + Name: "Stagger", + Old: "10000000000", + New: "60000000000", + }, + }, + }, + }, + }, + }, + { + // Periodic added + Old: &Job{}, + New: &Job{ + Periodic: &PeriodicConfig{ + Enabled: false, + Spec: "*/15 * * * * *", + SpecType: "foo", + ProhibitOverlap: false, + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Periodic", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Enabled", + Old: "", + New: "false", + }, + { + Type: DiffTypeAdded, + Name: "ProhibitOverlap", + Old: "", + New: "false", + }, + { + Type: DiffTypeAdded, + Name: "Spec", + Old: "", + New: "*/15 * * * * *", + }, + { + Type: DiffTypeAdded, + Name: "SpecType", + Old: "", + New: "foo", + }, + }, + }, + }, + }, + }, + { + // Periodic deleted + Old: &Job{ + Periodic: &PeriodicConfig{ + Enabled: false, + Spec: "*/15 * * * * *", + SpecType: "foo", + ProhibitOverlap: false, + }, + }, + New: &Job{}, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeDeleted, + Name: "Periodic", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Enabled", + Old: "false", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "ProhibitOverlap", + Old: "false", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Spec", + Old: "*/15 * * * * *", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "SpecType", + Old: "foo", + New: "", + }, + }, + }, + }, + }, + }, + { + // Periodic edited + Old: &Job{ + Periodic: &PeriodicConfig{ + Enabled: false, + Spec: "*/15 * * * * *", + SpecType: "foo", + ProhibitOverlap: false, + }, + }, + New: &Job{ + Periodic: &PeriodicConfig{ + Enabled: true, + Spec: "* * * * * *", + SpecType: "cron", + ProhibitOverlap: true, + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Periodic", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Enabled", + Old: "false", + New: "true", + }, + { + Type: DiffTypeEdited, + Name: "ProhibitOverlap", + Old: "false", + New: "true", + }, + { + Type: DiffTypeEdited, + Name: "Spec", + Old: "*/15 * * * * *", + New: "* * * * * *", + }, + { + Type: DiffTypeEdited, + Name: "SpecType", + Old: "foo", + New: "cron", + }, + }, + }, + }, + }, + }, + { + // Constraints edited + Old: &Job{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "bar", + RTarget: "bar", + Operand: "bar", + str: "bar", + }, + }, + }, + New: &Job{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "baz", + RTarget: "baz", + Operand: "baz", + str: "baz", + }, + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "LTarget", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "Operand", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "RTarget", + Old: "", + New: "baz", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "LTarget", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Operand", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "RTarget", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + { + // Task groups edited + Old: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "foo", + Count: 1, + }, + { + Name: "bar", + Count: 1, + }, + { + Name: "baz", + Count: 1, + }, + }, + }, + New: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "bar", + Count: 1, + }, + { + Name: "baz", + Count: 2, + }, + { + Name: "bam", + Count: 1, + }, + }, + }, + Expected: &JobDiff{ + Type: DiffTypeEdited, + TaskGroups: []*TaskGroupDiff{ + { + Type: DiffTypeAdded, + Name: "bam", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Count", + Old: "", + New: "1", + }, + }, + }, + { + Type: DiffTypeNone, + Name: "bar", + }, + { + Type: DiffTypeEdited, + Name: "baz", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Count", + Old: "1", + New: "2", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Count", + Old: "1", + New: "", + }, + }, + }, + }, + }, + }, + } + + for i, c := range cases { + actual, err := c.Old.Diff(c.New) + if c.Error && err == nil { + t.Fatalf("case %d: expected errored") + } else if err != nil { + if !c.Error { + t.Fatalf("case %d: errored %#v", i+1, err) + } else { + continue + } + } + + if !reflect.DeepEqual(actual, c.Expected) { + t.Fatalf("case %d: got:\n%#v\n want:\n%#v\n", + i+1, actual, c.Expected) + } + } +} + +func TestTaskGroupDiff(t *testing.T) { + cases := []struct { + Old, New *TaskGroup + Expected *TaskGroupDiff + Error bool + }{ + { + Old: nil, + New: nil, + Expected: &TaskGroupDiff{ + Type: DiffTypeNone, + }, + }, + { + // Primitive only that has different names + Old: &TaskGroup{ + Name: "foo", + Count: 10, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &TaskGroup{ + Name: "bar", + Count: 10, + Meta: map[string]string{ + "foo": "bar", + }, + }, + Error: true, + }, + { + // Primitive only that is the same + Old: &TaskGroup{ + Name: "foo", + Count: 10, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &TaskGroup{ + Name: "foo", + Count: 10, + Meta: map[string]string{ + "foo": "bar", + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeNone, + Name: "foo", + }, + }, + { + // Primitive only that has diffs + Old: &TaskGroup{ + Name: "foo", + Count: 10, + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &TaskGroup{ + Name: "foo", + Count: 100, + Meta: map[string]string{ + "foo": "baz", + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Name: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Count", + Old: "10", + New: "100", + }, + { + Type: DiffTypeEdited, + Name: "Meta[foo]", + Old: "bar", + New: "baz", + }, + }, + }, + }, + { + // Map diff + Old: &TaskGroup{ + Meta: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + New: &TaskGroup{ + Meta: map[string]string{ + "bar": "bar", + "baz": "baz", + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Meta[baz]", + Old: "", + New: "baz", + }, + { + Type: DiffTypeDeleted, + Name: "Meta[foo]", + Old: "foo", + New: "", + }, + }, + }, + }, + { + // Constraints edited + Old: &TaskGroup{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "bar", + RTarget: "bar", + Operand: "bar", + str: "bar", + }, + }, + }, + New: &TaskGroup{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "baz", + RTarget: "baz", + Operand: "baz", + str: "baz", + }, + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "LTarget", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "Operand", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "RTarget", + Old: "", + New: "baz", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "LTarget", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Operand", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "RTarget", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + { + // RestartPolicy added + Old: &TaskGroup{}, + New: &TaskGroup{ + RestartPolicy: &RestartPolicy{ + Attempts: 1, + Interval: 1 * time.Second, + Delay: 1 * time.Second, + Mode: "fail", + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "RestartPolicy", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Attempts", + Old: "", + New: "1", + }, + { + Type: DiffTypeAdded, + Name: "Delay", + Old: "", + New: "1000000000", + }, + { + Type: DiffTypeAdded, + Name: "Interval", + Old: "", + New: "1000000000", + }, + { + Type: DiffTypeAdded, + Name: "Mode", + Old: "", + New: "fail", + }, + }, + }, + }, + }, + }, + { + // RestartPolicy deleted + Old: &TaskGroup{ + RestartPolicy: &RestartPolicy{ + Attempts: 1, + Interval: 1 * time.Second, + Delay: 1 * time.Second, + Mode: "fail", + }, + }, + New: &TaskGroup{}, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeDeleted, + Name: "RestartPolicy", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Attempts", + Old: "1", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Delay", + Old: "1000000000", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Interval", + Old: "1000000000", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Mode", + Old: "fail", + New: "", + }, + }, + }, + }, + }, + }, + { + // RestartPolicy edited + Old: &TaskGroup{ + RestartPolicy: &RestartPolicy{ + Attempts: 1, + Interval: 1 * time.Second, + Delay: 1 * time.Second, + Mode: "fail", + }, + }, + New: &TaskGroup{ + RestartPolicy: &RestartPolicy{ + Attempts: 2, + Interval: 2 * time.Second, + Delay: 2 * time.Second, + Mode: "delay", + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "RestartPolicy", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Attempts", + Old: "1", + New: "2", + }, + { + Type: DiffTypeEdited, + Name: "Delay", + Old: "1000000000", + New: "2000000000", + }, + { + Type: DiffTypeEdited, + Name: "Interval", + Old: "1000000000", + New: "2000000000", + }, + { + Type: DiffTypeEdited, + Name: "Mode", + Old: "fail", + New: "delay", + }, + }, + }, + }, + }, + }, + { + // Tasks edited + Old: &TaskGroup{ + Tasks: []*Task{ + { + Name: "foo", + Driver: "docker", + }, + { + Name: "bar", + Driver: "docker", + }, + { + Name: "baz", + Driver: "docker", + }, + }, + }, + New: &TaskGroup{ + Tasks: []*Task{ + { + Name: "bar", + Driver: "docker", + }, + { + Name: "baz", + Driver: "exec", + }, + { + Name: "bam", + Driver: "docker", + }, + }, + }, + Expected: &TaskGroupDiff{ + Type: DiffTypeEdited, + Tasks: []*TaskDiff{ + { + Type: DiffTypeAdded, + Name: "bam", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Driver", + Old: "", + New: "docker", + }, + { + Type: DiffTypeAdded, + Name: "KillTimeout", + Old: "", + New: "0", + }, + }, + }, + { + Type: DiffTypeNone, + Name: "bar", + }, + { + Type: DiffTypeEdited, + Name: "baz", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Driver", + Old: "docker", + New: "exec", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Driver", + Old: "docker", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "KillTimeout", + Old: "0", + New: "", + }, + }, + }, + }, + }, + }, + } + + for i, c := range cases { + actual, err := c.Old.Diff(c.New) + if c.Error && err == nil { + t.Fatalf("case %d: expected errored") + } else if err != nil { + if !c.Error { + t.Fatalf("case %d: errored %#v", i+1, err) + } else { + continue + } + } + + if !reflect.DeepEqual(actual, c.Expected) { + t.Fatalf("case %d: got:\n%#v\n want:\n%#v\n", + i+1, actual, c.Expected) + } + } +} + +func TestTaskDiff(t *testing.T) { + cases := []struct { + Old, New *Task + Expected *TaskDiff + Error bool + }{ + { + Old: nil, + New: nil, + Expected: &TaskDiff{ + Type: DiffTypeNone, + }, + }, + { + // Primitive only that has different names + Old: &Task{ + Name: "foo", + Meta: map[string]string{ + "foo": "bar", + }, + }, + New: &Task{ + Name: "bar", + Meta: map[string]string{ + "foo": "bar", + }, + }, + Error: true, + }, + { + // Primitive only that is the same + Old: &Task{ + Name: "foo", + Driver: "exec", + User: "foo", + Env: map[string]string{ + "FOO": "bar", + }, + Meta: map[string]string{ + "foo": "bar", + }, + KillTimeout: 1 * time.Second, + }, + New: &Task{ + Name: "foo", + Driver: "exec", + User: "foo", + Env: map[string]string{ + "FOO": "bar", + }, + Meta: map[string]string{ + "foo": "bar", + }, + KillTimeout: 1 * time.Second, + }, + Expected: &TaskDiff{ + Type: DiffTypeNone, + Name: "foo", + }, + }, + { + // Primitive only that has diffs + Old: &Task{ + Name: "foo", + Driver: "exec", + User: "foo", + Env: map[string]string{ + "FOO": "bar", + }, + Meta: map[string]string{ + "foo": "bar", + }, + KillTimeout: 1 * time.Second, + }, + New: &Task{ + Name: "foo", + Driver: "docker", + User: "bar", + Env: map[string]string{ + "FOO": "baz", + }, + Meta: map[string]string{ + "foo": "baz", + }, + KillTimeout: 2 * time.Second, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Name: "foo", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "Driver", + Old: "exec", + New: "docker", + }, + { + Type: DiffTypeEdited, + Name: "Env[FOO]", + Old: "bar", + New: "baz", + }, + { + Type: DiffTypeEdited, + Name: "KillTimeout", + Old: "1000000000", + New: "2000000000", + }, + { + Type: DiffTypeEdited, + Name: "Meta[foo]", + Old: "bar", + New: "baz", + }, + { + Type: DiffTypeEdited, + Name: "User", + Old: "foo", + New: "bar", + }, + }, + }, + }, + { + // Map diff + Old: &Task{ + Meta: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + Env: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + New: &Task{ + Meta: map[string]string{ + "bar": "bar", + "baz": "baz", + }, + Env: map[string]string{ + "bar": "bar", + "baz": "baz", + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Env[baz]", + Old: "", + New: "baz", + }, + { + Type: DiffTypeDeleted, + Name: "Env[foo]", + Old: "foo", + New: "", + }, + { + Type: DiffTypeAdded, + Name: "Meta[baz]", + Old: "", + New: "baz", + }, + { + Type: DiffTypeDeleted, + Name: "Meta[foo]", + Old: "foo", + New: "", + }, + }, + }, + }, + { + // Constraints edited + Old: &Task{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "bar", + RTarget: "bar", + Operand: "bar", + str: "bar", + }, + }, + }, + New: &Task{ + Constraints: []*Constraint{ + { + LTarget: "foo", + RTarget: "foo", + Operand: "foo", + str: "foo", + }, + { + LTarget: "baz", + RTarget: "baz", + Operand: "baz", + str: "baz", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "LTarget", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "Operand", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "RTarget", + Old: "", + New: "baz", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Constraint", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "LTarget", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Operand", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "RTarget", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + { + // LogConfig added + Old: &Task{}, + New: &Task{ + LogConfig: &LogConfig{ + MaxFiles: 1, + MaxFileSizeMB: 10, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "LogConfig", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "MaxFileSizeMB", + Old: "", + New: "10", + }, + { + Type: DiffTypeAdded, + Name: "MaxFiles", + Old: "", + New: "1", + }, + }, + }, + }, + }, + }, + { + // LogConfig deleted + Old: &Task{ + LogConfig: &LogConfig{ + MaxFiles: 1, + MaxFileSizeMB: 10, + }, + }, + New: &Task{}, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeDeleted, + Name: "LogConfig", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "MaxFileSizeMB", + Old: "10", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "MaxFiles", + Old: "1", + New: "", + }, + }, + }, + }, + }, + }, + { + // RestartPolicy edited + Old: &Task{ + LogConfig: &LogConfig{ + MaxFiles: 1, + MaxFileSizeMB: 10, + }, + }, + New: &Task{ + LogConfig: &LogConfig{ + MaxFiles: 2, + MaxFileSizeMB: 20, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "LogConfig", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "MaxFileSizeMB", + Old: "10", + New: "20", + }, + { + Type: DiffTypeEdited, + Name: "MaxFiles", + Old: "1", + New: "2", + }, + }, + }, + }, + }, + }, + { + // Artifacts edited + Old: &Task{ + Artifacts: []*TaskArtifact{ + { + GetterSource: "foo", + GetterOptions: map[string]string{ + "foo": "bar", + }, + RelativeDest: "foo", + }, + { + GetterSource: "bar", + GetterOptions: map[string]string{ + "bar": "baz", + }, + RelativeDest: "bar", + }, + }, + }, + New: &Task{ + Artifacts: []*TaskArtifact{ + { + GetterSource: "foo", + GetterOptions: map[string]string{ + "foo": "bar", + }, + RelativeDest: "foo", + }, + { + GetterSource: "bam", + GetterOptions: map[string]string{ + "bam": "baz", + }, + RelativeDest: "bam", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Artifact", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "GetterOptions[bam]", + Old: "", + New: "baz", + }, + { + Type: DiffTypeAdded, + Name: "GetterSource", + Old: "", + New: "bam", + }, + { + Type: DiffTypeAdded, + Name: "RelativeDest", + Old: "", + New: "bam", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Artifact", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "GetterOptions[bar]", + Old: "baz", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "GetterSource", + Old: "bar", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "RelativeDest", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + { + // Resources edited (no networks) + Old: &Task{ + Resources: &Resources{ + CPU: 100, + MemoryMB: 100, + DiskMB: 100, + IOPS: 100, + }, + }, + New: &Task{ + Resources: &Resources{ + CPU: 200, + MemoryMB: 200, + DiskMB: 200, + IOPS: 200, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Resources", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "CPU", + Old: "100", + New: "200", + }, + { + Type: DiffTypeEdited, + Name: "DiskMB", + Old: "100", + New: "200", + }, + { + Type: DiffTypeEdited, + Name: "IOPS", + Old: "100", + New: "200", + }, + { + Type: DiffTypeEdited, + Name: "MemoryMB", + Old: "100", + New: "200", + }, + }, + }, + }, + }, + }, + { + // Network Resources edited + Old: &Task{ + Resources: &Resources{ + Networks: []*NetworkResource{ + { + Device: "foo", + CIDR: "foo", + IP: "foo", + MBits: 100, + ReservedPorts: []Port{ + { + Label: "foo", + Value: 80, + }, + }, + DynamicPorts: []Port{ + { + Label: "bar", + }, + }, + }, + }, + }, + }, + New: &Task{ + Resources: &Resources{ + Networks: []*NetworkResource{ + { + Device: "bar", + CIDR: "bar", + IP: "bar", + MBits: 200, + ReservedPorts: []Port{ + { + Label: "foo", + Value: 81, + }, + }, + DynamicPorts: []Port{ + { + Label: "baz", + }, + }, + }, + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Resources", + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Network", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "MBits", + Old: "", + New: "200", + }, + }, + Objects: []*ObjectDiff{ + { + Type: DiffTypeAdded, + Name: "Static Port", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Label", + Old: "", + New: "foo", + }, + { + Type: DiffTypeAdded, + Name: "Value", + Old: "", + New: "81", + }, + }, + }, + { + Type: DiffTypeAdded, + Name: "Dynamic Port", + Fields: []*FieldDiff{ + { + Type: DiffTypeAdded, + Name: "Label", + Old: "", + New: "baz", + }, + }, + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Network", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "MBits", + Old: "100", + New: "", + }, + }, + Objects: []*ObjectDiff{ + { + Type: DiffTypeDeleted, + Name: "Static Port", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Label", + Old: "foo", + New: "", + }, + { + Type: DiffTypeDeleted, + Name: "Value", + Old: "80", + New: "", + }, + }, + }, + { + Type: DiffTypeDeleted, + Name: "Dynamic Port", + Fields: []*FieldDiff{ + { + Type: DiffTypeDeleted, + Name: "Label", + Old: "bar", + New: "", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + // Config same + Old: &Task{ + Config: map[string]interface{}{ + "foo": 1, + "bar": "bar", + "bam": []string{"a", "b"}, + "baz": map[string]int{ + "a": 1, + "b": 2, + }, + "boom": &Port{ + Label: "boom_port", + }, + }, + }, + New: &Task{ + Config: map[string]interface{}{ + "foo": 1, + "bar": "bar", + "bam": []string{"a", "b"}, + "baz": map[string]int{ + "a": 1, + "b": 2, + }, + "boom": &Port{ + Label: "boom_port", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeNone, + }, + }, + { + // Config edited + Old: &Task{ + Config: map[string]interface{}{ + "foo": 1, + "bar": "baz", + "bam": []string{"a", "b"}, + "baz": map[string]int{ + "a": 1, + "b": 2, + }, + "boom": &Port{ + Label: "boom_port", + }, + }, + }, + New: &Task{ + Config: map[string]interface{}{ + "foo": 2, + "bar": "baz", + "bam": []string{"a", "c", "d"}, + "baz": map[string]int{ + "b": 3, + "c": 4, + }, + "boom": &Port{ + Label: "boom_port2", + }, + }, + }, + Expected: &TaskDiff{ + Type: DiffTypeEdited, + Objects: []*ObjectDiff{ + { + Type: DiffTypeEdited, + Name: "Config", + Fields: []*FieldDiff{ + { + Type: DiffTypeEdited, + Name: "bam[1]", + Old: "b", + New: "c", + }, + { + Type: DiffTypeAdded, + Name: "bam[2]", + Old: "", + New: "d", + }, + { + Type: DiffTypeDeleted, + Name: "baz[a]", + Old: "1", + New: "", + }, + { + Type: DiffTypeEdited, + Name: "baz[b]", + Old: "2", + New: "3", + }, + { + Type: DiffTypeAdded, + Name: "baz[c]", + Old: "", + New: "4", + }, + { + Type: DiffTypeEdited, + Name: "boom.Label", + Old: "boom_port", + New: "boom_port2", + }, + { + Type: DiffTypeEdited, + Name: "foo", + Old: "1", + New: "2", + }, + }, + }, + }, + }, + }, + } + + for i, c := range cases { + actual, err := c.Old.Diff(c.New) + if c.Error && err == nil { + t.Fatalf("case %d: expected errored") + } else if err != nil { + if !c.Error { + t.Fatalf("case %d: errored %#v", i+1, err) + } else { + continue + } + } + + if !reflect.DeepEqual(actual, c.Expected) { + t.Fatalf("case %d: got:\n%#v\n want:\n%#v\n", + i+1, actual, c.Expected) + } + } +}