mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
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:
parent
5e3f2b7773
commit
e5f21ad32a
@ -1,7 +1,7 @@
|
|||||||
Features:
|
Features:
|
||||||
- Improved logging
|
- Improved logging
|
||||||
- Generate DNS Name from template for services/ingress if annotation is missing but `--fqdn-template` is specified
|
- 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.
|
- Route 53: Support creation of ALIAS records when endpoint target is a ELB/ALB.
|
||||||
- Ownership via TXT records
|
- Ownership via TXT records
|
||||||
1. Create TXT records to mark the records managed by External DNS
|
1. Create TXT records to mark the records managed by External DNS
|
||||||
|
2
main.go
2
main.go
@ -105,7 +105,7 @@ func main() {
|
|||||||
var p provider.Provider
|
var p provider.Provider
|
||||||
switch cfg.Provider {
|
switch cfg.Provider {
|
||||||
case "google":
|
case "google":
|
||||||
p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DryRun)
|
p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.Domain, cfg.DryRun)
|
||||||
case "aws":
|
case "aws":
|
||||||
p, err = provider.NewAWSProvider(cfg.Domain, cfg.DryRun)
|
p, err = provider.NewAWSProvider(cfg.Domain, cfg.DryRun)
|
||||||
default:
|
default:
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
@ -327,12 +326,3 @@ func canonicalHostedZone(hostname string) string {
|
|||||||
|
|
||||||
return ""
|
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, ".") + "."
|
|
||||||
}
|
|
||||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
@ -33,17 +35,12 @@ type managedZonesCreateCallInterface interface {
|
|||||||
Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error)
|
Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type managedZonesDeleteCallInterface interface {
|
|
||||||
Do(opts ...googleapi.CallOption) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type managedZonesListCallInterface interface {
|
type managedZonesListCallInterface interface {
|
||||||
Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error
|
Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type managedZonesServiceInterface interface {
|
type managedZonesServiceInterface interface {
|
||||||
Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface
|
Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface
|
||||||
Delete(project string, managedZone string) managedZonesDeleteCallInterface
|
|
||||||
List(project string) managedZonesListCallInterface
|
List(project string) managedZonesListCallInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +76,6 @@ func (m managedZonesService) Create(project string, managedzone *dns.ManagedZone
|
|||||||
return m.service.Create(project, 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 {
|
func (m managedZonesService) List(project string) managedZonesListCallInterface {
|
||||||
return m.service.List(project)
|
return m.service.List(project)
|
||||||
}
|
}
|
||||||
@ -101,6 +94,8 @@ type googleProvider struct {
|
|||||||
project string
|
project string
|
||||||
// Enabled dry-run will print any modifying actions rather than execute them.
|
// Enabled dry-run will print any modifying actions rather than execute them.
|
||||||
dryRun bool
|
dryRun bool
|
||||||
|
// only consider hosted zones managing domains ending in this suffix
|
||||||
|
domain string
|
||||||
// A client for managing resource record sets
|
// A client for managing resource record sets
|
||||||
resourceRecordSetsClient resourceRecordSetsClientInterface
|
resourceRecordSetsClient resourceRecordSetsClientInterface
|
||||||
// A client for managing hosted zones
|
// A client for managing hosted zones
|
||||||
@ -110,7 +105,7 @@ type googleProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewGoogleProvider initializes a new Google CloudDNS based Provider.
|
// 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)
|
gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -123,6 +118,7 @@ func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
|
|||||||
|
|
||||||
provider := &googleProvider{
|
provider := &googleProvider{
|
||||||
project: project,
|
project: project,
|
||||||
|
domain: domain,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
|
resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
|
||||||
managedZonesClient: managedZonesService{dnsClient.ManagedZones},
|
managedZonesClient: managedZonesService{dnsClient.ManagedZones},
|
||||||
@ -133,49 +129,33 @@ func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Zones returns the list of hosted zones.
|
// 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 {
|
f := func(resp *dns.ManagedZonesListResponse) error {
|
||||||
// each page is processed sequentially, no need for a mutex here.
|
for _, zone := range resp.ManagedZones {
|
||||||
zones = append(zones, resp.ManagedZones...)
|
if strings.HasSuffix(zone.DnsName, p.domain) {
|
||||||
|
zones[zone.Name] = zone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f)
|
if err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return zones, nil
|
return zones, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateZone creates a hosted zone given a name.
|
// Records returns the list of records in all relevant zones.
|
||||||
func (p *googleProvider) CreateZone(name, domain string) error {
|
func (p *googleProvider) Records(_ string) (endpoints []*endpoint.Endpoint, _ error) {
|
||||||
zone := &dns.ManagedZone{
|
zones, err := p.Zones()
|
||||||
Name: name,
|
|
||||||
DnsName: domain,
|
|
||||||
Description: "Automatically managed zone by kubernetes.io/external-dns",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := p.managedZonesClient.Create(p.project, zone).Do()
|
|
||||||
if err != nil {
|
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 {
|
f := func(resp *dns.ResourceRecordSetsListResponse) error {
|
||||||
for _, r := range resp.Rrsets {
|
for _, r := range resp.Rrsets {
|
||||||
// TODO(linki, ownership): Remove once ownership system is in place.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.resourceRecordSetsClient.List(p.project, zone).Pages(context.TODO(), f)
|
for _, z := range zones {
|
||||||
if err != nil {
|
if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(context.TODO(), f); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRecords creates a given set of DNS records in the given hosted zone.
|
// 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 := &dns.Change{}
|
||||||
|
|
||||||
change.Additions = append(change.Additions, newRecords(endpoints)...)
|
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.
|
// 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 := &dns.Change{}
|
||||||
|
|
||||||
change.Additions = append(change.Additions, newRecords(records)...)
|
change.Additions = append(change.Additions, newRecords(records)...)
|
||||||
change.Deletions = append(change.Deletions, newRecords(oldRecords)...)
|
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.
|
// 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 := &dns.Change{}
|
||||||
|
|
||||||
change.Deletions = append(change.Deletions, newRecords(endpoints)...)
|
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.
|
// 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 := &dns.Change{}
|
||||||
|
|
||||||
change.Additions = append(change.Additions, newRecords(changes.Create)...)
|
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)...)
|
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.
|
// 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 {
|
if len(change.Additions) == 0 && len(change.Deletions) == 0 {
|
||||||
log.Infoln("Received empty list of records for creation and deletion")
|
log.Infoln("Received empty list of records for creation and deletion")
|
||||||
return nil
|
return nil
|
||||||
@ -261,9 +241,20 @@ func (p *googleProvider) submitChange(zone string, change *dns.Change) error {
|
|||||||
log.Infof("Add records: %s %s %s", add.Name, add.Type, add.Rrdatas)
|
log.Infof("Add records: %s %s %s", add.Name, add.Type, add.Rrdatas)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.dryRun {
|
if p.dryRun {
|
||||||
_, err := p.changesClient.Create(p.project, zone, change).Do()
|
return nil
|
||||||
if err != 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 err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,6 +262,54 @@ func (p *googleProvider) submitChange(zone string, change *dns.Change) error {
|
|||||||
return nil
|
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.
|
// newRecords returns a collection of RecordSets based on the given endpoints.
|
||||||
func newRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet {
|
func newRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet {
|
||||||
records := make([]*dns.ResourceRecordSet, len(endpoints))
|
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.
|
// newRecord returns a RecordSet based on the given endpoint.
|
||||||
func newRecord(endpoint *endpoint.Endpoint) *dns.ResourceRecordSet {
|
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{
|
return &dns.ResourceRecordSet{
|
||||||
Name: ensureTrailingDot(endpoint.DNSName),
|
Name: ensureTrailingDot(endpoint.DNSName),
|
||||||
Rrdatas: []string{ensureTrailingDot(endpoint.Target)},
|
Rrdatas: []string{target},
|
||||||
Ttl: 300,
|
Ttl: 300,
|
||||||
Type: suitableType(endpoint),
|
Type: suitableType(endpoint),
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
@ -25,320 +27,603 @@ import (
|
|||||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
"github.com/kubernetes-incubator/external-dns/plan"
|
"github.com/kubernetes-incubator/external-dns/plan"
|
||||||
|
|
||||||
dns "google.golang.org/api/dns/v1"
|
"google.golang.org/api/dns/v1"
|
||||||
googleapi "google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
expectedZones = []*dns.ManagedZone{{Name: "expected"}}
|
testZones = map[string]*dns.ManagedZone{}
|
||||||
expectedRecordSets = []*dns.ResourceRecordSet{
|
testRecords = map[string]map[string]*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"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockManagedZonesCreateCall struct{}
|
type mockManagedZonesCreateCall struct {
|
||||||
|
project string
|
||||||
|
managedZone *dns.ManagedZone
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) {
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
testZones[zoneKey] = m.managedZone
|
||||||
|
|
||||||
|
return m.managedZone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockErrManagedZonesCreateCall struct{}
|
type mockManagedZonesListCall struct {
|
||||||
|
project string
|
||||||
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 {
|
func (m *mockManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error {
|
||||||
return f(&dns.ManagedZonesListResponse{ManagedZones: expectedZones})
|
zones := []*dns.ManagedZone{}
|
||||||
}
|
|
||||||
|
|
||||||
type mockErrManagedZonesListCall struct{}
|
for k, v := range testZones {
|
||||||
|
if strings.HasPrefix(k, m.project+"/") {
|
||||||
|
zones = append(zones, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockErrManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error {
|
return f(&dns.ManagedZonesListResponse{ManagedZones: zones})
|
||||||
return fmt.Errorf("failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockManagedZonesClient struct{}
|
type mockManagedZonesClient struct{}
|
||||||
|
|
||||||
func (m *mockManagedZonesClient) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
|
func (m *mockManagedZonesClient) Create(project string, managedZone *dns.ManagedZone) managedZonesCreateCallInterface {
|
||||||
return &mockManagedZonesCreateCall{}
|
return &mockManagedZonesCreateCall{project: project, managedZone: managedZone}
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockManagedZonesClient) Delete(project string, managedZone string) managedZonesDeleteCallInterface {
|
|
||||||
return &mockManagedZonesDeleteCall{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockManagedZonesClient) List(project string) managedZonesListCallInterface {
|
func (m *mockManagedZonesClient) List(project string) managedZonesListCallInterface {
|
||||||
return &mockManagedZonesListCall{}
|
return &mockManagedZonesListCall{project: project}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockErrManagedZonesClient struct{}
|
type mockResourceRecordSetsListCall struct {
|
||||||
|
project string
|
||||||
func (m *mockErrManagedZonesClient) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
|
managedZone string
|
||||||
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 {
|
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)
|
||||||
}
|
|
||||||
|
|
||||||
type mockErrResourceRecordSetsListCall struct{}
|
if _, ok := testZones[zoneKey]; !ok {
|
||||||
|
return &googleapi.Error{Code: http.StatusNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockErrResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error {
|
resp := []*dns.ResourceRecordSet{}
|
||||||
return fmt.Errorf("failed")
|
|
||||||
|
for _, v := range testRecords[zoneKey] {
|
||||||
|
resp = append(resp, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(&dns.ResourceRecordSetsListResponse{Rrsets: resp})
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockResourceRecordSetsClient struct{}
|
type mockResourceRecordSetsClient struct{}
|
||||||
|
|
||||||
func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
|
func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
|
||||||
return &mockResourceRecordSetsListCall{}
|
return &mockResourceRecordSetsListCall{project: project, managedZone: managedZone}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockErrResourceRecordSetsClient struct{}
|
type mockChangesCreateCall struct {
|
||||||
|
project string
|
||||||
func (m *mockErrResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
|
managedZone string
|
||||||
return &mockErrResourceRecordSetsListCall{}
|
change *dns.Change
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockChangesCreateCall struct{}
|
|
||||||
|
|
||||||
func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) {
|
func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) {
|
||||||
return nil, nil
|
zoneKey := zoneKey(m.project, m.managedZone)
|
||||||
}
|
|
||||||
|
|
||||||
type mockErrChangesCreateCall struct{}
|
if _, ok := testZones[zoneKey]; !ok {
|
||||||
|
return nil, &googleapi.Error{Code: http.StatusNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockErrChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, error) {
|
if _, ok := testRecords[zoneKey]; !ok {
|
||||||
return nil, fmt.Errorf("failed")
|
testRecords[zoneKey] = make(map[string]*dns.ResourceRecordSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}
|
type mockChangesClient struct{}
|
||||||
|
|
||||||
func (m *mockChangesClient) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface {
|
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 {
|
func recordKey(recordType, recordName string) string {
|
||||||
return &mockErrChangesCreateCall{}
|
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) {
|
func TestGoogleZones(t *testing.T) {
|
||||||
provider := &googleProvider{
|
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{})
|
||||||
project: "project",
|
|
||||||
managedZonesClient: &mockManagedZonesClient{},
|
|
||||||
}
|
|
||||||
|
|
||||||
zones, err := provider.Zones()
|
zones, err := provider.Zones()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(zones) != len(expectedZones) {
|
validateZones(t, zones, map[string]*dns.ManagedZone{
|
||||||
t.Errorf("expected %d zones, got %d", len(expectedZones), len(zones))
|
"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."},
|
||||||
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) {
|
func TestGoogleRecords(t *testing.T) {
|
||||||
provider := &googleProvider{
|
originalEndpoints := []*endpoint.Endpoint{
|
||||||
project: "project",
|
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
|
||||||
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
|
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 {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(endpoints) != len(expectedRecordSets)-1 {
|
validateEndpoints(t, records, originalEndpoints)
|
||||||
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) {
|
func TestGoogleCreateRecords(t *testing.T) {
|
||||||
provider := &googleProvider{
|
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{})
|
||||||
project: "project",
|
|
||||||
changesClient: &mockChangesClient{},
|
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{
|
if err := provider.CreateRecords(records); err != nil {
|
||||||
{
|
t.Fatal(err)
|
||||||
DNSName: "dns-name",
|
|
||||||
Target: "target",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := provider.CreateRecords("zone", endpoints)
|
records, err := provider.Records("_")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.changesClient = &mockErrChangesClient{}
|
validateEndpoints(t, records, []*endpoint.Endpoint{
|
||||||
|
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
|
||||||
err = provider.CreateRecords("zone", endpoints)
|
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
|
||||||
if err == nil {
|
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", "CNAME"),
|
||||||
t.Errorf("expected error")
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGoogleUpdateRecords(t *testing.T) {
|
func TestGoogleUpdateRecords(t *testing.T) {
|
||||||
provider := &googleProvider{
|
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{
|
||||||
project: "project",
|
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
|
||||||
changesClient: &mockChangesClient{},
|
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{
|
if err := provider.UpdateRecords(updatedRecords, currentRecords); err != nil {
|
||||||
{
|
t.Fatal(err)
|
||||||
DNSName: "dns-name",
|
|
||||||
Target: "target",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oldRecords := []*endpoint.Endpoint{
|
records, err := provider.Records("_")
|
||||||
{
|
|
||||||
DNSName: "dns-name",
|
|
||||||
Target: "target",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := provider.UpdateRecords("zone", records, oldRecords)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.UpdateRecords("zone", nil, nil)
|
validateEndpoints(t, records, []*endpoint.Endpoint{
|
||||||
if err != nil {
|
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
|
||||||
t.Errorf("should not fail: %s", err)
|
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"),
|
||||||
|
})
|
||||||
provider.dryRun = true
|
|
||||||
|
|
||||||
err = provider.UpdateRecords("zone", records, oldRecords)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("should not fail: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGoogleDeleteRecords(t *testing.T) {
|
func TestGoogleDeleteRecords(t *testing.T) {
|
||||||
provider := &googleProvider{
|
originalEndpoints := []*endpoint.Endpoint{
|
||||||
project: "project",
|
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", "A"),
|
||||||
changesClient: &mockChangesClient{},
|
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{
|
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, originalEndpoints)
|
||||||
{
|
|
||||||
DNSName: "dns-name",
|
if err := provider.DeleteRecords(originalEndpoints); err != nil {
|
||||||
Target: "target",
|
t.Fatal(err)
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := provider.DeleteRecords("zone", endpoints)
|
records, err := provider.Records("_")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateEndpoints(t, records, []*endpoint.Endpoint{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGoogleApplyChanges(t *testing.T) {
|
func TestGoogleApplyChanges(t *testing.T) {
|
||||||
provider := &googleProvider{
|
provider := newGoogleProvider(t, "ext-dns-test-2.gcp.zalan.do.", false, []*endpoint.Endpoint{
|
||||||
project: "project",
|
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", "A"),
|
||||||
changesClient: &mockChangesClient{},
|
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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := &plan.Changes{}
|
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"),
|
||||||
|
}
|
||||||
|
|
||||||
err := provider.ApplyChanges("zone", changes)
|
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 {
|
if err != nil {
|
||||||
t.Errorf("should not fail: %s", err)
|
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: "zalando-external-dns-test",
|
||||||
|
domain: domain,
|
||||||
|
dryRun: false,
|
||||||
|
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
|
||||||
|
managedZonesClient: &mockManagedZonesClient{},
|
||||||
|
changesClient: &mockChangesClient{},
|
||||||
|
}
|
||||||
|
|
||||||
|
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.",
|
||||||
|
})
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
"github.com/kubernetes-incubator/external-dns/plan"
|
"github.com/kubernetes-incubator/external-dns/plan"
|
||||||
@ -40,3 +41,12 @@ func suitableType(ep *endpoint.Endpoint) string {
|
|||||||
}
|
}
|
||||||
return "CNAME"
|
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, ".") + "."
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user