add multi-zone capability to google provider (take 2) (#163)

* feat(google): auto-detect and multiple zone support

* chore: run gofmt with the simplified command

* fix: pass desired domain to google provider

* feat(google): correctly auto-detect records for sub-zones

* chore: update changelog with support for multiple zones in google

* fix(google): don't append traling dot to TXT records

* ref(provider): extract hostname sanitization to general provider
This commit is contained in:
Martin Linkhorst 2017-04-25 17:45:36 +02:00 committed by GitHub
parent 5e3f2b7773
commit e5f21ad32a
7 changed files with 632 additions and 284 deletions

View File

@ -1,7 +1,7 @@
Features:
- Improved logging
- Generate DNS Name from template for services/ingress if annotation is missing but `--fqdn-template` is specified
- Route 53: Support creation of records in multiple hosted zones.
- Route 53, Google CloudDNS: Support creation of records in multiple hosted zones.
- Route 53: Support creation of ALIAS records when endpoint target is a ELB/ALB.
- Ownership via TXT records
1. Create TXT records to mark the records managed by External DNS

View File

@ -105,7 +105,7 @@ func main() {
var p provider.Provider
switch cfg.Provider {
case "google":
p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DryRun)
p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.Domain, cfg.DryRun)
case "aws":
p, err = provider.NewAWSProvider(cfg.Domain, cfg.DryRun)
default:

View File

@ -17,7 +17,6 @@ limitations under the License.
package provider
import (
"net"
"strings"
log "github.com/Sirupsen/logrus"
@ -327,12 +326,3 @@ func canonicalHostedZone(hostname string) string {
return ""
}
// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already.
func ensureTrailingDot(hostname string) string {
if net.ParseIP(hostname) != nil {
return hostname
}
return strings.TrimSuffix(hostname, ".") + "."
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package provider
import (
"strings"
log "github.com/Sirupsen/logrus"
"golang.org/x/net/context"
@ -33,17 +35,12 @@ type managedZonesCreateCallInterface interface {
Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error)
}
type managedZonesDeleteCallInterface interface {
Do(opts ...googleapi.CallOption) error
}
type managedZonesListCallInterface interface {
Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error
}
type managedZonesServiceInterface interface {
Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface
Delete(project string, managedZone string) managedZonesDeleteCallInterface
List(project string) managedZonesListCallInterface
}
@ -79,10 +76,6 @@ func (m managedZonesService) Create(project string, managedzone *dns.ManagedZone
return m.service.Create(project, managedzone)
}
func (m managedZonesService) Delete(project string, managedZone string) managedZonesDeleteCallInterface {
return m.service.Delete(project, managedZone)
}
func (m managedZonesService) List(project string) managedZonesListCallInterface {
return m.service.List(project)
}
@ -101,6 +94,8 @@ type googleProvider struct {
project string
// Enabled dry-run will print any modifying actions rather than execute them.
dryRun bool
// only consider hosted zones managing domains ending in this suffix
domain string
// A client for managing resource record sets
resourceRecordSetsClient resourceRecordSetsClientInterface
// A client for managing hosted zones
@ -110,7 +105,7 @@ type googleProvider struct {
}
// NewGoogleProvider initializes a new Google CloudDNS based Provider.
func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
func NewGoogleProvider(project string, domain string, dryRun bool) (Provider, error) {
gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, err
@ -123,6 +118,7 @@ func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
provider := &googleProvider{
project: project,
domain: domain,
dryRun: dryRun,
resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
managedZonesClient: managedZonesService{dnsClient.ManagedZones},
@ -133,49 +129,33 @@ func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
}
// Zones returns the list of hosted zones.
func (p *googleProvider) Zones() (zones []*dns.ManagedZone, _ error) {
func (p *googleProvider) Zones() (map[string]*dns.ManagedZone, error) {
zones := make(map[string]*dns.ManagedZone)
f := func(resp *dns.ManagedZonesListResponse) error {
// each page is processed sequentially, no need for a mutex here.
zones = append(zones, resp.ManagedZones...)
for _, zone := range resp.ManagedZones {
if strings.HasSuffix(zone.DnsName, p.domain) {
zones[zone.Name] = zone
}
}
return nil
}
err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f)
if err != nil {
if err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f); err != nil {
return nil, err
}
return zones, nil
}
// CreateZone creates a hosted zone given a name.
func (p *googleProvider) CreateZone(name, domain string) error {
zone := &dns.ManagedZone{
Name: name,
DnsName: domain,
Description: "Automatically managed zone by kubernetes.io/external-dns",
}
_, err := p.managedZonesClient.Create(p.project, zone).Do()
// Records returns the list of records in all relevant zones.
func (p *googleProvider) Records(_ string) (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.Zones()
if err != nil {
return err
return nil, err
}
return nil
}
// DeleteZone deletes a hosted zone given a name.
func (p *googleProvider) DeleteZone(name string) error {
err := p.managedZonesClient.Delete(p.project, name).Do()
if err != nil {
return err
}
return nil
}
// Records returns the list of A records in a given hosted zone.
func (p *googleProvider) Records(zone string) (endpoints []*endpoint.Endpoint, _ error) {
f := func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
// TODO(linki, ownership): Remove once ownership system is in place.
@ -196,44 +176,45 @@ func (p *googleProvider) Records(zone string) (endpoints []*endpoint.Endpoint, _
return nil
}
err := p.resourceRecordSetsClient.List(p.project, zone).Pages(context.TODO(), f)
if err != nil {
for _, z := range zones {
if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(context.TODO(), f); err != nil {
return nil, err
}
}
return endpoints, nil
}
// CreateRecords creates a given set of DNS records in the given hosted zone.
func (p *googleProvider) CreateRecords(zone string, endpoints []*endpoint.Endpoint) error {
func (p *googleProvider) CreateRecords(endpoints []*endpoint.Endpoint) error {
change := &dns.Change{}
change.Additions = append(change.Additions, newRecords(endpoints)...)
return p.submitChange(zone, change)
return p.submitChange(change)
}
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
func (p *googleProvider) UpdateRecords(zone string, records, oldRecords []*endpoint.Endpoint) error {
func (p *googleProvider) UpdateRecords(records, oldRecords []*endpoint.Endpoint) error {
change := &dns.Change{}
change.Additions = append(change.Additions, newRecords(records)...)
change.Deletions = append(change.Deletions, newRecords(oldRecords)...)
return p.submitChange(zone, change)
return p.submitChange(change)
}
// DeleteRecords deletes a given set of DNS records in a given zone.
func (p *googleProvider) DeleteRecords(zone string, endpoints []*endpoint.Endpoint) error {
func (p *googleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
change := &dns.Change{}
change.Deletions = append(change.Deletions, newRecords(endpoints)...)
return p.submitChange(zone, change)
return p.submitChange(change)
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *googleProvider) ApplyChanges(zone string, changes *plan.Changes) error {
func (p *googleProvider) ApplyChanges(_ string, changes *plan.Changes) error {
change := &dns.Change{}
change.Additions = append(change.Additions, newRecords(changes.Create)...)
@ -243,12 +224,11 @@ func (p *googleProvider) ApplyChanges(zone string, changes *plan.Changes) error
change.Deletions = append(change.Deletions, newRecords(changes.Delete)...)
return p.submitChange(zone, change)
return p.submitChange(change)
}
// submitChange takes a zone and a Change and sends it to Google.
func (p *googleProvider) submitChange(zone string, change *dns.Change) error {
func (p *googleProvider) submitChange(change *dns.Change) error {
if len(change.Additions) == 0 && len(change.Deletions) == 0 {
log.Infoln("Received empty list of records for creation and deletion")
return nil
@ -261,16 +241,75 @@ func (p *googleProvider) submitChange(zone string, change *dns.Change) error {
log.Infof("Add records: %s %s %s", add.Name, add.Type, add.Rrdatas)
}
if !p.dryRun {
_, err := p.changesClient.Create(p.project, zone, change).Do()
if p.dryRun {
return nil
}
zones, err := p.Zones()
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changes := separateChange(zones, change)
for z, c := range changes {
if _, err := p.changesClient.Create(p.project, z, c).Do(); err != nil {
return err
}
}
return nil
}
// separateChange separates a multi-zone change into a single change per zone.
func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change {
changes := make(map[string]*dns.Change)
for _, z := range zones {
changes[z.Name] = &dns.Change{
Additions: []*dns.ResourceRecordSet{},
Deletions: []*dns.ResourceRecordSet{},
}
}
for _, a := range change.Additions {
if zone := suitableManagedZone(ensureTrailingDot(a.Name), zones); zone != nil {
changes[zone.Name].Additions = append(changes[zone.Name].Additions, a)
}
}
for _, d := range change.Deletions {
if zone := suitableManagedZone(ensureTrailingDot(d.Name), zones); zone != nil {
changes[zone.Name].Deletions = append(changes[zone.Name].Deletions, d)
}
}
// separating a change could lead to empty sub changes, remove them here.
for zone, change := range changes {
if len(change.Additions) == 0 && len(change.Deletions) == 0 {
delete(changes, zone)
}
}
return changes
}
// suitableManagedZone returns the most suitable zone for a given hostname and a set of zones.
func suitableManagedZone(hostname string, zones map[string]*dns.ManagedZone) *dns.ManagedZone {
var zone *dns.ManagedZone
for _, z := range zones {
if strings.HasSuffix(hostname, z.DnsName) {
if zone == nil || len(z.DnsName) > len(zone.DnsName) {
zone = z
}
}
}
return zone
}
// newRecords returns a collection of RecordSets based on the given endpoints.
func newRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet {
records := make([]*dns.ResourceRecordSet, len(endpoints))
@ -284,9 +323,17 @@ func newRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet {
// newRecord returns a RecordSet based on the given endpoint.
func newRecord(endpoint *endpoint.Endpoint) *dns.ResourceRecordSet {
// TODO(linki): works around appending a trailing dot to TXT records. I think
// we should go back to storing DNS names with a trailing dot internally. This
// way we can use it has is here and trim it off if it exists when necessary.
target := endpoint.Target
if suitableType(endpoint) == "CNAME" {
target = ensureTrailingDot(target)
}
return &dns.ResourceRecordSet{
Name: ensureTrailingDot(endpoint.DNSName),
Rrdatas: []string{ensureTrailingDot(endpoint.Target)},
Rrdatas: []string{target},
Ttl: 300,
Type: suitableType(endpoint),
}

View File

@ -18,6 +18,8 @@ package provider
import (
"fmt"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
@ -25,320 +27,603 @@ import (
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
dns "google.golang.org/api/dns/v1"
googleapi "google.golang.org/api/googleapi"
"google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
)
var (
expectedZones = []*dns.ManagedZone{{Name: "expected"}}
expectedRecordSets = []*dns.ResourceRecordSet{
{
Type: "A",
Name: "expected-1",
Rrdatas: []string{"8.8.8.8"},
},
{
Type: "CNAME",
Name: "expected-2",
Rrdatas: []string{"target.com"},
},
{
Type: "NS",
Name: "unexpected",
Rrdatas: []string{"target"},
},
}
testZones = map[string]*dns.ManagedZone{}
testRecords = map[string]map[string]*dns.ResourceRecordSet{}
)
type mockManagedZonesCreateCall struct{}
type mockManagedZonesCreateCall struct {
project string
managedZone *dns.ManagedZone
}
func (m *mockManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) {
return nil, nil
zoneKey := zoneKey(m.project, m.managedZone.Name)
if _, ok := testZones[zoneKey]; ok {
return nil, &googleapi.Error{Code: http.StatusConflict}
}
type mockErrManagedZonesCreateCall struct{}
testZones[zoneKey] = m.managedZone
func (m *mockErrManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) {
return nil, fmt.Errorf("failed")
return m.managedZone, nil
}
type mockManagedZonesDeleteCall struct{}
func (m *mockManagedZonesDeleteCall) Do(opts ...googleapi.CallOption) error {
return nil
type mockManagedZonesListCall struct {
project string
}
type mockErrManagedZonesDeleteCall struct{}
func (m *mockErrManagedZonesDeleteCall) Do(opts ...googleapi.CallOption) error {
return fmt.Errorf("failed")
}
type mockManagedZonesListCall struct{}
func (m *mockManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error {
return f(&dns.ManagedZonesListResponse{ManagedZones: expectedZones})
zones := []*dns.ManagedZone{}
for k, v := range testZones {
if strings.HasPrefix(k, m.project+"/") {
zones = append(zones, v)
}
}
type mockErrManagedZonesListCall struct{}
func (m *mockErrManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error {
return fmt.Errorf("failed")
return f(&dns.ManagedZonesListResponse{ManagedZones: zones})
}
type mockManagedZonesClient struct{}
func (m *mockManagedZonesClient) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
return &mockManagedZonesCreateCall{}
}
func (m *mockManagedZonesClient) Delete(project string, managedZone string) managedZonesDeleteCallInterface {
return &mockManagedZonesDeleteCall{}
func (m *mockManagedZonesClient) Create(project string, managedZone *dns.ManagedZone) managedZonesCreateCallInterface {
return &mockManagedZonesCreateCall{project: project, managedZone: managedZone}
}
func (m *mockManagedZonesClient) List(project string) managedZonesListCallInterface {
return &mockManagedZonesListCall{}
return &mockManagedZonesListCall{project: project}
}
type mockErrManagedZonesClient struct{}
func (m *mockErrManagedZonesClient) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
return &mockErrManagedZonesCreateCall{}
type mockResourceRecordSetsListCall struct {
project string
managedZone string
}
func (m *mockErrManagedZonesClient) Delete(project string, managedZone string) managedZonesDeleteCallInterface {
return &mockErrManagedZonesDeleteCall{}
}
func (m *mockErrManagedZonesClient) List(project string) managedZonesListCallInterface {
return &mockErrManagedZonesListCall{}
}
type mockResourceRecordSetsListCall struct{}
func (m *mockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error {
return f(&dns.ResourceRecordSetsListResponse{Rrsets: expectedRecordSets})
zoneKey := zoneKey(m.project, m.managedZone)
if _, ok := testZones[zoneKey]; !ok {
return &googleapi.Error{Code: http.StatusNotFound}
}
type mockErrResourceRecordSetsListCall struct{}
resp := []*dns.ResourceRecordSet{}
func (m *mockErrResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error {
return fmt.Errorf("failed")
for _, v := range testRecords[zoneKey] {
resp = append(resp, v)
}
return f(&dns.ResourceRecordSetsListResponse{Rrsets: resp})
}
type mockResourceRecordSetsClient struct{}
func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
return &mockResourceRecordSetsListCall{}
return &mockResourceRecordSetsListCall{project: project, managedZone: managedZone}
}
type mockErrResourceRecordSetsClient struct{}
func (m *mockErrResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
return &mockErrResourceRecordSetsListCall{}
type mockChangesCreateCall struct {
project string
managedZone string
change *dns.Change
}
type mockChangesCreateCall struct{}
func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) {
return nil, nil
zoneKey := zoneKey(m.project, m.managedZone)
if _, ok := testZones[zoneKey]; !ok {
return nil, &googleapi.Error{Code: http.StatusNotFound}
}
type mockErrChangesCreateCall struct{}
if _, ok := testRecords[zoneKey]; !ok {
testRecords[zoneKey] = make(map[string]*dns.ResourceRecordSet)
}
func (m *mockErrChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) {
return nil, fmt.Errorf("failed")
for _, c := range append(m.change.Additions, m.change.Deletions...) {
if !isValidRecordSet(c) {
return nil, &googleapi.Error{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("invalid record: %v", c),
}
}
}
for _, del := range m.change.Deletions {
recordKey := recordKey(del.Type, del.Name)
delete(testRecords[zoneKey], recordKey)
}
for _, add := range m.change.Additions {
recordKey := recordKey(add.Type, add.Name)
testRecords[zoneKey][recordKey] = add
}
return m.change, nil
}
type mockChangesClient struct{}
func (m *mockChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface {
return &mockChangesCreateCall{}
return &mockChangesCreateCall{project: project, managedZone: managedZone, change: change}
}
type mockErrChangesClient struct{}
func zoneKey(project, zoneName string) string {
return project + "/" + zoneName
}
func (m *mockErrChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface {
return &mockErrChangesCreateCall{}
func recordKey(recordType, recordName string) string {
return recordType + "/" + recordName
}
func isValidRecordSet(recordSet *dns.ResourceRecordSet) bool {
if !hasTrailingDot(recordSet.Name) {
return false
}
switch recordSet.Type {
case "CNAME":
for _, rrd := range recordSet.Rrdatas {
if !hasTrailingDot(rrd) {
return false
}
}
case "A", "TXT":
for _, rrd := range recordSet.Rrdatas {
if hasTrailingDot(rrd) {
return false
}
}
default:
panic("unhandled record type")
}
return true
}
func hasTrailingDot(target string) bool {
return strings.HasSuffix(target, ".")
}
func TestGoogleZones(t *testing.T) {
provider := &googleProvider{
project: "project",
managedZonesClient: &mockManagedZonesClient{},
}
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{})
zones, err := provider.Zones()
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
if len(zones) != len(expectedZones) {
t.Errorf("expected %d zones, got %d", len(expectedZones), len(zones))
}
provider.managedZonesClient = &mockErrManagedZonesClient{}
_, err = provider.Zones()
if err == nil {
t.Errorf("expected error")
}
}
func TestGoogleCreateZone(t *testing.T) {
provider := &googleProvider{
project: "project",
managedZonesClient: &mockManagedZonesClient{},
}
err := provider.CreateZone("name", "domain")
if err != nil {
t.Errorf("should not fail: %s", err)
}
provider.managedZonesClient = &mockErrManagedZonesClient{}
err = provider.CreateZone("name", "domain")
if err == nil {
t.Errorf("expected error")
}
}
func TestGoogleDeleteZone(t *testing.T) {
provider := &googleProvider{
project: "project",
managedZonesClient: &mockManagedZonesClient{},
}
err := provider.DeleteZone("name")
if err != nil {
t.Errorf("should not fail: %s", err)
}
provider.managedZonesClient = &mockErrManagedZonesClient{}
err = provider.DeleteZone("name")
if err == nil {
t.Errorf("expected error")
}
validateZones(t, zones, map[string]*dns.ManagedZone{
"zone-1-ext-dns-test-2-gcp-zalan-do": {Name: "zone-1-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-1.ext-dns-test-2.gcp.zalan.do."},
"zone-2-ext-dns-test-2-gcp-zalan-do": {Name: "zone-2-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-2.ext-dns-test-2.gcp.zalan.do."},
"zone-3-ext-dns-test-2-gcp-zalan-do": {Name: "zone-3-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do."},
})
}
func TestGoogleRecords(t *testing.T) {
provider := &googleProvider{
project: "project",
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
}
endpoints, err := provider.Records("zone")
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, originalEndpoints)
records, err := provider.Records("_")
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
if len(endpoints) != len(expectedRecordSets)-1 {
t.Errorf("expected %d endpoints, got %d", len(expectedRecordSets)-1, len(endpoints))
}
provider.resourceRecordSetsClient = &mockErrResourceRecordSetsClient{}
_, err = provider.Records("zone")
if err == nil {
t.Errorf("expected error")
}
validateEndpoints(t, records, originalEndpoints)
}
func TestGoogleCreateRecords(t *testing.T) {
provider := &googleProvider{
project: "project",
changesClient: &mockChangesClient{},
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", ""),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", ""),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", ""),
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "dns-name",
Target: "target",
},
if err := provider.CreateRecords(records); err != nil {
t.Fatal(err)
}
err := provider.CreateRecords("zone", endpoints)
records, err := provider.Records("_")
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
provider.changesClient = &mockErrChangesClient{}
err = provider.CreateRecords("zone", endpoints)
if err == nil {
t.Errorf("expected error")
}
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
})
}
func TestGoogleUpdateRecords(t *testing.T) {
provider := &googleProvider{
project: "project",
changesClient: &mockChangesClient{},
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
})
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
}
records := []*endpoint.Endpoint{
{
DNSName: "dns-name",
Target: "target",
},
if err := provider.UpdateRecords(updatedRecords, currentRecords); err != nil {
t.Fatal(err)
}
oldRecords := []*endpoint.Endpoint{
{
DNSName: "dns-name",
Target: "target",
},
}
err := provider.UpdateRecords("zone", records, oldRecords)
records, err := provider.Records("_")
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
err = provider.UpdateRecords("zone", nil, nil)
if err != nil {
t.Errorf("should not fail: %s", err)
}
provider.dryRun = true
err = provider.UpdateRecords("zone", records, oldRecords)
if err != nil {
t.Errorf("should not fail: %s", err)
}
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
})
}
func TestGoogleDeleteRecords(t *testing.T) {
provider := &googleProvider{
project: "project",
changesClient: &mockChangesClient{},
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", "CNAME"),
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "dns-name",
Target: "target",
},
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, originalEndpoints)
if err := provider.DeleteRecords(originalEndpoints); err != nil {
t.Fatal(err)
}
err := provider.DeleteRecords("zone", endpoints)
records, err := provider.Records("_")
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
validateEndpoints(t, records, []*endpoint.Endpoint{})
}
func TestGoogleApplyChanges(t *testing.T) {
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", "CNAME"),
})
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", "CNAME"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", "CNAME"),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
Delete: deleteRecords,
}
if err := provider.ApplyChanges("_", changes); err != nil {
t.Fatal(err)
}
records, err := provider.Records("_")
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", "A"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", "CNAME"),
})
}
func TestGoogleApplyChangesDryRun(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", "CNAME"),
}
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", true, originalEndpoints)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", "CNAME"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", "A"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", "CNAME"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", "A"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", "CNAME"),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
Delete: deleteRecords,
}
if err := provider.ApplyChanges("_", changes); err != nil {
t.Fatal(err)
}
records, err := provider.Records("_")
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, records, originalEndpoints)
}
func TestGoogleApplyChangesEmpty(t *testing.T) {
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{})
if err := provider.ApplyChanges("_", &plan.Changes{}); err != nil {
t.Error(err)
}
}
func TestSeparateChanges(t *testing.T) {
change := &dns.Change{
Additions: []*dns.ResourceRecordSet{
{Name: "qux.foo.example.org.", Ttl: 1},
{Name: "qux.bar.example.org.", Ttl: 2},
},
Deletions: []*dns.ResourceRecordSet{
{Name: "wambo.foo.example.org.", Ttl: 10},
{Name: "wambo.bar.example.org.", Ttl: 20},
},
}
zones := map[string]*dns.ManagedZone{
"foo-example-org": {
Name: "foo-example-org",
DnsName: "foo.example.org.",
},
"bar-example-org": {
Name: "bar-example-org",
DnsName: "bar.example.org.",
},
"baz-example-org": {
Name: "baz-example-org",
DnsName: "baz.example.org.",
},
}
changes := separateChange(zones, change)
if len(changes) != 2 {
t.Fatalf("expected %d change(s), got %d", 2, len(changes))
}
validateChange(t, changes["foo-example-org"], &dns.Change{
Additions: []*dns.ResourceRecordSet{
{Name: "qux.foo.example.org.", Ttl: 1},
},
Deletions: []*dns.ResourceRecordSet{
{Name: "wambo.foo.example.org.", Ttl: 10},
},
})
validateChange(t, changes["bar-example-org"], &dns.Change{
Additions: []*dns.ResourceRecordSet{
{Name: "qux.bar.example.org.", Ttl: 2},
},
Deletions: []*dns.ResourceRecordSet{
{Name: "wambo.bar.example.org.", Ttl: 20},
},
})
}
func TestGoogleSuitableZone(t *testing.T) {
zones := map[string]*dns.ManagedZone{
"example-org": {Name: "example-org", DnsName: "example.org."},
"bar-example-org": {Name: "bar-example-org", DnsName: "bar.example.org."},
}
for _, tc := range []struct {
hostname string
expected *dns.ManagedZone
}{
{"foo.bar.example.org.", zones["bar-example-org"]},
{"foo.example.org.", zones["example-org"]},
{"foo.kubernetes.io.", nil},
} {
suitableZone := suitableManagedZone(tc.hostname, zones)
if suitableZone != tc.expected {
t.Errorf("expected %v, got %v", tc.expected, suitableZone)
}
}
}
func validateZones(t *testing.T, zones map[string]*dns.ManagedZone, expected map[string]*dns.ManagedZone) {
if len(zones) != len(expected) {
t.Fatalf("expected %d zone(s), got %d", len(expected), len(zones))
}
for i, zone := range zones {
validateZone(t, zone, expected[i])
}
}
func validateZone(t *testing.T, zone *dns.ManagedZone, expected *dns.ManagedZone) {
if zone.Name != expected.Name {
t.Errorf("expected %s, got %s", expected.Name, zone.Name)
}
if zone.DnsName != expected.DnsName {
t.Errorf("expected %s, got %s", expected.DnsName, zone.DnsName)
}
}
func validateChange(t *testing.T, change *dns.Change, expected *dns.Change) {
validateChangeRecords(t, change.Additions, expected.Additions)
validateChangeRecords(t, change.Deletions, expected.Deletions)
}
func validateChangeRecords(t *testing.T, records []*dns.ResourceRecordSet, expected []*dns.ResourceRecordSet) {
if len(records) != len(expected) {
t.Fatalf("expected %d change(s), got %d", len(expected), len(records))
}
for i := range records {
validateChangeRecord(t, records[i], expected[i])
}
}
func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected *dns.ResourceRecordSet) {
if record.Name != expected.Name {
t.Errorf("expected %s, got %s", expected.Name, record.Name)
}
if record.Ttl != expected.Ttl {
t.Errorf("expected %d, got %d", expected.Ttl, record.Ttl)
}
}
func newGoogleProvider(t *testing.T, domain string, dryRun bool, records []*endpoint.Endpoint) *googleProvider {
provider := &googleProvider{
project: "project",
project: "zalando-external-dns-test",
domain: domain,
dryRun: false,
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
managedZonesClient: &mockManagedZonesClient{},
changesClient: &mockChangesClient{},
}
changes := &plan.Changes{}
createZone(t, provider, &dns.ManagedZone{
Name: "zone-1-ext-dns-test-2-gcp-zalan-do",
DnsName: "zone-1.ext-dns-test-2.gcp.zalan.do.",
})
err := provider.ApplyChanges("zone", changes)
createZone(t, provider, &dns.ManagedZone{
Name: "zone-2-ext-dns-test-2-gcp-zalan-do",
DnsName: "zone-2.ext-dns-test-2.gcp.zalan.do.",
})
createZone(t, provider, &dns.ManagedZone{
Name: "zone-3-ext-dns-test-2-gcp-zalan-do",
DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do.",
})
setupGoogleRecords(t, provider, records)
provider.dryRun = dryRun
return provider
}
func createZone(t *testing.T, provider *googleProvider, zone *dns.ManagedZone) {
zone.Description = "Testing zone for kubernetes.io/external-dns"
if _, err := provider.managedZonesClient.Create("zalando-external-dns-test", zone).Do(); err != nil {
if err, ok := err.(*googleapi.Error); !ok || err.Code != http.StatusConflict {
t.Fatal(err)
}
}
}
func setupGoogleRecords(t *testing.T, provider *googleProvider, endpoints []*endpoint.Endpoint) {
clearGoogleRecords(t, provider, "zone-1-ext-dns-test-2-gcp-zalan-do")
clearGoogleRecords(t, provider, "zone-2-ext-dns-test-2-gcp-zalan-do")
records, err := provider.Records("_")
if err != nil {
t.Errorf("should not fail: %s", err)
t.Fatal(err)
}
validateEndpoints(t, records, []*endpoint.Endpoint{})
if err = provider.CreateRecords(endpoints); err != nil {
t.Fatal(err)
}
records, err = provider.Records("_")
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, records, endpoints)
}
func clearGoogleRecords(t *testing.T, provider *googleProvider, zone string) {
recordSets := []*dns.ResourceRecordSet{}
if err := provider.resourceRecordSetsClient.List(provider.project, zone).Pages(context.TODO(), func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
switch r.Type {
case "A", "CNAME":
recordSets = append(recordSets, r)
}
}
return nil
}); err != nil {
t.Fatal(err)
}
if len(recordSets) != 0 {
if _, err := provider.changesClient.Create(provider.project, zone, &dns.Change{
Deletions: recordSets,
}).Do(); err != nil {
t.Fatal(err)
}
}
}

View File

@ -18,6 +18,7 @@ package provider
import (
"net"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
@ -40,3 +41,12 @@ func suitableType(ep *endpoint.Endpoint) string {
}
return "CNAME"
}
// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already.
func ensureTrailingDot(hostname string) string {
if net.ParseIP(hostname) != nil {
return hostname
}
return strings.TrimSuffix(hostname, ".") + "."
}

View File

@ -43,3 +43,19 @@ func TestSuitableType(t *testing.T) {
}
}
}
func TestEnsureTrailingDot(t *testing.T) {
for _, tc := range []struct {
input, expected string
}{
{"example.org", "example.org."},
{"example.org.", "example.org."},
{"8.8.8.8", "8.8.8.8"},
} {
output := ensureTrailingDot(tc.input)
if output != tc.expected {
t.Errorf("expected %s, got %s", tc.expected, output)
}
}
}