Allow specifying a custom TTL through annotation on Ingress or Service (#320)

* Add RecordTTL

* Route53: test for custom TTL

* Fix tests

* Fix remaining tests

* Add ttl when endpoint is created from ingress

* Missed a word

* Fix bad refactoring

* Add ingress custom TTL test

* gofmt

* Satisfy go-lint

* Unshadow `endpoint` in azure provider

* Fix and add an output test

* Add TTL for endpoints generated from service templates

* Take TTL into account when generating update plan

* Tests for TTL change impact on the plan

* Refactor factory method name

* Refactoring

* Run gofmt

* Make endpoint string format look like BIND config

* Update plan and plan_test

* Replace NewEndpointWithTTLValue with NewEndpointWithTTL in aws

* Remove NewEndpointWithTTLValue func

* Update references to TTL

* Remove getTTLValue func

* Handle merge conflict

* Update tests

* Update README, CHANGELOG and documentation

* Run gofmt

* Move getTTLFromAnnotations to a common file

* Refactor getTTLFromAnnotations

* Gofmt

* Add tests for getTTLFromAnnotations

* Trigger build

* Add boilerplate header

* Update README/CHANGELOG according to code review

* Add ttl.md and link it from README

* change CNAME string to endpoint.RecordTypeCNAME

* fix test cases with AWS ALIAS records, these do not behave different in these tests
This commit is contained in:
Kevin J. Qiu 2017-10-11 08:17:02 -04:00 committed by Martin Linkhorst
parent f5639c4cb7
commit 71723bdd5b
19 changed files with 436 additions and 76 deletions

View File

@ -1,4 +1,8 @@
- [AWS Route53 provider] Support customization of DNS record TTL through the use of annotation `external-dns.alpha.kubernetes.io/ttl` on services or ingresses (#320) @kevinjqiu
- Added support for [DNSimple](https://dnsimple.com/) as DNS provider (#224) @jose5918
## v0.4.5 - 2017-09-24
- Add `--log-level` flag to control log verbosity and remove `--debug` flag in favour of `--log-level=debug` (#339) @ultimateboy
- AWS: Allow filtering for private and public zones via `--aws-zone-type` flag (#329) @linki
- CloudFlare: Add `--cloudflare-proxied` flag to toggle CloudFlare proxy feature (#340) @dunglas

View File

@ -79,6 +79,14 @@ Annotate the Service with your desired external DNS name. Make sure to change `e
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
```
Optionally, you can customize the TTL value of the resulting DNS record by using the `external-dns.alpha.kubernetes.io/ttl` annotation:
```console
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
```
For more details on configuring TTL, see [here](docs/ttl.md).
Locally run a single sync loop of ExternalDNS.
```console

30
docs/ttl.md Normal file
View File

@ -0,0 +1,30 @@
Configure DNS record TTL (Time-To-Live)
=======================================
An optional annotation `external-dns.alpha.kubernetes.io/ttl` is available to customize the TTL value of a DNS record.
To configure it, simply annotate a service/ingress, e.g.:
```yaml
apiVersion: v1
kind: Service
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
external-dns.alpha.kubernetes.io/ttl: "60"
...
```
TTL must be a positive integer encoded as string.
Providers
=========
- [x] AWS (Route53)
- [ ] Azure
- [ ] Cloudflare
- [ ] DigitalOcean
- [ ] Google
- [ ] InMemory
PRs welcome!

View File

@ -184,6 +184,25 @@ $ curl nginx.external-dns-test.my-org.com.
Ingress objects on AWS require a separately deployed Ingress controller which we'll describe in another tutorial.
## Custom TTL
The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation `external-dns.alpha.kubernetes.io/ttl`.
e.g., modify the service manifest YAML file above:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
external-dns.alpha.kubernetes.io/ttl: 60
spec:
...
```
This will set the DNS record's TTL to 60 seconds.
## Clean up
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

View File

@ -32,6 +32,14 @@ const (
RecordTypeTXT = "TXT"
)
// TTL is a structure defining the TTL of a DNS record
type TTL int64
// IsConfigured returns true if TTL is configured, false otherwise
func (ttl TTL) IsConfigured() bool {
return ttl > 0
}
// Endpoint is a high-level way of a connection between a service and an IP
type Endpoint struct {
// The hostname of the DNS record
@ -40,17 +48,25 @@ type Endpoint struct {
Target string
// RecordType type of record, e.g. CNAME, A, TXT etc
RecordType string
// TTL for the record
RecordTTL TTL
// 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, recordType string) *Endpoint {
return NewEndpointWithTTL(dnsName, target, recordType, TTL(0))
}
// NewEndpointWithTTL initialization method to be used to create an endpoint with a TTL struct
func NewEndpointWithTTL(dnsName, target, recordType string, ttl TTL) *Endpoint {
return &Endpoint{
DNSName: strings.TrimSuffix(dnsName, "."),
Target: strings.TrimSuffix(target, "."),
RecordType: recordType,
Labels: map[string]string{},
RecordTTL: ttl,
}
}
@ -64,5 +80,5 @@ func (e *Endpoint) MergeLabels(labels map[string]string) {
}
func (e *Endpoint) String() string {
return fmt.Sprintf(`%s -> %s (type "%s")`, e.DNSName, e.Target, e.RecordType)
return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Target)
}

View File

@ -45,7 +45,7 @@ func (b byAllFields) Less(i, j int) bool {
// considers example.org. and example.org DNSName/Target as different endpoints
func SameEndpoint(a, b *endpoint.Endpoint) bool {
return a.DNSName == b.DNSName && a.Target == b.Target && a.RecordType == b.RecordType &&
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey]
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL
}
// SameEndpoints compares two slices of endpoints regardless of order

View File

@ -49,15 +49,22 @@ func ExampleSameEndpoints() {
Target: "foo.com",
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "cbc.com",
Target: "foo.com",
RecordType: "CNAME",
RecordTTL: endpoint.TTL(60),
},
}
sort.Sort(byAllFields(eps))
for _, ep := range eps {
fmt.Println(ep)
}
// Output:
// abc.com -> 1.2.3.4 (type "A")
// abc.com -> something (type "TXT")
// bbc.com -> foo.com (type "CNAME")
// example.org -> load-balancer.org (type "")
// example.org -> load-balancer.org (type "TXT")
// abc.com 0 IN A 1.2.3.4
// abc.com 0 IN TXT something
// bbc.com 0 IN CNAME foo.com
// cbc.com 60 IN CNAME foo.com
// example.org 0 IN load-balancer.org
// example.org 0 IN TXT load-balancer.org
}

View File

@ -65,17 +65,26 @@ func (p *Plan) Calculate() *Plan {
continue
}
// If there already is a record update it if it changed.
if desired.Target != current.Target {
changes.UpdateOld = append(changes.UpdateOld, current)
targetChanged := targetChanged(desired, current)
shouldUpdateTTL := shouldUpdateTTL(desired, current)
desired.RecordType = current.RecordType // inherit the type from the dns provider
desired.MergeLabels(current.Labels) // inherit the labels from the dns provider, including Owner ID
changes.UpdateNew = append(changes.UpdateNew, desired)
if !targetChanged && !shouldUpdateTTL {
log.Debugf("Skipping endpoint %v because nothing has changed", desired)
continue
}
log.Debugf("Skipping endpoint %v because target has not changed", desired)
changes.UpdateOld = append(changes.UpdateOld, current)
desired.MergeLabels(current.Labels) // inherit the labels from the dns provider, including Owner ID
if targetChanged {
desired.RecordType = current.RecordType // inherit the type from the dns provider
}
if !shouldUpdateTTL {
desired.RecordTTL = current.RecordTTL
}
changes.UpdateNew = append(changes.UpdateNew, desired)
}
// Ensure all undesired records are removed. Each current record that cannot
@ -100,6 +109,17 @@ func (p *Plan) Calculate() *Plan {
return plan
}
func targetChanged(desired, current *endpoint.Endpoint) bool {
return desired.Target != current.Target
}
func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
if !desired.RecordTTL.IsConfigured() {
return false
}
return desired.RecordTTL != current.RecordTTL
}
// recordExists checks whether a record can be found in a list of records.
func recordExists(needle *endpoint.Endpoint, haystack []*endpoint.Endpoint) (*endpoint.Endpoint, bool) {
for _, record := range haystack {

View File

@ -46,6 +46,16 @@ func TestCalculate(t *testing.T) {
typedV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeA)}
typedV1 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", endpoint.RecordTypeA)}
// test case with TTL
ttl := endpoint.TTL(300)
ttl2 := endpoint.TTL(50)
ttlV1 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl)}
ttlV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", endpoint.RecordTypeCNAME)}
ttlV3 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl)}
ttlV4 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl2)}
ttlV5 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeCNAME)}
ttlV6 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v2", endpoint.RecordTypeCNAME, ttl)}
for _, tc := range []struct {
policies []Policy
current, desired []*endpoint.Endpoint
@ -69,6 +79,14 @@ func TestCalculate(t *testing.T) {
{[]Policy{&SyncPolicy{}}, labeledV1, noLabels, empty, labeledV1, labeledV2, empty},
// RecordType should be inherited
{[]Policy{&SyncPolicy{}}, typedV1, noType, empty, typedV1, typedV2, empty},
// If desired TTL is not configured, do not update
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV2, empty, empty, empty, empty},
// If desired TTL is configured but is the same as current TTL, do not update
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV3, empty, empty, empty, empty},
// If desired TTL is configured and is not the same as current TTL, need to update
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV4, empty, ttlV1, ttlV4, empty},
// If target changed and desired TTL is not configured, do not update TTL
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV5, empty, ttlV1, ttlV6, empty},
} {
// setup plan
plan := &Plan{

View File

@ -158,12 +158,18 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
if !supportedRecordType(aws.StringValue(r.Type)) {
continue
}
var ttl endpoint.TTL
if r.TTL != nil {
ttl = endpoint.TTL(*r.TTL)
}
for _, rr := range r.ResourceRecords {
endpoints = append(endpoints, endpoint.NewEndpoint(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(rr.Value), aws.StringValue(r.Type)))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(rr.Value), aws.StringValue(r.Type), ttl))
}
if r.AliasTarget != nil {
endpoints = append(endpoints, endpoint.NewEndpoint(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.AliasTarget.DNSName), endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.AliasTarget.DNSName), endpoint.RecordTypeCNAME, ttl))
}
}
@ -310,7 +316,11 @@ func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
}
} else {
change.ResourceRecordSet.Type = aws.String(endpoint.RecordType)
change.ResourceRecordSet.TTL = aws.Int64(recordTTL)
if !endpoint.RecordTTL.IsConfigured() {
change.ResourceRecordSet.TTL = aws.Int64(recordTTL)
} else {
change.ResourceRecordSet.TTL = aws.Int64(int64(endpoint.RecordTTL))
}
change.ResourceRecordSet.ResourceRecords = []*route53.ResourceRecord{
{
Value: aws.String(endpoint.Target),

View File

@ -203,9 +203,9 @@ func TestAWSZones(t *testing.T) {
func TestAWSRecords(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
})
@ -214,20 +214,22 @@ func TestAWSRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
})
}
func TestAWSCreateRecords(t *testing.T) {
customTTL := endpoint.TTL(60)
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", "172.17.0.1", endpoint.RecordTypeA, customTTL),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
}
@ -237,17 +239,18 @@ func TestAWSCreateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", "172.17.0.1", endpoint.RecordTypeA, customTTL),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
})
}
func TestAWSUpdateRecords(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
})
currentRecords := []*endpoint.Endpoint{
@ -267,17 +270,17 @@ func TestAWSUpdateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
})
}
func TestAWSDeleteRecords(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
}
@ -295,14 +298,14 @@ func TestAWSDeleteRecords(t *testing.T) {
func TestAWSApplyChanges(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
})
createRecords := []*endpoint.Endpoint{
@ -345,27 +348,27 @@ func TestAWSApplyChanges(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
})
}
func TestAWSApplyChangesDryRun(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
}
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), true, originalEndpoints)
@ -490,7 +493,7 @@ func TestAWSChangesByZones(t *testing.T) {
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "expected and actual endpoints don't match")
assert.True(t, testutils.SameEndpoints(endpoints, expected), "expected and actual endpoints don't match. %s:%s", endpoints, expected)
}
func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) {

View File

@ -151,14 +151,14 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
log.Errorf("Failed to extract target for '%s' with type '%s'.", name, recordType)
return true
}
endpoint := endpoint.NewEndpoint(name, target, recordType)
ep := endpoint.NewEndpoint(name, target, recordType)
log.Debugf(
"Found %s record for '%s' with target '%s'.",
endpoint.RecordType,
endpoint.DNSName,
endpoint.Target,
ep.RecordType,
ep.DNSName,
ep.Target,
)
endpoints = append(endpoints, endpoint)
endpoints = append(endpoints, ep)
return true
})
if err != nil {

View File

@ -112,11 +112,15 @@ func getEndpointsFromTargetAnnotation(ing *v1beta1.Ingress, hostname string) []*
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, exists := ing.Annotations[targetAnnotationKey]
if exists {
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
// splits the hostname annotation and removes the trailing periods
targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, targetHostname, suitableType(targetHostname)))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, targetHostname, suitableType(targetHostname), ttl))
}
}
return endpoints
@ -139,12 +143,16 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
return endpoints, nil
}
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
for _, lb := range ing.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.IP, endpoint.RecordTypeA, ttl))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
}
}
@ -167,12 +175,17 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
continue
}
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
for _, lb := range ing.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.IP, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rule.Host, lb.IP, endpoint.RecordTypeA, ttl))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(rule.Host, lb.Hostname, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rule.Host, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
}
}
}

View File

@ -388,6 +388,44 @@ func testIngressEndpoints(t *testing.T) {
},
},
},
{
title: "ingress rules with annotation and custom TTL",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{
targetAnnotationKey: "ingress-target.com",
ttlAnnotationKey: "6",
},
dnsnames: []string{"example.org"},
ips: []string{},
},
{
name: "fake2",
namespace: namespace,
annotations: map[string]string{
targetAnnotationKey: "ingress-target.com",
ttlAnnotationKey: "1",
},
dnsnames: []string{"example2.org"},
ips: []string{"8.8.8.8"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "ingress-target.com",
RecordTTL: endpoint.TTL(6),
},
{
DNSName: "example2.org",
Target: "ingress-target.com",
RecordTTL: endpoint.TTL(1),
},
},
},
{
title: "template for ingress with annotation",
targetNamespace: "",

View File

@ -160,30 +160,37 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*
endpoints = append(endpoints, extractServiceIps(svc, hostname)...)
}
}
return endpoints
}
func extractServiceIps(svc *v1.Service, hostname string) []*endpoint.Endpoint {
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
log.Warn(err)
}
if svc.Spec.ClusterIP == v1.ClusterIPNone {
log.Debugf("Unable to associate %s headless service with a Cluster IP", svc.Name)
return []*endpoint.Endpoint{}
}
return []*endpoint.Endpoint{endpoint.NewEndpoint(hostname, svc.Spec.ClusterIP, endpoint.RecordTypeA)}
return []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(hostname, svc.Spec.ClusterIP, endpoint.RecordTypeA, ttl)}
}
func extractLoadBalancerEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
log.Warn(err)
}
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
//TODO(ideahitme): consider retrieving record type from resource annotation instead of empty
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.IP, endpoint.RecordTypeA, ttl))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
}
}

View File

@ -488,6 +488,85 @@ func testServiceSourceEndpoints(t *testing.T) {
[]*endpoint.Endpoint{},
true,
},
{
"ttl not annotated should have RecordTTL.IsConfigured set to false",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
},
false,
},
{
"ttl annotated but invalid should have RecordTTL.IsConfigured set to false",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "foo",
},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
},
false,
},
{
"ttl annotated and is valid should set Record.TTL",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "10",
},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(10)},
},
false,
},
{
"Negative ttl is not valid",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "-10",
},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
},
false,
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client

View File

@ -43,6 +43,10 @@ func validateEndpoint(t *testing.T, endpoint, expected *endpoint.Endpoint) {
t.Errorf("expected %s, got %s", expected.Target, endpoint.Target)
}
if endpoint.RecordTTL != expected.RecordTTL {
t.Errorf("expected %v, got %v", expected.RecordTTL, endpoint.RecordTTL)
}
// if non-empty record type is expected, check that it matches.
if expected.RecordType != "" && endpoint.RecordType != expected.RecordType {
t.Errorf("expected %s, got %s", expected.RecordType, endpoint.RecordType)

View File

@ -17,7 +17,10 @@ limitations under the License.
package source
import (
"fmt"
"math"
"net"
"strconv"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
@ -29,15 +32,38 @@ const (
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
// The annotation used for defining the desired ingress target
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
// The value of the controller annotation so that we feel resposible
// The annotation used for defining the desired DNS record TTL
ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl"
// The value of the controller annotation so that we feel responsible
controllerAnnotationValue = "dns-controller"
)
const (
ttlMinimum = 1
ttlMaximum = math.MaxUint32
)
// Source defines the interface Endpoint sources should implement.
type Source interface {
Endpoints() ([]*endpoint.Endpoint, error)
}
func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error) {
ttlNotConfigured := endpoint.TTL(0)
ttlAnnotation, exists := annotations[ttlAnnotationKey]
if !exists {
return ttlNotConfigured, nil
}
ttlValue, err := strconv.ParseInt(ttlAnnotation, 10, 64)
if err != nil {
return ttlNotConfigured, fmt.Errorf("\"%v\" is not a valid TTL value", ttlAnnotation)
}
if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
return ttlNotConfigured, fmt.Errorf("TTL value must be between [%d, %d]", ttlMinimum, ttlMaximum)
}
return endpoint.TTL(ttlValue), nil
}
// 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 {

View File

@ -16,7 +16,65 @@ limitations under the License.
package source
import "testing"
import (
"fmt"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/stretchr/testify/assert"
)
func TestGetTTLFromAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedTTL endpoint.TTL
expectedErr error
}{
{
title: "TTL annotation not present",
annotations: map[string]string{"foo": "bar"},
expectedTTL: endpoint.TTL(0),
expectedErr: nil,
},
{
title: "TTL annotation value is not a number",
annotations: map[string]string{ttlAnnotationKey: "foo"},
expectedTTL: endpoint.TTL(0),
expectedErr: fmt.Errorf("\"foo\" is not a valid TTL value"),
},
{
title: "TTL annotation value is empty",
annotations: map[string]string{ttlAnnotationKey: ""},
expectedTTL: endpoint.TTL(0),
expectedErr: fmt.Errorf("\"\" is not a valid TTL value"),
},
{
title: "TTL annotation value is negative number",
annotations: map[string]string{ttlAnnotationKey: "-1"},
expectedTTL: endpoint.TTL(0),
expectedErr: fmt.Errorf("TTL value must be between [%d, %d]", ttlMinimum, ttlMaximum),
},
{
title: "TTL annotation value is too high",
annotations: map[string]string{ttlAnnotationKey: fmt.Sprintf("%d", 1<<32)},
expectedTTL: endpoint.TTL(0),
expectedErr: fmt.Errorf("TTL value must be between [%d, %d]", ttlMinimum, ttlMaximum),
},
{
title: "TTL annotation value is set correctly",
annotations: map[string]string{ttlAnnotationKey: "60"},
expectedTTL: endpoint.TTL(60),
expectedErr: nil,
},
} {
t.Run(tc.title, func(t *testing.T) {
ttl, err := getTTLFromAnnotations(tc.annotations)
assert.Equal(t, tc.expectedTTL, ttl)
assert.Equal(t, tc.expectedErr, err)
})
}
}
func TestSuitableType(t *testing.T) {
for _, tc := range []struct {