From 6b2b691807e5c319e156a142cf50cc82e06860f0 Mon Sep 17 00:00:00 2001 From: Mikkel Oscar Lyderik Larsen Date: Sat, 11 Mar 2017 20:19:37 +0100 Subject: [PATCH] Add tests for Google DNS provider --- dnsprovider/google.go | 133 ++++++++++++--- dnsprovider/google_test.go | 338 +++++++++++++++++++++++++++++++++++++ main.go | 19 +-- 3 files changed, 450 insertions(+), 40 deletions(-) create mode 100644 dnsprovider/google_test.go diff --git a/dnsprovider/google.go b/dnsprovider/google.go index 0a390fd756..108af503fc 100644 --- a/dnsprovider/google.go +++ b/dnsprovider/google.go @@ -22,35 +22,124 @@ import ( log "github.com/Sirupsen/logrus" + "golang.org/x/oauth2/google" "google.golang.org/api/dns/v1" + googleapi "google.golang.org/api/googleapi" "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" ) -// GoogleProvider is an implementation of DNSProvider for Google CloudDNS. -type GoogleProvider struct { +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 +} + +type resourceRecordSetsListCallInterface interface { + Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error +} + +type resourceRecordSetsClientInterface interface { + List(project string, managedZone string) resourceRecordSetsListCallInterface +} + +type changesCreateCallInterface interface { + Do(opts ...googleapi.CallOption) (*dns.Change, error) +} + +type changesServiceInterface interface { + Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface +} + +type resourceRecordSetsService struct { + *dns.ResourceRecordSetsService +} + +func (r resourceRecordSetsService) List(project string, managedZone string) resourceRecordSetsListCallInterface { + return r.List(project, managedZone) +} + +type managedZonesService struct { + *dns.ManagedZonesService +} + +func (m managedZonesService) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface { + return m.Create(project, managedzone) +} + +func (m managedZonesService) Delete(project string, managedZone string) managedZonesDeleteCallInterface { + return m.Delete(project, managedZone) +} + +func (m managedZonesService) List(project string) managedZonesListCallInterface { + return m.List(project) +} + +type changesService struct { + *dns.ChangesService +} + +func (c changesService) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface { + return c.Create(project, managedZone, change) +} + +// googleProvider is an implementation of DNSProvider for Google CloudDNS. +type googleProvider struct { // The Google project to work in - Project string + project string // Enabled dry-run will print any modifying actions rather than execute them. - DryRun bool + dryRun bool // A client for managing resource record sets - ResourceRecordSetsClient *dns.ResourceRecordSetsService + resourceRecordSetsClient resourceRecordSetsClientInterface // A client for managing hosted zones - ManagedZonesClient *dns.ManagedZonesService + managedZonesClient managedZonesServiceInterface // A client for managing change sets - ChangesClient *dns.ChangesService + changesClient changesServiceInterface +} + +// NewGoogleProvider initializes a new Google CloudDNS based DNSProvider. +func NewGoogleProvider(project string, dryRun bool) (DNSProvider, error) { + gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope) + if err != nil { + return nil, err + } + + dnsClient, err := dns.New(gcloud) + if err != nil { + return nil, err + } + + return &googleProvider{ + project: project, + dryRun: dryRun, + resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets}, + managedZonesClient: managedZonesService{dnsClient.ManagedZones}, + changesClient: changesService{dnsClient.Changes}, + }, nil } // Zones returns the list of hosted zones. -func (p *GoogleProvider) Zones() (zones []*dns.ManagedZone, _ error) { +func (p *googleProvider) Zones() (zones []*dns.ManagedZone, _ error) { f := func(resp *dns.ManagedZonesListResponse) error { // each page is processed sequentially, no need for a mutex here. zones = append(zones, resp.ManagedZones...) return nil } - err := p.ManagedZonesClient.List(p.Project).Pages(context.TODO(), f) + err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f) if err != nil { return nil, err } @@ -59,14 +148,14 @@ func (p *GoogleProvider) Zones() (zones []*dns.ManagedZone, _ error) { } // CreateZone creates a hosted zone given a name. -func (p *GoogleProvider) CreateZone(name, domain string) error { +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() + _, err := p.managedZonesClient.Create(p.project, zone).Do() if err != nil { return err } @@ -75,8 +164,8 @@ func (p *GoogleProvider) CreateZone(name, domain string) error { } // DeleteZone deletes a hosted zone given a name. -func (p *GoogleProvider) DeleteZone(name string) error { - err := p.ManagedZonesClient.Delete(p.Project, name).Do() +func (p *googleProvider) DeleteZone(name string) error { + err := p.managedZonesClient.Delete(p.project, name).Do() if err != nil { if !isNotFound(err) { return err @@ -87,7 +176,7 @@ func (p *GoogleProvider) DeleteZone(name string) error { } // Records returns the list of A records in a given hosted zone. -func (p *GoogleProvider) Records(zone string) (endpoints []endpoint.Endpoint, _ error) { +func (p *googleProvider) Records(zone string) (endpoints []endpoint.Endpoint, _ error) { f := func(resp *dns.ResourceRecordSetsListResponse) error { for _, r := range resp.Rrsets { if r.Type != "A" { @@ -106,7 +195,7 @@ func (p *GoogleProvider) Records(zone string) (endpoints []endpoint.Endpoint, _ return nil } - err := p.ResourceRecordSetsClient.List(p.Project, zone).Pages(context.TODO(), f) + err := p.resourceRecordSetsClient.List(p.project, zone).Pages(context.TODO(), f) if err != nil { return nil, err } @@ -115,7 +204,7 @@ func (p *GoogleProvider) Records(zone string) (endpoints []endpoint.Endpoint, _ } // 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(zone string, endpoints []endpoint.Endpoint) error { change := &dns.Change{} change.Additions = append(change.Additions, newRecords(endpoints)...) @@ -124,7 +213,7 @@ func (p *GoogleProvider) CreateRecords(zone string, endpoints []endpoint.Endpoin } // 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(zone string, records, oldRecords []endpoint.Endpoint) error { change := &dns.Change{} change.Additions = append(change.Additions, newRecords(records)...) @@ -134,7 +223,7 @@ func (p *GoogleProvider) UpdateRecords(zone string, records, oldRecords []endpoi } // 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(zone string, endpoints []endpoint.Endpoint) error { change := &dns.Change{} change.Deletions = append(change.Deletions, newRecords(endpoints)...) @@ -143,7 +232,7 @@ func (p *GoogleProvider) DeleteRecords(zone string, endpoints []endpoint.Endpoin } // 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(zone string, changes *plan.Changes) error { change := &dns.Change{} change.Additions = append(change.Additions, newRecords(changes.Create)...) @@ -157,8 +246,8 @@ func (p *GoogleProvider) ApplyChanges(zone string, changes *plan.Changes) error } // submitChange takes a zone and a Change and sends it to Google. -func (p *GoogleProvider) submitChange(zone string, change *dns.Change) error { - if p.DryRun { +func (p *googleProvider) submitChange(zone string, change *dns.Change) error { + if p.dryRun { for _, add := range change.Additions { log.Infof("Add records: %s %s %s", add.Name, add.Type, add.Rrdatas) } @@ -174,7 +263,7 @@ func (p *GoogleProvider) submitChange(zone string, change *dns.Change) error { return nil } - _, err := p.ChangesClient.Create(p.Project, zone, change).Do() + _, err := p.changesClient.Create(p.project, zone, change).Do() if err != nil { if !isNotFound(err) { return err diff --git a/dnsprovider/google_test.go b/dnsprovider/google_test.go new file mode 100644 index 0000000000..633ddfe863 --- /dev/null +++ b/dnsprovider/google_test.go @@ -0,0 +1,338 @@ +/* +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 dnsprovider + +import ( + "context" + "fmt" + "testing" + + "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" +) + +var ( + expectedZones = []*dns.ManagedZone{{Name: "expected"}} + expectedRecordSets = []*dns.ResourceRecordSet{ + { + Type: "A", + Name: "expected", + Rrdatas: []string{"target"}, + }, + { + Type: "CNAME", + Name: "unexpected", + Rrdatas: []string{"target"}, + }, + } +) + +type mockManagedZonesCreateCall struct{} + +func (m *mockManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) { + return nil, nil +} + +type mockErrManagedZonesCreateCall struct{} + +func (m *mockErrManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) { + return nil, fmt.Errorf("failed") +} + +type mockManagedZonesDeleteCall struct{} + +func (m *mockManagedZonesDeleteCall) Do(opts ...googleapi.CallOption) error { + return nil +} + +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}) +} + +type mockErrManagedZonesListCall struct{} + +func (m *mockErrManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error { + return fmt.Errorf("failed") +} + +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) List(project string) managedZonesListCallInterface { + return &mockManagedZonesListCall{} +} + +type mockErrManagedZonesClient struct{} + +func (m *mockErrManagedZonesClient) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface { + return &mockErrManagedZonesCreateCall{} +} + +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}) +} + +type mockErrResourceRecordSetsListCall struct{} + +func (m *mockErrResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error { + return fmt.Errorf("failed") +} + +type mockResourceRecordSetsClient struct{} + +func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface { + return &mockResourceRecordSetsListCall{} +} + +type mockErrResourceRecordSetsClient struct{} + +func (m *mockErrResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface { + return &mockErrResourceRecordSetsListCall{} +} + +type mockChangesCreateCall struct{} + +func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) { + return nil, nil +} + +type mockErrChangesCreateCall struct{} + +func (m *mockErrChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) { + return nil, fmt.Errorf("failed") +} + +type mockChangesClient struct{} + +func (m *mockChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface { + return &mockChangesCreateCall{} +} + +type mockErrChangesClient struct{} + +func (m *mockErrChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface { + return &mockErrChangesCreateCall{} +} + +func TestGoogleZones(t *testing.T) { + provider := &googleProvider{ + project: "project", + managedZonesClient: &mockManagedZonesClient{}, + } + + zones, err := provider.Zones() + if err != nil { + t.Errorf("should not fail: %s", 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") + } +} + +func TestGoogleRecords(t *testing.T) { + provider := &googleProvider{ + project: "project", + resourceRecordSetsClient: &mockResourceRecordSetsClient{}, + } + + endpoints, err := provider.Records("zone") + if err != nil { + t.Errorf("should not fail: %s", 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") + } +} + +func TestGoogleCreateRecords(t *testing.T) { + provider := &googleProvider{ + project: "project", + changesClient: &mockChangesClient{}, + } + + endpoints := []endpoint.Endpoint{ + { + DNSName: "dns-name", + Target: "target", + }, + } + + err := provider.CreateRecords("zone", endpoints) + if err != nil { + t.Errorf("should not fail: %s", err) + } + + provider.changesClient = &mockErrChangesClient{} + + err = provider.CreateRecords("zone", endpoints) + if err == nil { + t.Errorf("expected error") + } +} + +func TestGoogleUpdateRecords(t *testing.T) { + provider := &googleProvider{ + project: "project", + changesClient: &mockChangesClient{}, + } + + records := []endpoint.Endpoint{ + { + DNSName: "dns-name", + Target: "target", + }, + } + + oldRecords := []endpoint.Endpoint{ + { + DNSName: "dns-name", + Target: "target", + }, + } + + err := provider.UpdateRecords("zone", records, oldRecords) + if err != nil { + t.Errorf("should not fail: %s", 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) + } +} + +func TestGoogleDeleteRecords(t *testing.T) { + provider := &googleProvider{ + project: "project", + changesClient: &mockChangesClient{}, + } + + endpoints := []endpoint.Endpoint{ + { + DNSName: "dns-name", + Target: "target", + }, + } + + err := provider.DeleteRecords("zone", endpoints) + if err != nil { + t.Errorf("should not fail: %s", err) + } +} + +func TestGoogleApplyChanges(t *testing.T) { + provider := &googleProvider{ + project: "project", + changesClient: &mockChangesClient{}, + } + + changes := &plan.Changes{} + + err := provider.ApplyChanges("zone", changes) + if err != nil { + t.Errorf("should not fail: %s", err) + } +} diff --git a/main.go b/main.go index 9219a4a083..985eb34a58 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,6 @@ limitations under the License. package main import ( - "context" "fmt" "net/http" "os" @@ -27,9 +26,6 @@ import ( log "github.com/Sirupsen/logrus" - "golang.org/x/oauth2/google" - "google.golang.org/api/dns/v1" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -83,24 +79,11 @@ func main() { Namespace: cfg.Namespace, } - gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope) + dnsProvider, err := dnsprovider.NewGoogleProvider(cfg.GoogleProject, cfg.DryRun) if err != nil { log.Fatal(err) } - dnsClient, err := dns.New(gcloud) - if err != nil { - log.Fatal(err) - } - - dnsProvider := &dnsprovider.GoogleProvider{ - Project: cfg.GoogleProject, - DryRun: cfg.DryRun, - ResourceRecordSetsClient: dnsClient.ResourceRecordSets, - ManagedZonesClient: dnsClient.ManagedZones, - ChangesClient: dnsClient.Changes, - } - ctrl := controller.Controller{ Zone: cfg.GoogleZone, Source: source,