kickoff txt registry (#137)

* kickoff txt registry

* fix inmemory dns provider to include recordtype info for validation

* Merge master

* fix ununsed variable in inmemory provider

* add tests for records

* add test for no prefix name formatter

* implement apply changes with tests

* add flag to enable txt registry

* add txt registry to main

* improve sort testing

* filter out non-owned records

* NewEndpoint(...) requires record type

* use newendpoint in aws_test, fix tests

* change suitable type implementation

* fix the test for compatibility component

* change inmemory provider to include recordtype and use suitable type

* fix comments, CNAME should target hostname

* name mapper do not use pointer on struct

* txt prefix - just concatenate, remove spew, fix txt record label

* allow TXT records as result from dns provider

* add changelog

* fix tests

* TXT records need to be enclosed in double quotes
This commit is contained in:
Yerken 2017-04-11 23:10:38 +02:00 committed by Henning Jacobs
parent 3d296f37d9
commit 98de0142ba
23 changed files with 862 additions and 171 deletions

2
.gitignore vendored
View File

@ -20,6 +20,7 @@
/output*/
/_output*/
/_output
/build
# Emacs save files
*~
@ -40,3 +41,4 @@ cscope.*
# coverage output
cover.out
*.coverprofile

View File

@ -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:

View File

@ -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{},
}
}

View File

@ -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",

View File

@ -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
}

View File

@ -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[]}
}

11
main.go
View File

@ -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)
}

View File

@ -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: <text|json>")
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: <noop|txt>")
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)
}

View File

@ -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",
},
},
{

View File

@ -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
}

View File

@ -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)),
},
}

View File

@ -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)
}

View File

@ -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),
}
}

View File

@ -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)
}
}

View File

@ -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",
},
},
},

View File

@ -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"
}

View File

@ -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",
},
},
})

View File

@ -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
}

160
registry/txt.go Normal file
View File

@ -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
}

384
registry/txt_test.go Normal file
View File

@ -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
}

View File

@ -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, ""))
}
}

View File

@ -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, ""))
}
}
}

View File

@ -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, ""))
}
}