diff --git a/.gitignore b/.gitignore index b76ed69ce..c3a2831ef 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ /output*/ /_output*/ /_output +/build # Emacs save files *~ @@ -40,3 +41,4 @@ cscope.* # coverage output cover.out +*.coverprofile diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a00cb75..efebc12fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Features: + - Ownership via TXT records + 1. Create TXT records to mark the records managed by External DNS + 2. Supported for AWS Route53 and Google CloudDNS + 3. Configurable TXT record DNS name format + ## v0.2.0 - 2017-04-07 Features: diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 7e149f7ee..3868b2b7a 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -31,16 +31,19 @@ type Endpoint struct { DNSName string // The target the DNS record points to Target string + // RecordType type of record, e.g. CNAME, A, TXT etc + RecordType string // Labels stores labels defined for the Endpoint Labels map[string]string } // NewEndpoint initialization method to be used to create an endpoint -func NewEndpoint(dnsName, target string) *Endpoint { +func NewEndpoint(dnsName, target, recordType string) *Endpoint { return &Endpoint{ - DNSName: strings.TrimSuffix(dnsName, "."), - Target: strings.TrimSuffix(target, "."), - Labels: map[string]string{}, + DNSName: strings.TrimSuffix(dnsName, "."), + Target: strings.TrimSuffix(target, "."), + RecordType: recordType, + Labels: map[string]string{}, } } diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index eb9fd6ef6..1642a3609 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -22,22 +22,22 @@ import ( ) func TestNewEndpoint(t *testing.T) { - e := NewEndpoint("example.org", "1.2.3.4") - if e.DNSName != "example.org" || e.Target != "1.2.3.4" { + e := NewEndpoint("example.org", "foo.com", "CNAME") + if e.DNSName != "example.org" || e.Target != "foo.com" || e.RecordType != "CNAME" { t.Error("endpoint is not initialized correctly") } if e.Labels == nil { t.Error("Labels is not initialized") } - w := NewEndpoint("example.org.", "load-balancer.com.") - if w.DNSName != "example.org" || w.Target != "load-balancer.com" { + w := NewEndpoint("example.org.", "load-balancer.com.", "") + if w.DNSName != "example.org" || w.Target != "load-balancer.com" || w.RecordType != "" { t.Error("endpoint is not initialized correctly") } } func TestMergeLabels(t *testing.T) { - e := NewEndpoint("abc.com", "1.2.3.4") + e := NewEndpoint("abc.com", "1.2.3.4", "A") e.Labels = map[string]string{ "foo": "bar", "baz": "qux", diff --git a/internal/testutils/endpoint.go b/internal/testutils/endpoint.go index e6b8b69ff..64941fad7 100644 --- a/internal/testutils/endpoint.go +++ b/internal/testutils/endpoint.go @@ -17,14 +17,35 @@ limitations under the License. package testutils import "github.com/kubernetes-incubator/external-dns/endpoint" +import "sort" /** test utility functions for endpoints verifications */ -// SameEndpoint returns true if two endpoint are same +type byAllFields []*endpoint.Endpoint + +func (b byAllFields) Len() int { return len(b) } +func (b byAllFields) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byAllFields) Less(i, j int) bool { + if b[i].DNSName < b[j].DNSName { + return true + } + if b[i].DNSName == b[j].DNSName { + if b[i].Target < b[j].Target { + return true + } + if b[i].Target == b[j].Target { + return b[i].RecordType <= b[j].RecordType + } + return false + } + return false +} + +// SameEndpoint returns true if two endpoints are same // considers example.org. and example.org DNSName/Target as different endpoints -// TODO:might need reconsideration regarding trailing dot func SameEndpoint(a, b *endpoint.Endpoint) bool { - return a.DNSName == b.DNSName && a.Target == b.Target && a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] + return a.DNSName == b.DNSName && a.Target == b.Target && a.RecordType == b.RecordType && + a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] } // SameEndpoints compares two slices of endpoints regardless of order @@ -37,34 +58,16 @@ func SameEndpoints(a, b []*endpoint.Endpoint) bool { return false } - calculator := map[string]map[string]uint8{} //testutils is not meant for large data sets - for _, recordA := range a { - if _, exists := calculator[recordA.DNSName]; !exists { - calculator[recordA.DNSName] = map[string]uint8{} - } - if _, exists := calculator[recordA.DNSName][recordA.Target]; !exists { - calculator[recordA.DNSName][recordA.Target] = 0 - } - calculator[recordA.DNSName][recordA.Target]++ - } - for _, recordB := range b { - if _, exists := calculator[recordB.DNSName]; !exists { + sa := a[:] + sb := b[:] + sort.Sort(byAllFields(sa)) + sort.Sort(byAllFields(sb)) + + for i := range sa { + if !SameEndpoint(sa[i], sb[i]) { return false } - if _, exists := calculator[recordB.DNSName][recordB.Target]; !exists { - return false - } - calculator[recordB.DNSName][recordB.Target]-- } - - for _, byDNSName := range calculator { - for _, byCounter := range byDNSName { - if byCounter != 0 { - return false - } - } - } - return true } diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go new file mode 100644 index 000000000..823bfc9f1 --- /dev/null +++ b/internal/testutils/endpoint_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testutils + +import ( + "fmt" + "sort" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +func ExampleSameEndpoints() { + eps := []*endpoint.Endpoint{ + { + DNSName: "example.org", + Target: "load-balancer.org", + }, + { + DNSName: "example.org", + Target: "load-balancer.org", + RecordType: "TXT", + }, + { + DNSName: "abc.com", + Target: "something", + RecordType: "TXT", + }, + { + DNSName: "abc.com", + Target: "1.2.3.4", + RecordType: "A", + }, + { + DNSName: "bbc.com", + Target: "foo.com", + RecordType: "CNAME", + }, + } + sort.Sort(byAllFields(eps)) + for _, ep := range eps { + fmt.Println(ep) + } + // Output: + // &{abc.com 1.2.3.4 A map[]} + // &{abc.com something TXT map[]} + // &{bbc.com foo.com CNAME map[]} + // &{example.org load-balancer.org map[]} + // &{example.org load-balancer.org TXT map[]} +} diff --git a/main.go b/main.go index d08134dfe..66042941f 100644 --- a/main.go +++ b/main.go @@ -94,7 +94,16 @@ func main() { log.Fatal(err) } - r, err := registry.NewNoopRegistry(p) + var r registry.Registry + switch cfg.Registry { + case "noop": + r, err = registry.NewNoopRegistry(p) + case "txt": + r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.RecordOwnerID) + default: + log.Fatalf("unknown registry: %s", cfg.Registry) + } + if err != nil { log.Fatal(err) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index b61a7a9e4..febc023d7 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -46,6 +46,9 @@ type Config struct { Debug bool LogFormat string Version bool + Registry string + RecordOwnerID string + TXTPrefix string } // NewConfig returns new Config object @@ -65,11 +68,15 @@ func (cfg *Config) ParseFlags(args []string) error { flags.StringVar(&cfg.GoogleProject, "google-project", "", "gcloud project to target") flags.BoolVar(&cfg.Compatibility, "compatibility", false, "enable to process annotation semantics from legacy implementations") flags.StringVar(&cfg.MetricsAddress, "metrics-address", defaultMetricsAddress, "address to expose metrics on") - flags.StringVar(&cfg.LogFormat, "log-format", defaultLogFormat, "log format output. options: [\"text\", \"json\"]") + flags.StringVar(&cfg.LogFormat, "log-format", defaultLogFormat, "log format output: ") flags.DurationVar(&cfg.Interval, "interval", time.Minute, "interval between synchronizations") flags.BoolVar(&cfg.Once, "once", false, "run once and exit") flags.BoolVar(&cfg.DryRun, "dry-run", true, "dry-run mode") flags.BoolVar(&cfg.Debug, "debug", false, "debug mode") flags.BoolVar(&cfg.Version, "version", false, "display the version") + flags.StringVar(&cfg.Registry, "registry", "noop", "type of registry for ownership: ") + flags.StringVar(&cfg.RecordOwnerID, "record-owner-id", "", "id of the current external dns for labeling owned records") + flags.StringVar(&cfg.TXTPrefix, "txt-prefix", "", `prefix of the associated TXT records DNS name; if --txt-prefix="abc-", + corresponding txt record for CNAME [example.org] will have DNSName [abc-example.org]. Required for CNAME ownership support`) return flags.Parse(args) } diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 47b04cac5..316f922ba 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -48,6 +48,9 @@ func TestParseFlags(t *testing.T) { Debug: false, LogFormat: defaultLogFormat, Version: false, + Registry: "noop", + RecordOwnerID: "", + TXTPrefix: "", }, }, { @@ -69,6 +72,9 @@ func TestParseFlags(t *testing.T) { Debug: false, LogFormat: defaultLogFormat, Version: false, + Registry: "noop", + RecordOwnerID: "", + TXTPrefix: "", }, }, { @@ -90,6 +96,9 @@ func TestParseFlags(t *testing.T) { Debug: false, LogFormat: defaultLogFormat, Version: false, + Registry: "noop", + RecordOwnerID: "", + TXTPrefix: "", }, }, { @@ -116,6 +125,9 @@ func TestParseFlags(t *testing.T) { Debug: false, LogFormat: "json", Version: false, + Registry: "noop", + RecordOwnerID: "", + TXTPrefix: "", }, }, { @@ -134,6 +146,9 @@ func TestParseFlags(t *testing.T) { "--once", "--dry-run=false", "--debug", + "--registry=txt", + "--record-owner-id=owner-1", + "--txt-prefix=associated-txt-record", "--version"}}, expected: &Config{ InCluster: true, @@ -151,6 +166,9 @@ func TestParseFlags(t *testing.T) { Debug: true, LogFormat: "yaml", Version: true, + Registry: "txt", + RecordOwnerID: "owner-1", + TXTPrefix: "associated-txt-record", }, }, { diff --git a/plan/plan_test.go b/plan/plan_test.go index 8eadbba07..f83658f8c 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -30,14 +30,14 @@ func TestCalculate(t *testing.T) { // empty list of records empty := []*endpoint.Endpoint{} // a simple entry - fooV1 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1")} + fooV1 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", "CNAME")} // the same entry but with different target - fooV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2")} + fooV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", "CNAME")} // another simple entry - bar := []*endpoint.Endpoint{endpoint.NewEndpoint("bar", "v1")} + bar := []*endpoint.Endpoint{endpoint.NewEndpoint("bar", "v1", "CNAME")} // test case with labels - noLabels := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2")} + noLabels := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", "CNAME")} labeledV2 := []*endpoint.Endpoint{newEndpointWithOwner("foo", "v2", "123")} labeledV1 := []*endpoint.Endpoint{newEndpointWithOwner("foo", "v1", "123")} @@ -77,10 +77,10 @@ func TestCalculate(t *testing.T) { // BenchmarkCalculate benchmarks the Calculate method. func BenchmarkCalculate(b *testing.B) { - foo := endpoint.NewEndpoint("foo", "v1") - barV1 := endpoint.NewEndpoint("bar", "v1") - barV2 := endpoint.NewEndpoint("bar", "v2") - baz := endpoint.NewEndpoint("baz", "v1") + foo := endpoint.NewEndpoint("foo", "v1", "") + barV1 := endpoint.NewEndpoint("bar", "v1", "") + barV2 := endpoint.NewEndpoint("bar", "v2", "") + baz := endpoint.NewEndpoint("baz", "v1", "") plan := &Plan{ Current: []*endpoint.Endpoint{foo, barV1}, @@ -94,10 +94,10 @@ func BenchmarkCalculate(b *testing.B) { // ExamplePlan shows how plan can be used. func ExamplePlan() { - foo := endpoint.NewEndpoint("foo.example.com", "1.2.3.4") - barV1 := endpoint.NewEndpoint("bar.example.com", "8.8.8.8") - barV2 := endpoint.NewEndpoint("bar.example.com", "8.8.4.4") - baz := endpoint.NewEndpoint("baz.example.com", "6.6.6.6") + foo := endpoint.NewEndpoint("foo.example.com", "1.2.3.4", "") + barV1 := endpoint.NewEndpoint("bar.example.com", "8.8.8.8", "") + barV2 := endpoint.NewEndpoint("bar.example.com", "8.8.4.4", "") + baz := endpoint.NewEndpoint("baz.example.com", "6.6.6.6", "") // Plan where // * foo should be deleted @@ -128,15 +128,14 @@ func ExamplePlan() { for _, ep := range plan.Changes.Delete { fmt.Println(ep) } - // Output: // Create: - // &{baz.example.com 6.6.6.6 map[]} + // &{baz.example.com 6.6.6.6 map[] } // UpdateOld: - // &{bar.example.com 8.8.8.8 map[]} + // &{bar.example.com 8.8.8.8 map[] } // UpdateNew: - // &{bar.example.com 8.8.4.4 map[]} + // &{bar.example.com 8.8.4.4 map[] } // Delete: - // &{foo.example.com 1.2.3.4 map[]} + // &{foo.example.com 1.2.3.4 map[] } } // validateEntries validates that the list of entries matches expected. @@ -153,7 +152,7 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) { } func newEndpointWithOwner(dnsName, target, ownerID string) *endpoint.Endpoint { - e := endpoint.NewEndpoint(dnsName, target) + e := endpoint.NewEndpoint(dnsName, target, "CNAME") e.Labels[endpoint.OwnerLabelKey] = ownerID return e } diff --git a/provider/aws.go b/provider/aws.go index d898ae29b..fbd1dda28 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -76,14 +76,14 @@ func (p *AWSProvider) Records(zone string) ([]*endpoint.Endpoint, error) { // TODO(linki, ownership): Remove once ownership system is in place. // See: https://github.com/kubernetes-incubator/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370 switch aws.StringValue(r.Type) { - case route53.RRTypeA, route53.RRTypeCname: + case route53.RRTypeA, route53.RRTypeCname, route53.RRTypeTxt: break default: continue } for _, rr := range r.ResourceRecords { - endpoints = append(endpoints, endpoint.NewEndpoint(aws.StringValue(r.Name), aws.StringValue(rr.Value))) + endpoints = append(endpoints, endpoint.NewEndpoint(aws.StringValue(r.Name), aws.StringValue(rr.Value), aws.StringValue(r.Type))) } } @@ -181,7 +181,7 @@ func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change { }, }, TTL: aws.Int64(300), - Type: aws.String(suitableType(endpoint.Target)), + Type: aws.String(suitableType(endpoint)), }, } diff --git a/provider/aws_test.go b/provider/aws_test.go index d399c7470..4055fa595 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -133,16 +133,15 @@ func (r *Route53APIStub) CreateHostedZone(input *route53.CreateHostedZoneInput) func TestAWSRecords(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "list-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) records, err := provider.Records(testZone) if err != nil { t.Fatal(err) } - validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "list-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } @@ -150,7 +149,7 @@ func TestAWSCreateRecords(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } if err := provider.CreateRecords(testZone, records); err != nil { @@ -163,20 +162,20 @@ func TestAWSCreateRecords(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } func TestAWSUpdateRecords(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), } if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil { @@ -189,17 +188,17 @@ func TestAWSUpdateRecords(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), }) } func TestAWSDeleteRecords(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } if err := provider.DeleteRecords(testZone, currentRecords); err != nil { @@ -216,23 +215,23 @@ func TestAWSDeleteRecords(t *testing.T) { func TestAWSApplyChanges(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) createRecords := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), } deleteRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } changes := &plan.Changes{ @@ -252,8 +251,8 @@ func TestAWSApplyChanges(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), }) } @@ -269,7 +268,7 @@ func TestAWSCreateRecordsDryRun(t *testing.T) { provider := newAWSProvider(t, true, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } if err := provider.CreateRecords(testZone, records); err != nil { @@ -286,14 +285,14 @@ func TestAWSCreateRecordsDryRun(t *testing.T) { func TestAWSUpdateRecordsDryRun(t *testing.T) { provider := newAWSProvider(t, true, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), } if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil { @@ -306,17 +305,17 @@ func TestAWSUpdateRecordsDryRun(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } func TestAWSDeleteRecordsDryRun(t *testing.T) { provider := newAWSProvider(t, true, []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } if err := provider.DeleteRecords(testZone, currentRecords); err != nil { @@ -329,29 +328,29 @@ func TestAWSDeleteRecordsDryRun(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } func TestAWSApplyChangesDryRun(t *testing.T) { provider := newAWSProvider(t, true, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) createRecords := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "1.2.3.4"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), } deleteRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), } changes := &plan.Changes{ @@ -371,8 +370,8 @@ func TestAWSApplyChangesDryRun(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } @@ -380,7 +379,7 @@ func TestAWSCreateRecordsCNAME(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", ""), } if err := provider.CreateRecords(testZone, records); err != nil { @@ -393,20 +392,20 @@ func TestAWSCreateRecordsCNAME(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), }) } func TestAWSUpdateRecordsCNAME(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", ""), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "bar.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.elb.amazonaws.com", ""), } if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil { @@ -419,17 +418,17 @@ func TestAWSUpdateRecordsCNAME(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "bar.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.elb.amazonaws.com", "CNAME"), }) } func TestAWSDeleteRecordsCNAME(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "baz.elb.amazonaws.com"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "baz.elb.amazonaws.com", "CNAME"), }) currentRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "baz.elb.amazonaws.com"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "baz.elb.amazonaws.com", ""), } if err := provider.DeleteRecords(testZone, currentRecords); err != nil { @@ -446,23 +445,23 @@ func TestAWSDeleteRecordsCNAME(t *testing.T) { func TestAWSApplyChangesCNAME(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "qux.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "qux.elb.amazonaws.com", "CNAME"), }) createRecords := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", ""), } currentRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "bar.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.elb.amazonaws.com", ""), } updatedRecords := []*endpoint.Endpoint{ - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "baz.elb.amazonaws.com"}, + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "baz.elb.amazonaws.com", ""), } deleteRecords := []*endpoint.Endpoint{ - {DNSName: "delete-test.ext-dns-test.teapot.zalan.do", Target: "qux.elb.amazonaws.com"}, + endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "qux.elb.amazonaws.com", ""), } changes := &plan.Changes{ @@ -482,14 +481,14 @@ func TestAWSApplyChangesCNAME(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.elb.amazonaws.com"}, - {DNSName: "update-test.ext-dns-test.teapot.zalan.do", Target: "baz.elb.amazonaws.com"}, + endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), + endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "baz.elb.amazonaws.com", "CNAME"), }) } func TestAWSSanitizeZone(t *testing.T) { provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - {DNSName: "list-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) records, err := provider.Records(testZone) @@ -498,7 +497,7 @@ func TestAWSSanitizeZone(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "list-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) records, err = provider.Records("/hostedzone/" + testZone) @@ -507,13 +506,13 @@ func TestAWSSanitizeZone(t *testing.T) { } validateEndpoints(t, records, []*endpoint.Endpoint{ - {DNSName: "list-test.ext-dns-test.teapot.zalan.do", Target: "8.8.8.8"}, + endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), }) } func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { if !testutils.SameEndpoints(endpoints, expected) { - t.Errorf("expected %v, got %v", expected, endpoints) + t.Fatalf("expected %v, got %v", expected, endpoints) } } @@ -552,7 +551,6 @@ func setupRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.End if err != nil { t.Fatal(err) } - validateEndpoints(t, records, endpoints) } diff --git a/provider/google.go b/provider/google.go index 78100e806..b7a6afb27 100644 --- a/provider/google.go +++ b/provider/google.go @@ -185,7 +185,7 @@ func (p *googleProvider) Records(zone string) (endpoints []*endpoint.Endpoint, _ // TODO(linki, ownership): Remove once ownership system is in place. // See: https://github.com/kubernetes-incubator/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370 switch r.Type { - case "A", "CNAME": + case "A", "CNAME", "TXT": break default: continue @@ -193,7 +193,7 @@ func (p *googleProvider) Records(zone string) (endpoints []*endpoint.Endpoint, _ for _, rr := range r.Rrdatas { // each page is processed sequentially, no need for a mutex here. - endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, rr)) + endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, rr, r.Type)) } } @@ -294,7 +294,7 @@ func newRecord(endpoint *endpoint.Endpoint) *dns.ResourceRecordSet { Name: endpoint.DNSName, Rrdatas: []string{endpoint.Target}, Ttl: 300, - Type: suitableType(endpoint.Target), + Type: suitableType(endpoint), } } diff --git a/provider/inmemory.go b/provider/inmemory.go index c6d2ab9ef..08c218613 100644 --- a/provider/inmemory.go +++ b/provider/inmemory.go @@ -24,8 +24,6 @@ import ( ) var ( - defaultType = "A" - // ErrZoneAlreadyExists error returned when zone cannot be created when it already exists ErrZoneAlreadyExists = errors.New("specified zone already exists") // ErrZoneNotFound error returned when specified zone does not exists @@ -35,7 +33,7 @@ var ( // ErrRecordNotFound when update/delete request is sent but record not found ErrRecordNotFound = errors.New("record not found") // ErrInvalidBatchRequest when record is repeated in create/update/delete - ErrInvalidBatchRequest = errors.New("record should only be specified in one list") + ErrInvalidBatchRequest = errors.New("invalid batch request") ) type zone map[string][]*InMemoryRecord @@ -100,18 +98,25 @@ func (im *InMemoryProvider) ApplyChanges(zone string, changes *plan.Changes) err for _, newEndpoint := range changes.Create { im.zones[zone][newEndpoint.DNSName] = append(im.zones[zone][newEndpoint.DNSName], &InMemoryRecord{ - Type: defaultType, + Type: suitableType(newEndpoint), Endpoint: newEndpoint, }) } for _, updateEndpoint := range changes.UpdateNew { - recordToUpdate := im.findByType(defaultType, im.zones[zone][updateEndpoint.DNSName]) - recordToUpdate.Target = updateEndpoint.Target + for _, curEndpoint := range changes.UpdateOld { + if curEndpoint.DNSName == updateEndpoint.DNSName && curEndpoint.RecordType == updateEndpoint.RecordType { + for _, recordToUpdate := range im.zones[zone][updateEndpoint.DNSName] { + if recordToUpdate.Target == curEndpoint.Target { + recordToUpdate.Target = updateEndpoint.Target + } + } + } + } } for _, deleteEndpoint := range changes.Delete { newRecordSet := make([]*InMemoryRecord, 0) for _, record := range im.zones[zone][deleteEndpoint.DNSName] { - if record.Type != defaultType { + if record.Type != suitableType(deleteEndpoint) { newRecordSet = append(newRecordSet, record) } } @@ -126,38 +131,50 @@ func (im *InMemoryProvider) validateChangeBatch(zone string, changes *plan.Chang if !ok { return ErrZoneNotFound } - mesh := map[string]bool{} + mesh := map[string]map[string]bool{} for _, newEndpoint := range changes.Create { - if im.findByType(defaultType, existing[newEndpoint.DNSName]) != nil { + if im.findByType(suitableType(newEndpoint), existing[newEndpoint.DNSName]) != nil { return ErrRecordAlreadyExists } if _, exists := mesh[newEndpoint.DNSName]; exists { - return ErrInvalidBatchRequest + if mesh[newEndpoint.DNSName][suitableType(newEndpoint)] { + return ErrInvalidBatchRequest + } + mesh[newEndpoint.DNSName][suitableType(newEndpoint)] = true + continue } - mesh[newEndpoint.DNSName] = true + mesh[newEndpoint.DNSName] = map[string]bool{suitableType(newEndpoint): true} } for _, updateEndpoint := range changes.UpdateNew { - if im.findByType(defaultType, existing[updateEndpoint.DNSName]) == nil { + if im.findByType(suitableType(updateEndpoint), existing[updateEndpoint.DNSName]) == nil { return ErrRecordNotFound } if _, exists := mesh[updateEndpoint.DNSName]; exists { - return ErrInvalidBatchRequest + if mesh[updateEndpoint.DNSName][suitableType(updateEndpoint)] { + return ErrInvalidBatchRequest + } + mesh[updateEndpoint.DNSName][suitableType(updateEndpoint)] = true + continue } - mesh[updateEndpoint.DNSName] = true + mesh[updateEndpoint.DNSName] = map[string]bool{suitableType(updateEndpoint): true} } for _, updateOldEndpoint := range changes.UpdateOld { - if rec := im.findByType(defaultType, existing[updateOldEndpoint.DNSName]); rec == nil || rec.Target != updateOldEndpoint.Target { + if rec := im.findByType(suitableType(updateOldEndpoint), existing[updateOldEndpoint.DNSName]); rec == nil || rec.Target != updateOldEndpoint.Target { return ErrRecordNotFound } } for _, deleteEndpoint := range changes.Delete { - if rec := im.findByType(defaultType, existing[deleteEndpoint.DNSName]); rec == nil || rec.Target != deleteEndpoint.Target { + if rec := im.findByType(suitableType(deleteEndpoint), existing[deleteEndpoint.DNSName]); rec == nil || rec.Target != deleteEndpoint.Target { return ErrRecordNotFound } if _, exists := mesh[deleteEndpoint.DNSName]; exists { - return ErrInvalidBatchRequest + if mesh[deleteEndpoint.DNSName][suitableType(deleteEndpoint)] { + return ErrInvalidBatchRequest + } + mesh[deleteEndpoint.DNSName][suitableType(deleteEndpoint)] = true + continue } - mesh[deleteEndpoint.DNSName] = true + mesh[deleteEndpoint.DNSName] = map[string]bool{suitableType(deleteEndpoint): true} } return nil } @@ -176,6 +193,7 @@ func (im *InMemoryProvider) endpoints(zone string) []*endpoint.Endpoint { if zoneRecords, exists := im.zones[zone]; exists { for _, recordsPerName := range zoneRecords { for _, record := range recordsPerName { + record.Endpoint.RecordType = record.Type endpoints = append(endpoints, record.Endpoint) } } diff --git a/provider/inmemory_test.go b/provider/inmemory_test.go index 721e6c40d..11a89934a 100644 --- a/provider/inmemory_test.go +++ b/provider/inmemory_test.go @@ -25,7 +25,9 @@ import ( "github.com/kubernetes-incubator/external-dns/plan" ) -var _ Provider = &InMemoryProvider{} +var ( + _ Provider = &InMemoryProvider{} +) func TestInMemoryProvider(t *testing.T) { t.Run("Records", testInMemoryRecords) @@ -201,7 +203,7 @@ func testInMemoryEndpoints(t *testing.T) { DNSName: "example.org", Target: "8.8.8.8", }, - Type: defaultType, + Type: "A", }, { Endpoint: &endpoint.Endpoint{ @@ -214,7 +216,7 @@ func testInMemoryEndpoints(t *testing.T) { { Endpoint: &endpoint.Endpoint{ DNSName: "foo.org", - Target: "4.4.4.4", + Target: "bar.org", }, Type: "CNAME", }, @@ -227,22 +229,25 @@ func testInMemoryEndpoints(t *testing.T) { DNSName: "example.com", Target: "4.4.4.4", }, - Type: "CNAME", + Type: "A", }, }, }, }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Target: "8.8.8.8", + DNSName: "example.org", + Target: "8.8.8.8", + RecordType: "A", }, { - DNSName: "example.org", + DNSName: "example.org", + RecordType: "TXT", }, { - DNSName: "foo.org", - Target: "4.4.4.4", + DNSName: "foo.org", + Target: "bar.org", + RecordType: "CNAME", }, }, }, @@ -289,7 +294,7 @@ func testInMemoryRecords(t *testing.T) { DNSName: "example.org", Target: "8.8.8.8", }, - Type: defaultType, + Type: "A", }, { Endpoint: &endpoint.Endpoint{ @@ -352,7 +357,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) { DNSName: "example.org", Target: "8.8.8.8", }, - Type: defaultType, + Type: "A", }, { Endpoint: &endpoint.Endpoint{ @@ -365,7 +370,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) { { Endpoint: &endpoint.Endpoint{ DNSName: "foo.org", - Target: "4.4.4.4", + Target: "bar.org", }, Type: "CNAME", }, @@ -376,7 +381,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) { DNSName: "foo.bar.org", Target: "5.5.5.5", }, - Type: defaultType, + Type: "A", }, }, }, @@ -385,7 +390,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) { { Endpoint: &endpoint.Endpoint{ DNSName: "example.com", - Target: "4.4.4.4", + Target: "another-example.com", }, Type: "CNAME", }, @@ -719,7 +724,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "example.org", Target: "8.8.8.8", }, - Type: defaultType, + Type: "A", }, { Endpoint: &endpoint.Endpoint{ @@ -807,7 +812,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.org", Target: "4.8.8.4", }, - Type: defaultType, + Type: "A", }, }, "foo.bar.new.org": []*InMemoryRecord{ @@ -816,7 +821,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.new.org", Target: "4.8.8.9", }, - Type: defaultType, + Type: "A", }, }, }, @@ -843,7 +848,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "example.org", Target: "8.8.8.8", }, - Type: defaultType, + Type: "A", }, { Endpoint: &endpoint.Endpoint{ @@ -867,7 +872,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.org", Target: "5.5.5.5", }, - Type: defaultType, + Type: "A", }, }, }, diff --git a/provider/provider.go b/provider/provider.go index d41d8844f..a52f61ac1 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -31,10 +31,12 @@ type Provider interface { // suitableType returns the DNS resource record type suitable for the target. // In this case type A for IPs and type CNAME for everything else. -func suitableType(target string) string { - if net.ParseIP(target) == nil { +func suitableType(ep *endpoint.Endpoint) string { + if ep.RecordType != "" { + return ep.RecordType + } + if net.ParseIP(ep.Target) == nil { return "CNAME" } - return "A" } diff --git a/registry/noop_test.go b/registry/noop_test.go index 70bba1678..2dacff9eb 100644 --- a/registry/noop_test.go +++ b/registry/noop_test.go @@ -80,17 +80,19 @@ func testNoopApplyChanges(t *testing.T) { providerRecords := []*endpoint.Endpoint{ { DNSName: "example.org", - Target: "8.8.8.8", + Target: "old-lb.com", }, } expectedUpdate := []*endpoint.Endpoint{ { - DNSName: "example.org", - Target: "new-example-lb.com", + DNSName: "example.org", + Target: "new-example-lb.com", + RecordType: "CNAME", }, { - DNSName: "new-record.org", - Target: "new-lb.org", + DNSName: "new-record.org", + Target: "new-lb.org", + RecordType: "CNAME", }, } @@ -136,7 +138,7 @@ func testNoopApplyChanges(t *testing.T) { UpdateOld: []*endpoint.Endpoint{ { DNSName: "example.org", - Target: "8.8.8.8", + Target: "old-lb.com", }, }, }) diff --git a/registry/registry.go b/registry/registry.go index b30e52ed1..a24e76807 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -29,3 +29,14 @@ type Registry interface { Records(zone string) ([]*endpoint.Endpoint, error) ApplyChanges(zone string, changes *plan.Changes) error } + +//TODO(ideahitme): consider moving this to Plan +func filterOwnedRecords(ownerID string, eps []*endpoint.Endpoint) []*endpoint.Endpoint { + filtered := []*endpoint.Endpoint{} + for _, ep := range eps { + if ep.Labels[endpoint.OwnerLabelKey] == ownerID { + filtered = append(filtered, ep) + } + } + return filtered +} diff --git a/registry/txt.go b/registry/txt.go new file mode 100644 index 000000000..3cf30aa5d --- /dev/null +++ b/registry/txt.go @@ -0,0 +1,160 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "errors" + + "fmt" + "regexp" + "strings" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + "github.com/kubernetes-incubator/external-dns/provider" +) + +var ( + txtLabelRegex = regexp.MustCompile("^\"heritage=external-dns;external-dns/record-owner-id=(.+)\"") + txtLabelFormat = "\"heritage=external-dns;external-dns/record-owner-id=%s\"" +) + +// TXTRegistry implements registry interface with ownership implemented via associated TXT records +type TXTRegistry struct { + provider provider.Provider + ownerID string //refers to the owner id of the current instance + mapper nameMapper +} + +// NewTXTRegistry returns new TXTRegistry object +func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string) (*TXTRegistry, error) { + if ownerID == "" { + return nil, errors.New("owner id cannot be empty") + } + + mapper := newPrefixNameMapper(txtPrefix) + + return &TXTRegistry{ + provider: provider, + ownerID: ownerID, + mapper: mapper, + }, nil +} + +// Records returns the current records from the registry excluding TXT Records +// If TXT records was created previously to indicate ownership its corresponding value +// will be added to the endpoints Labels map +func (im *TXTRegistry) Records(zone string) ([]*endpoint.Endpoint, error) { + records, err := im.provider.Records(zone) + if err != nil { + return nil, err + } + + endpoints := make([]*endpoint.Endpoint, 0) + + ownerMap := map[string]string{} + + for _, record := range records { + if record.RecordType != "TXT" { + endpoints = append(endpoints, record) + continue + } + ownerID := im.extractOwnerID(record.Target) + if ownerID == "" { + //case when value of txt record cannot be identified + //record will not be removed as it will have empty owner + endpoints = append(endpoints, record) + continue + } + endpointDNSName := im.mapper.toEndpointName(record.DNSName) + ownerMap[endpointDNSName] = ownerID + } + + for _, ep := range endpoints { + ep.Labels[endpoint.OwnerLabelKey] = ownerMap[ep.DNSName] + } + + return endpoints, err +} + +// ApplyChanges updates dns provider with the changes +// for each created/deleted record it will also take into account TXT records for creation/deletion +func (im *TXTRegistry) ApplyChanges(zone string, changes *plan.Changes) error { + filteredChanges := &plan.Changes{ + Create: changes.Create, + UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew), + UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld), + Delete: filterOwnedRecords(im.ownerID, changes.Delete), + } + + for _, r := range filteredChanges.Create { + txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), im.getTXTLabel(), "TXT") + filteredChanges.Create = append(filteredChanges.Create, txt) + } + for _, r := range filteredChanges.Delete { + txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), im.getTXTLabel(), "TXT") + filteredChanges.Delete = append(filteredChanges.Delete, txt) + } + + return im.provider.ApplyChanges(zone, filteredChanges) +} + +/** + TXT registry specific private methods +*/ + +func (im *TXTRegistry) getTXTLabel() string { + return fmt.Sprintf(txtLabelFormat, im.ownerID) +} + +func (im *TXTRegistry) extractOwnerID(txtLabel string) string { + if matches := txtLabelRegex.FindStringSubmatch(txtLabel); len(matches) == 2 { + return matches[1] + } + return "" +} + +/** + nameMapper defines interface which maps the dns name defined for the source + to the dns name which TXT record will be created with +*/ + +type nameMapper interface { + toEndpointName(string) string + toTXTName(string) string +} + +type prefixNameMapper struct { + prefix string +} + +var _ nameMapper = prefixNameMapper{} + +func newPrefixNameMapper(prefix string) prefixNameMapper { + return prefixNameMapper{prefix: prefix} +} + +func (pr prefixNameMapper) toEndpointName(txtDNSName string) string { + if strings.HasPrefix(txtDNSName, pr.prefix) { + return strings.TrimPrefix(txtDNSName, pr.prefix) + } + return "" +} + +func (pr prefixNameMapper) toTXTName(endpointDNSName string) string { + return pr.prefix + endpointDNSName +} diff --git a/registry/txt_test.go b/registry/txt_test.go new file mode 100644 index 000000000..88302ad12 --- /dev/null +++ b/registry/txt_test.go @@ -0,0 +1,384 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "testing" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/internal/testutils" + "github.com/kubernetes-incubator/external-dns/plan" + "github.com/kubernetes-incubator/external-dns/provider" +) + +const ( + testZone = "test-zone.example.com." +) + +func TestTXTRegistry(t *testing.T) { + t.Run("TestNewTXTRegistry", testTXTRegistryNew) + t.Run("TestRecords", testTXTRegistryRecords) + t.Run("TestApplyChanges", testTXTRegistryApplyChanges) +} + +func testTXTRegistryNew(t *testing.T) { + p := provider.NewInMemoryProvider() + _, err := NewTXTRegistry(p, "txt", "") + if err == nil { + t.Fatal("owner should be specified") + } + + r, err := NewTXTRegistry(p, "txt", "owner") + if err != nil { + t.Fatal(err) + } + if _, ok := r.mapper.(prefixNameMapper); !ok { + t.Error("incorrectly initialized txt registry instance") + } + if r.ownerID != "owner" || r.provider != p { + t.Error("incorrectly initialized txt registry instance") + } + + r, err = NewTXTRegistry(p, "", "owner") + if err != nil { + t.Fatal(err) + } + if _, ok := r.mapper.(prefixNameMapper); !ok { + t.Error("Incorrect type of prefix name mapper") + } + + rs, err := r.Records("random-zone") + if err == nil || rs != nil { + t.Error("incorrect zone should trigger error") + } +} + +func testTXTRegistryRecords(t *testing.T) { + t.Run("With prefix", testTXTRegistryRecordsPrefixed) + t.Run("No prefix", testTXTRegistryRecordsNoPrefix) +} + +func testTXTRegistryRecordsPrefixed(t *testing.T) { + p := provider.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(testZone, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", "CNAME", ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", "CNAME", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", "ALIAS", ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", "TXT", ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner-2\"", "TXT", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + }) + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Target: "foo.loadbalancer.com", + RecordType: "CNAME", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Target: "my-domain.com", + RecordType: "CNAME", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "txt.bar.test-zone.example.org", + Target: "baz.test-zone.example.org", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "qux.test-zone.example.org", + Target: "random", + RecordType: "TXT", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "tar.test-zone.example.org", + Target: "tar.loadbalancer.com", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner-2", + }, + }, + { + DNSName: "foobar.test-zone.example.org", + Target: "foobar.loadbalancer.com", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + } + + r, _ := NewTXTRegistry(p, "txt.", "owner") + records, _ := r.Records(testZone) + if !testutils.SameEndpoints(records, expectedRecords) { + t.Error("incorrect result returned from txt registry") + } +} + +func testTXTRegistryRecordsNoPrefix(t *testing.T) { + p := provider.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(testZone, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", "CNAME", ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", "CNAME", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", "ALIAS", ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", "TXT", ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner-2\"", "TXT", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + }) + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Target: "foo.loadbalancer.com", + RecordType: "CNAME", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Target: "my-domain.com", + RecordType: "CNAME", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "txt.bar.test-zone.example.org", + Target: "baz.test-zone.example.org", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "qux.test-zone.example.org", + Target: "random", + RecordType: "TXT", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "tar.test-zone.example.org", + Target: "tar.loadbalancer.com", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "foobar.test-zone.example.org", + Target: "foobar.loadbalancer.com", + RecordType: "ALIAS", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + } + + r, _ := NewTXTRegistry(p, "", "owner") + records, _ := r.Records(testZone) + + if !testutils.SameEndpoints(records, expectedRecords) { + t.Error("incorrect result returned from txt registry") + } +} + +func testTXTRegistryApplyChanges(t *testing.T) { + t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix) + t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix) +} + +func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { + p := provider.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(testZone, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", "CNAME", ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", "CNAME", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", "ALIAS", ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", "TXT", ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + }) + r, _ := NewTXTRegistry(p, "txt.", "owner") + + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", "owner"), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", "ALIAS", "owner"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", "owner"), + }, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""), + newEndpointWithOwner("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", "owner"), + newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", "ALIAS", "owner"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", "owner"), + }, + } + p.OnApplyChanges = func(got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + if !testutils.SamePlanChanges(mGot, mExpected) { + t.Error("incorrect plan changes are passed to provider") + } + } + err := r.ApplyChanges(testZone, changes) + if err != nil { + t.Fatal(err) + } + + changes = &plan.Changes{} + p.OnApplyChanges = func(c *plan.Changes) {} + err = r.ApplyChanges("new-zone", changes) + if err == nil { + t.Error("expected error") + } +} + +func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { + p := provider.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(testZone, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", "CNAME", ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", "CNAME", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", "ALIAS", ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", "TXT", ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", ""), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + }) + r, _ := NewTXTRegistry(p, "", "owner") + + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", "owner"), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", "ALIAS", "owner-2"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", "ALIAS", "owner-2"), + }, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""), + newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", "ALIAS", "owner"), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns;external-dns/record-owner-id=owner\"", "TXT", ""), + }, + UpdateNew: []*endpoint.Endpoint{}, + UpdateOld: []*endpoint.Endpoint{}, + } + p.OnApplyChanges = func(got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + if !testutils.SamePlanChanges(mGot, mExpected) { + t.Error("incorrect plan changes are passed to provider") + } + } + err := r.ApplyChanges(testZone, changes) + if err != nil { + t.Fatal(err) + } +} + +/** + +helper methods + +*/ + +func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint.Endpoint { + e := endpoint.NewEndpoint(dnsName, target, recordType) + e.Labels[endpoint.OwnerLabelKey] = ownerID + return e +} diff --git a/source/compatibility.go b/source/compatibility.go index 3007eba02..d494848f1 100644 --- a/source/compatibility.go +++ b/source/compatibility.go @@ -46,10 +46,10 @@ func legacyEndpointsFromMateService(svc *v1.Service) []*endpoint.Endpoint { // Create a corresponding endpoint for each configured external entrypoint. for _, lb := range svc.Status.LoadBalancer.Ingress { if lb.IP != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP)) + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, "")) } if lb.Hostname != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname)) + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, "")) } } diff --git a/source/ingress.go b/source/ingress.go index db48b4b4b..d4d878ee2 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -71,10 +71,10 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint { } for _, lb := range ing.Status.LoadBalancer.Ingress { if lb.IP != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.IP)) + endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.IP, "")) } if lb.Hostname != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.Hostname)) + endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.Hostname, "")) } } } diff --git a/source/service.go b/source/service.go index 4bf380285..4545fe132 100644 --- a/source/service.go +++ b/source/service.go @@ -88,10 +88,11 @@ func endpointsFromService(svc *v1.Service) []*endpoint.Endpoint { // Create a corresponding endpoint for each configured external entrypoint. for _, lb := range svc.Status.LoadBalancer.Ingress { if lb.IP != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP)) + //TODO(ideahitme): consider retrieving record type from resource annotation instead of empty + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, "")) } if lb.Hostname != "" { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname)) + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, "")) } }