From bd8dd3d499dba695d849b67d33e10d35230ea251 Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Tue, 21 Feb 2017 13:49:28 +0100 Subject: [PATCH] feat(plan): first implementation of plan --- plan/plan.go | 75 +++++++++++++++++++++++++++++++ plan/plan_test.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 plan/plan.go create mode 100644 plan/plan_test.go diff --git a/plan/plan.go b/plan/plan.go new file mode 100644 index 000000000..5c05e0d2f --- /dev/null +++ b/plan/plan.go @@ -0,0 +1,75 @@ +package plan + +// DNSRecord holds information about a DNS record. +type DNSRecord struct { + // The hostname of the DNS record + DNSName string + // The target the DNS record points to + Target string +} + +// Plan can convert a list of desired and current records to a series of create, +// update and delete actions. +type Plan struct { + // List of current records + Current []DNSRecord + // List of desired records + Desired []DNSRecord + + // The following lists hold actions to take in orer to move current state to + // desired state. + + // Records that need to be created + Create []DNSRecord + // Records that need to be updated (current data) + UpdateOld []DNSRecord + // Records that need to be updated (desired data) + UpdateNew []DNSRecord + // Records that need to be deleted + Delete []DNSRecord +} + +// Calculate computes the actions needed to move current state to desired state. +func (p *Plan) Calculate() *Plan { + plan := &Plan{} + + // Ensure all desired records exist. For each desired record make sure it's + // either created or updated. + for _, desired := range p.Desired { + // Get the matching current record if it exists. + current, exists := recordExists(desired, p.Current) + + // If there's no current record create desired record. + if !exists { + plan.Create = append(plan.Create, desired) + continue + } + + // If there already is a record update it if it changed. + if desired.Target != current.Target { + plan.UpdateOld = append(plan.UpdateOld, current) + plan.UpdateNew = append(plan.UpdateNew, desired) + } + } + + // Ensure all undesired records are removed. Each current record that cannot + // be found in the list of desired records is removed. + for _, current := range p.Current { + if _, exists := recordExists(current, p.Desired); !exists { + plan.Delete = append(plan.Delete, current) + } + } + + return plan +} + +// recordExists checks whether a record can be found in a list of records. +func recordExists(needle DNSRecord, haystack []DNSRecord) (DNSRecord, bool) { + for _, record := range haystack { + if record.DNSName == needle.DNSName { + return record, true + } + } + + return DNSRecord{}, false +} diff --git a/plan/plan_test.go b/plan/plan_test.go new file mode 100644 index 000000000..173d5d8d1 --- /dev/null +++ b/plan/plan_test.go @@ -0,0 +1,112 @@ +package plan + +import ( + "fmt" + "testing" +) + +// TestCalculate tests that a plan can calculate actions to move a list of +// current records to a list of desired records. +func TestCalculate(t *testing.T) { + // empty list of records + empty := []DNSRecord{} + // a simple entry + fooV1 := []DNSRecord{{DNSName: "foo", Target: "v1"}} + // the same entry but with different target + fooV2 := []DNSRecord{{DNSName: "foo", Target: "v2"}} + // another simple entry + bar := []DNSRecord{{DNSName: "bar", Target: "v1"}} + + for _, tc := range []struct { + current, desired, create, updateOld, updateNew, delete []DNSRecord + }{ + // Nothing exists and nothing desired doesn't change anything. + {empty, empty, empty, empty, empty, empty}, + // More desired than current creates the desired. + {empty, fooV1, fooV1, empty, empty, empty}, + // Desired equals current doesn't change anything. + {fooV1, fooV1, empty, empty, empty, empty}, + // Nothing is desired deletes the current. + {fooV1, empty, empty, empty, empty, fooV1}, + // Current and desired match but Target is different triggers an update. + {fooV1, fooV2, empty, fooV1, fooV2, empty}, + // Both exist but are different creates desired and deletes current. + {fooV1, bar, bar, empty, empty, fooV1}, + } { + // setup plan + plan := &Plan{ + Current: tc.current, + Desired: tc.desired, + } + + // calculate actions + plan = plan.Calculate() + + // validate actions + validateEntries(t, plan.Create, tc.create) + validateEntries(t, plan.UpdateOld, tc.updateOld) + validateEntries(t, plan.UpdateNew, tc.updateNew) + validateEntries(t, plan.Delete, tc.delete) + } +} + +// BenchmarkCalculate benchmarks the Calculate method. +func BenchmarkCalculate(b *testing.B) { + foo := DNSRecord{DNSName: "foo", Target: "v1"} + barV1 := DNSRecord{DNSName: "bar", Target: "v1"} + barV2 := DNSRecord{DNSName: "bar", Target: "v2"} + baz := DNSRecord{DNSName: "baz", Target: "v1"} + + plan := &Plan{ + Current: []DNSRecord{foo, barV1}, + Desired: []DNSRecord{barV2, baz}, + } + + for i := 0; i < b.N; i++ { + plan.Calculate() + } +} + +// ExamplePlan shows how plan can be used. +func ExamplePlan() { + foo := DNSRecord{DNSName: "foo.example.com", Target: "1.2.3.4"} + barV1 := DNSRecord{DNSName: "bar.example.com", Target: "8.8.8.8"} + barV2 := DNSRecord{DNSName: "bar.example.com", Target: "8.8.4.4"} + baz := DNSRecord{DNSName: "baz.example.com", Target: "6.6.6.6"} + + // Plan where + // * foo should be deleted + // * bar should be updated from v1 to v2 + // * baz should be created + plan := &Plan{ + Current: []DNSRecord{foo, barV1}, + Desired: []DNSRecord{barV2, baz}, + } + + // calculate actions + plan = plan.Calculate() + + // print actions + fmt.Println("Create:", plan.Create) + fmt.Println("UpdateOld:", plan.UpdateOld) + fmt.Println("UpdateNew:", plan.UpdateNew) + fmt.Println("Delete:", plan.Delete) + // Output: + // Create: [{baz.example.com 6.6.6.6}] + // UpdateOld: [{bar.example.com 8.8.8.8}] + // UpdateNew: [{bar.example.com 8.8.4.4}] + // Delete: [{foo.example.com 1.2.3.4}] +} + +// validateEntries validates that the list of entries matches expected. +func validateEntries(t *testing.T, entries, expected []DNSRecord) { + if len(entries) != len(expected) { + t.Fatalf("expected %q to match %q", entries, expected) + } + + for i := range entries { + if entries[i] != expected[i] { + t.Fatalf("expected %q to match %q", entries, expected) + } + } +}