From 4cfd9606725d520c23b3ad6787d993eae5060a11 Mon Sep 17 00:00:00 2001 From: Peter Stokes Date: Thu, 21 Nov 2024 10:20:06 +0000 Subject: [PATCH] Add routing policy support to Google provider. --- docs/tutorials/google.md | 54 +++ main.go | 2 +- pkg/apis/externaldns/types.go | 3 + pkg/apis/externaldns/types_test.go | 4 + provider/google/google.go | 319 ++++++++++++++++-- provider/google/google_test.go | 510 ++++++++++++++++++++++++++--- source/source.go | 6 + 7 files changed, 812 insertions(+), 86 deletions(-) create mode 100644 docs/tutorials/google.md diff --git a/docs/tutorials/google.md b/docs/tutorials/google.md new file mode 100644 index 0000000000..2370cf6ab8 --- /dev/null +++ b/docs/tutorials/google.md @@ -0,0 +1,54 @@ +## Annotations + +Annotations which are specific to the Google +[CloudDNS](https://cloud.google.com/dns/docs/overview) provider. + +### Routing Policy + +The [routing policy](https://cloud.google.com/dns/docs/routing-policies-overview) +for resource record sets managed by ExternalDNS may be specified by applying the +`external-dns.alpha.kubernetes.io/google-routing-policy` annotation on any of the +supported [sources](../sources/about.md). + +#### Geolocation routing policies + +Specifying a value of `geo` for the `external-dns.alpha.kubernetes.io/google-routing-policy` +annotation will enable geolocation routing for associated resource record sets. The +location attributed to resource record sets may be deduced for instances of ExternalDNS +running within the Google Cloud platform or may be specified via the `--google-location` +command-line argument. Alternatively, a location may be explicitly specified via the +`external-dns.alpha.kubernetes.io/google-location` annotation, where the value is one +of Google Cloud's [locations/regions](https://cloud.google.com/docs/geography-and-regions). + +For example: +```yaml +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: geo-example + annotations: + external-dns.alpha.kubernetes.io/google-routing-policy: "geo" + external-dns.alpha.kubernetes.io/google-location: "us-east1" +``` + +#### Weighted Round Robin routing policies + +Specifying a value of `wrr` for the `external-dns.alpha.kubernetes.io/google-routing-policy` +annotation will enable weighted round-robin routing for associated resource record sets. +The weight to be attributed to resource record sets may be specified via the +`external-dns.alpha.kubernetes.io/google-weight` annotation, where the value is a string +representation of a floating-point number. The `external-dns.alpha.kubernetes.io/set-identifier` +annotation must also be applied providing a string value representation of an index into +the list of potential responses. + +For example: +```yaml +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: wrr-example + annotations: + external-dns.alpha.kubernetes.io/google-routing-policy: "wrr" + external-dns.alpha.kubernetes.io/google-weight: "100.0" + external-dns.alpha.kubernetes.io/set-identifier: "0" +``` \ No newline at end of file diff --git a/main.go b/main.go index 7d32da4a94..a737be38ee 100644 --- a/main.go +++ b/main.go @@ -248,7 +248,7 @@ func main() { case "cloudflare": p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage, cfg.CloudflareRegionKey) case "google": - p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) + p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, cfg.GoogleLocation, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) case "digitalocean": p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize) case "ovh": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 6cd78ee593..e46afc277e 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -69,6 +69,7 @@ type Config struct { Provider string ProviderCacheTime time.Duration GoogleProject string + GoogleLocation string GoogleBatchChangeSize int GoogleBatchChangeInterval time.Duration GoogleZoneVisibility string @@ -234,6 +235,7 @@ var defaultConfig = &Config{ Provider: "", ProviderCacheTime: 0, GoogleProject: "", + GoogleLocation: "", GoogleBatchChangeSize: 1000, GoogleBatchChangeInterval: time.Second, GoogleZoneVisibility: "", @@ -463,6 +465,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) + app.Flag("google-location", "When using the Google provider, current location is auto-detected, when running on GCP. Specify location with this. May be specified when running outside GCP.").Default(defaultConfig.GoogleLocation).StringVar(&cfg.GoogleLocation) app.Flag("google-batch-change-size", "When using the Google provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.GoogleBatchChangeSize)).IntVar(&cfg.GoogleBatchChangeSize) app.Flag("google-batch-change-interval", "When using the Google provider, set the interval between batch changes.").Default(defaultConfig.GoogleBatchChangeInterval.String()).DurationVar(&cfg.GoogleBatchChangeInterval) app.Flag("google-zone-visibility", "When using the Google provider, filter for zones with this visibility (optional, options: public, private)").Default(defaultConfig.GoogleZoneVisibility).EnumVar(&cfg.GoogleZoneVisibility, "", "public", "private") diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index ab77cc9ec8..2eda4d7aab 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -43,6 +43,7 @@ var ( Compatibility: "", Provider: "google", GoogleProject: "", + GoogleLocation: "", GoogleBatchChangeSize: 1000, GoogleBatchChangeInterval: time.Second, GoogleZoneVisibility: "", @@ -142,6 +143,7 @@ var ( Compatibility: "mate", Provider: "google", GoogleProject: "project", + GoogleLocation: "location", GoogleBatchChangeSize: 100, GoogleBatchChangeInterval: time.Second * 2, GoogleZoneVisibility: "private", @@ -271,6 +273,7 @@ func TestParseFlags(t *testing.T) { "--compatibility=mate", "--provider=google", "--google-project=project", + "--google-location=location", "--google-batch-change-size=100", "--google-batch-change-interval=2s", "--google-zone-visibility=private", @@ -391,6 +394,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_COMPATIBILITY": "mate", "EXTERNAL_DNS_PROVIDER": "google", "EXTERNAL_DNS_GOOGLE_PROJECT": "project", + "EXTERNAL_DNS_GOOGLE_LOCATION": "location", "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s", "EXTERNAL_DNS_GOOGLE_ZONE_VISIBILITY": "private", diff --git a/provider/google/google.go b/provider/google/google.go index 8c7d440b55..25debbb0ae 100644 --- a/provider/google/google.go +++ b/provider/google/google.go @@ -18,8 +18,11 @@ package google import ( "context" + "encoding/json" "fmt" + "net/http" "slices" + "strconv" "strings" "time" @@ -38,6 +41,13 @@ import ( const ( googleRecordTTL = 300 + + providerSpecificRoutingPolicy = "google-routing-policy" + providerSpecificRoutingPolicyNone = "" + providerSpecificRoutingPolicyGeo = "geo" + providerSpecificRoutingPolicyWrr = "wrr" + providerSpecificLocation = "google-location" + providerSpecificWeight = "google-weight" ) type managedZonesCreateCallInterface interface { @@ -53,11 +63,16 @@ type managedZonesServiceInterface interface { List() managedZonesListCallInterface } +type resourceRecordSetsGetCallInterface interface { + Do(opts ...googleapi.CallOption) (*dns.ResourceRecordSet, error) +} + type resourceRecordSetsListCallInterface interface { Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error } type resourceRecordSetsClientInterface interface { + Get(managedZone string, name string, type_ string) resourceRecordSetsGetCallInterface List(managedZone string) resourceRecordSetsListCallInterface } @@ -74,6 +89,10 @@ type resourceRecordSetsService struct { service *dns.ResourceRecordSetsService } +func (r resourceRecordSetsService) Get(managedZone string, name string, type_ string) resourceRecordSetsGetCallInterface { + return r.service.Get(r.project, managedZone, name, type_) +} + func (r resourceRecordSetsService) List(managedZone string) resourceRecordSetsListCallInterface { return r.service.List(r.project, managedZone) } @@ -103,6 +122,8 @@ func (c changesService) Create(managedZone string, change *dns.Change) changesCr // GoogleProvider is an implementation of Provider for Google CloudDNS. type GoogleProvider struct { provider.BaseProvider + // The default location to use + location string // Enabled dry-run will print any modifying actions rather than execute them. dryRun bool // Max batch size to submit to Google Cloud DNS per transaction. @@ -126,7 +147,7 @@ type GoogleProvider struct { } // NewGoogleProvider initializes a new Google CloudDNS based Provider. -func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, zoneVisibility string, dryRun bool) (*GoogleProvider, error) { +func NewGoogleProvider(ctx context.Context, project string, location string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, zoneVisibility string, dryRun bool) (*GoogleProvider, error) { gcloud, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope) if err != nil { return nil, err @@ -153,6 +174,14 @@ func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoin project = mProject } + if location == "" { + if zone, err := metadata.ZoneWithContext(ctx); err != nil { + parts := strings.Split(zone, "-") + location = strings.Join(parts[:len(parts)-1], "-") + log.Infof("Google location auto-detected: %s", location) + } + } + zoneTypeFilter := provider.NewZoneTypeFilter(zoneVisibility) provider := &GoogleProvider{ @@ -162,6 +191,7 @@ func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoin domainFilter: domainFilter, zoneTypeFilter: zoneTypeFilter, zoneIDFilter: zoneIDFilter, + location: location, managedZonesClient: managedZonesService{ project: project, service: dnsClient.ManagedZones, @@ -229,6 +259,25 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End if !p.SupportedRecordType(r.Type) { continue } + if r.RoutingPolicy != nil { + if r.RoutingPolicy.Geo != nil { + for _, item := range r.RoutingPolicy.Geo.Items { + ep := endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), item.Rrdatas...) + ep.WithProviderSpecific(providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo) + ep.WithProviderSpecific(providerSpecificLocation, item.Location) + endpoints = append(endpoints, ep.WithSetIdentifier(item.Location)) + } + } + if r.RoutingPolicy.Wrr != nil { + for index, item := range r.RoutingPolicy.Wrr.Items { + ep := endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), item.Rrdatas...) + ep.WithProviderSpecific(providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr) + ep.WithProviderSpecific(providerSpecificWeight, strconv.FormatFloat(item.Weight, 'g', 2, 64)) + endpoints = append(endpoints, ep.WithSetIdentifier(strconv.FormatInt(int64(index), 10))) + } + } + continue + } endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) } @@ -244,6 +293,36 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End return endpoints, nil } +// AdjustEndpoints augments Endpoints generated by various sources to be equivalent to those returned by Records +func (p *GoogleProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { + for _, ep := range endpoints { + if routingPolicy, ok := ep.GetProviderSpecificProperty(providerSpecificRoutingPolicy); ok { + switch routingPolicy { + case providerSpecificRoutingPolicyGeo: + location, ok := ep.GetProviderSpecificProperty(providerSpecificLocation) + if !ok && p.location != "" { + ep.WithProviderSpecific(providerSpecificLocation, p.location) + location = p.location + } + ep.WithSetIdentifier(location) + case providerSpecificRoutingPolicyWrr: + weight, ok := ep.GetProviderSpecificProperty(providerSpecificWeight) + if !ok { + weight = "100" + } + if weight, err := strconv.ParseFloat(weight, 64); err == nil { + ep.WithProviderSpecific(providerSpecificWeight, strconv.FormatFloat(weight, 'g', 2, 64)) + } + if index, err := strconv.ParseInt(ep.SetIdentifier, 10, 64); err != nil || index < 0 { + resource := ep.Labels[endpoint.ResourceLabelKey] + log.Warnf("Endpoint generated from '%s' has 'wrr' routing policy with non-integer set identifier '%s'", resource, ep.SetIdentifier) + } + } + } + } + return endpoints, nil +} + // ApplyChanges applies a given set of changes. func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, err := p.Zones(ctx) @@ -257,7 +336,7 @@ func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes zoneBatches := map[string][]*dns.Change{} for rrSetChange := range changes.All() { if zone, _ := zoneMap.FindZone(string(rrSetChange.Name)); zone != "" { - change := p.newChange(rrSetChange) + change := p.newChange(rrSetChange, zone) changeSize := len(change.Additions) + len(change.Deletions) if changeSize == 0 { continue @@ -274,7 +353,6 @@ func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes batch.Deletions = append(batch.Deletions, change.Deletions...) } } - for zone, batches := range zoneBatches { for index, batch := range batches { log.Infof("Change zone: %v batch #%d", zone, index) @@ -295,7 +373,6 @@ func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes } } } - return nil } @@ -310,43 +387,217 @@ func (p *GoogleProvider) SupportedRecordType(recordType string) bool { } // newChange returns a DNS change based upon the given resource record set change. -func (p *GoogleProvider) newChange(rrSetChange *plan.RRSetChange) *dns.Change { +func (p *GoogleProvider) newChange(rrSetChange *plan.RRSetChange, zone string) *dns.Change { change := dns.Change{} - for index, endpoints := range [][]*endpoint.Endpoint{rrSetChange.Delete, rrSetChange.Create} { - for _, ep := range endpoints { - record := dns.ResourceRecordSet{ - Name: provider.EnsureTrailingDot(ep.DNSName), - Ttl: googleRecordTTL, - Type: ep.RecordType, + current, err := p.resourceRecordSetsClient.Get(zone, string(rrSetChange.Name), string(rrSetChange.Type)).Do() + if err != nil { + if err, ok := err.(*googleapi.Error); !ok || err.Code != http.StatusNotFound || len(rrSetChange.Delete) > 0 { + log.Errorf("Error obtaining resource record set %s %s: %v", rrSetChange.Name, rrSetChange.Type, err) + return &dns.Change{} + } + current = &dns.ResourceRecordSet{} + } else { + change.Deletions = []*dns.ResourceRecordSet{current} + } + desired := &dns.ResourceRecordSet{} + data, err := current.MarshalJSON(); + if err == nil { + err = json.Unmarshal(data, desired) + } + if err != nil { + log.Errorf("Error processing resource record set %s %s: %v", rrSetChange.Name, rrSetChange.Type, err) + return &dns.Change{} + } + for _, ep := range rrSetChange.Delete { + routingPolicy, _ := ep.GetProviderSpecificProperty(providerSpecificRoutingPolicy) + switch routingPolicy { + case providerSpecificRoutingPolicyNone: + if len(desired.Rrdatas) == len(ep.Targets) { + match := true + for _, data := range rrSetDatas(ep) { + if !slices.Contains(desired.Rrdatas, data) { + match = false + break + } + } + if match { + desired = &dns.ResourceRecordSet{} + } + } + case providerSpecificRoutingPolicyGeo: + if desired.RoutingPolicy == nil || desired.RoutingPolicy.Geo == nil { + continue + } + if location, ok := ep.GetProviderSpecificProperty(providerSpecificLocation); ok { + filter := func(item *dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem) bool { + if item.Location == location && len(item.Rrdatas) == len(ep.Targets) { + for _, data := range rrSetDatas(ep) { + if !slices.Contains(item.Rrdatas, data) { + return false + } + } + return true + } + return false + } + index := slices.IndexFunc(desired.RoutingPolicy.Geo.Items, filter) + for index != -1 { + desired.RoutingPolicy.Geo.Items = slices.Delete( + desired.RoutingPolicy.Geo.Items, index, index + 1, + ) + index = slices.IndexFunc(desired.RoutingPolicy.Geo.Items, filter) + } + if len(desired.RoutingPolicy.Geo.Items) == 0 { + desired = &dns.ResourceRecordSet{} + } } - if ep.RecordTTL.IsConfigured() { - record.Ttl = int64(ep.RecordTTL) + case providerSpecificRoutingPolicyWrr: + if desired.RoutingPolicy == nil || desired.RoutingPolicy.Wrr == nil { + continue } - // 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. - switch record.Type { - case endpoint.RecordTypeCNAME: - record.Rrdatas = []string{provider.EnsureTrailingDot(ep.Targets[0])} - case endpoint.RecordTypeMX: - fallthrough - case endpoint.RecordTypeNS: - fallthrough - case endpoint.RecordTypeSRV: - record.Rrdatas = make([]string, len(ep.Targets)) - for i, target := range ep.Targets { - record.Rrdatas[i] = provider.EnsureTrailingDot(target) + index, err := strconv.ParseInt(ep.SetIdentifier, 10, 64) + length := int64(len(desired.RoutingPolicy.Wrr.Items)) + weight, ok := ep.GetProviderSpecificProperty(providerSpecificWeight) + if ok && err == nil && index >= 0 && index < length { + weight, err := strconv.ParseFloat(weight, 64) + if err != nil { + continue + } + item := desired.RoutingPolicy.Wrr.Items[index] + if item.Weight != weight || len(item.Rrdatas) != len(ep.Targets) { + continue + } + match := true + for _, data := range rrSetDatas(ep) { + if !slices.Contains(item.Rrdatas, data) { + match = false + break + } + } + if match { + if index + 1 < length { + desired.RoutingPolicy.Wrr.Items[index] = rrSetRoutingPolicyWrrItemPlaceholder(desired.Type) + } else { + desired.RoutingPolicy.Wrr.Items = desired.RoutingPolicy.Wrr.Items[:index] + } + } + filter := func(item *dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem) bool { + return item.Weight != 0 + } + index := slices.IndexFunc(desired.RoutingPolicy.Wrr.Items, filter) + if index == -1 { + desired = &dns.ResourceRecordSet{} } - default: - record.Rrdatas = slices.Clone(ep.Targets) } - switch index { - case 0: - change.Deletions = append(change.Deletions, &record) - case 1: - change.Additions = append(change.Additions, &record) + } + } + for _, ep := range rrSetChange.Create { + desired.Name = string(rrSetChange.Name) + desired.Type = string(rrSetChange.Type) + if ep.RecordTTL.IsConfigured() { + desired.Ttl = int64(ep.RecordTTL) + } + desired.Rrdatas = rrSetDatas(ep) + routingPolicy, _ := ep.GetProviderSpecificProperty(providerSpecificRoutingPolicy) + switch routingPolicy { + case providerSpecificRoutingPolicyGeo: + if location, ok := ep.GetProviderSpecificProperty(providerSpecificLocation); ok { + if desired.RoutingPolicy == nil { + desired.RoutingPolicy = &dns.RRSetRoutingPolicy{} + } + if desired.RoutingPolicy.Geo == nil { + desired.RoutingPolicy.Geo = &dns.RRSetRoutingPolicyGeoPolicy{} + } + index := -1 + for i, item := range desired.RoutingPolicy.Geo.Items { + if item.Location == location { + index = i + break + } + } + if index == -1 { + index = len(desired.RoutingPolicy.Geo.Items) + desired.RoutingPolicy.Geo.Items = append(desired.RoutingPolicy.Geo.Items, nil) + } + desired.RoutingPolicy.Geo.Items[index] = &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: location, + Rrdatas: desired.Rrdatas, + } + desired.Rrdatas = nil + } + case providerSpecificRoutingPolicyWrr: + index, err := strconv.ParseInt(ep.SetIdentifier, 10, 64) + weight, ok := ep.GetProviderSpecificProperty(providerSpecificWeight) + if ok && err == nil && index >= 0 { + weight, err := strconv.ParseFloat(weight, 64) + if err != nil { + continue + } + if desired.RoutingPolicy == nil { + desired.RoutingPolicy = &dns.RRSetRoutingPolicy{} + } + if desired.RoutingPolicy.Wrr == nil { + desired.RoutingPolicy.Wrr = &dns.RRSetRoutingPolicyWrrPolicy{} + } + length := int64(len(desired.RoutingPolicy.Wrr.Items)) + if index >= length { + desired.RoutingPolicy.Wrr.Items = slices.Grow(desired.RoutingPolicy.Wrr.Items, int(index + 1 - length))[:index + 1] + for i := length; i < index; i++ { + desired.RoutingPolicy.Wrr.Items[i] = rrSetRoutingPolicyWrrItemPlaceholder(desired.Type) + } + } + desired.RoutingPolicy.Wrr.Items[index] = &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: weight, + Rrdatas: desired.Rrdatas, + } + desired.Rrdatas = nil } } } + if desired.Name != "" { + if desired.Ttl == 0 { + desired.Ttl = googleRecordTTL + } + change.Additions = []*dns.ResourceRecordSet{desired} + } return &change } + +// Return Resource Record Set data for given endpoint +func rrSetDatas(ep *endpoint.Endpoint) []string { + // 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. + switch ep.RecordType { + case endpoint.RecordTypeCNAME: + return []string{provider.EnsureTrailingDot(ep.Targets[0])} + case endpoint.RecordTypeMX: + fallthrough + case endpoint.RecordTypeNS: + fallthrough + case endpoint.RecordTypeSRV: + rrdatas := make([]string, len(ep.Targets)) + for i, target := range ep.Targets { + rrdatas[i] = provider.EnsureTrailingDot(target) + } + return rrdatas + default: + return slices.Clone(ep.Targets) + } +} + +// Return a Weighted Round Robin routing policy item placeholder for given resource record type +func rrSetRoutingPolicyWrrItemPlaceholder(type_ string) *dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem { + var rrdatas []string + switch type_ { + case "A": + rrdatas = []string{"0.0.0.0"} + case "AAAA": + rrdatas = []string{"::"} + default: + rrdatas = []string{"."} + } + return &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Rrdatas: rrdatas, + } +} diff --git a/provider/google/google_test.go b/provider/google/google_test.go index cdb0fe824a..c12c7d5ede 100644 --- a/provider/google/google_test.go +++ b/provider/google/google_test.go @@ -184,6 +184,40 @@ func TestGoogleRecords(t *testing.T) { Ttl: 3, Rrdatas: []string{"cname."}, }, + &dns.ResourceRecordSet{ + Name: "geo.zone-1.local.", + Type: "A", + Ttl: 10, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "location", + Rrdatas: []string{"1.0.0.0"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr.zone-1.local.", + Type: "A", + Ttl: 10, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.0.0.0"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.0.0.1"}, + }, + }, + }, + }, + }, }, &dns.ManagedZone{ Name: "zone-2", @@ -207,6 +241,21 @@ func TestGoogleRecords(t *testing.T) { validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("a.zone-1.local", endpoint.RecordTypeA, endpoint.TTL(1), "1.0.0.0"), endpoint.NewEndpointWithTTL("cname.zone-1.local", endpoint.RecordTypeCNAME, endpoint.TTL(3), "cname"), + endpoint.NewEndpointWithTTL("geo.zone-1.local", endpoint.RecordTypeA, endpoint.TTL(10), "1.0.0.0").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "location", + ).WithSetIdentifier("location"), + endpoint.NewEndpointWithTTL("wrr.zone-1.local", endpoint.RecordTypeA, endpoint.TTL(10), "1.0.0.0").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50", + ).WithSetIdentifier("0"), + endpoint.NewEndpointWithTTL("wrr.zone-1.local", endpoint.RecordTypeA, endpoint.TTL(10), "1.0.0.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50", + ).WithSetIdentifier("1"), endpoint.NewEndpointWithTTL("a.zone-2.local", endpoint.RecordTypeA, endpoint.TTL(2), "2.0.0.0"), }) } @@ -265,6 +314,44 @@ func TestGoogleRecordsFilter(t *testing.T) { }) } +func TestGoogleAdjustEndpoints(t *testing.T) { + p := newGoogleProvider() + p.location = "default" + endpoints, err := p.AdjustEndpoints([]*endpoint.Endpoint{ + endpoint.NewEndpoint("a.record.", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("geo.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ), + endpoint.NewEndpoint("geo-location.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "location", + ), + endpoint.NewEndpoint("wrr.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithSetIdentifier("identifier"), + }) + assert.NoError(t, err) + assert.Equal(t, endpoints, []*endpoint.Endpoint{ + endpoint.NewEndpoint("a.record.", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("geo.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, p.location, + ).WithSetIdentifier(p.location), + endpoint.NewEndpoint("geo-location.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "location", + ).WithSetIdentifier("location"), + endpoint.NewEndpoint("wrr.record.", endpoint.RecordTypeA, "1.2.3.4").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "1e+02", + ).WithSetIdentifier("identifier"), + }) +} + func TestGoogleApplyChanges(t *testing.T) { records := map[*dns.ManagedZone][]*dns.ResourceRecordSet{ &dns.ManagedZone{ @@ -295,6 +382,142 @@ func TestGoogleApplyChanges(t *testing.T) { Ttl: googleRecordTTL, Rrdatas: []string{"delete-test-cname."}, }, + &dns.ResourceRecordSet{ + Name: "geo-update.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "update", + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-update-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-update-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "delete", + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "delete", + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 100.0, + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, }, &dns.ManagedZone{ Name: "zone-2", @@ -324,12 +547,42 @@ func TestGoogleApplyChanges(t *testing.T) { endpoint.NewEndpoint("create-test-ns.zone-1.local", endpoint.RecordTypeNS, "create-test-ns"), endpoint.NewEndpoint("filter-create-test.zone-3.local", endpoint.RecordTypeA, "4.2.2.2"), endpoint.NewEndpoint("nomatch-create-test.zone-0.local", endpoint.RecordTypeA, "4.2.2.1"), + endpoint.NewEndpoint("geo-create.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "create", + ), + endpoint.NewEndpoint("geo-update-create.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "create", + ), + endpoint.NewEndpoint("wrr-create.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "100", + ).WithSetIdentifier("1"), + endpoint.NewEndpoint("wrr-update-create.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50", + ).WithSetIdentifier("1"), }, UpdateOld: []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.local", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("update-test-ttl.zone-2.local", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("update-test-cname.zone-1.local", endpoint.RecordTypeCNAME, "update-test-cname"), endpoint.NewEndpoint("filter-update-test.zone-3.local", endpoint.RecordTypeA, "4.2.2.2"), + endpoint.NewEndpoint("geo-update.zone-1.local.", endpoint.RecordTypeA, "1.1.1.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "update", + ), + endpoint.NewEndpoint("wrr-update.zone-1.local.", endpoint.RecordTypeA, "1.1.1.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50", + ).WithSetIdentifier("1"), }, UpdateNew: []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.local", endpoint.RecordTypeA, "1.2.3.4"), @@ -337,6 +590,16 @@ func TestGoogleApplyChanges(t *testing.T) { endpoint.NewEndpoint("update-test-cname.zone-1.local", endpoint.RecordTypeCNAME, "updated-test-cname"), endpoint.NewEndpoint("filter-update-test.zone-3.local", endpoint.RecordTypeA, "5.6.7.8"), endpoint.NewEndpoint("nomatch-update-test.zone-0.local", endpoint.RecordTypeA, "8.7.6.5"), + endpoint.NewEndpoint("geo-update.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "update", + ), + endpoint.NewEndpoint("wrr-update.zone-1.local.", endpoint.RecordTypeA, "2.2.2.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50", + ).WithSetIdentifier("1"), }, Delete: []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.local", endpoint.RecordTypeA, "8.8.8.8"), @@ -344,6 +607,31 @@ func TestGoogleApplyChanges(t *testing.T) { endpoint.NewEndpoint("delete-test-cname.zone-1.local", endpoint.RecordTypeCNAME, "delete-test-cname"), endpoint.NewEndpoint("filter-delete-test.zone-3.local", endpoint.RecordTypeA, "4.2.2.2"), endpoint.NewEndpoint("nomatch-delete-test.zone-0.local", endpoint.RecordTypeA, "4.2.2.1"), + endpoint.NewEndpoint("geo-update.zone-1.local.", endpoint.RecordTypeA, "1.1.1.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "delete", + ), + endpoint.NewEndpoint("geo-update-delete.zone-1.local.", endpoint.RecordTypeA, "1.1.1.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "delete", + ), + endpoint.NewEndpoint("geo-delete.zone-1.local.", endpoint.RecordTypeA, "1.1.1.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyGeo, + ).WithProviderSpecific( + providerSpecificLocation, "delete", + ), + endpoint.NewEndpoint("wrr-update-delete.zone-1.local.", endpoint.RecordTypeA, "1.1.1.2").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "50.0", + ).WithSetIdentifier("1"), + endpoint.NewEndpoint("wrr-delete.zone-1.local.", endpoint.RecordTypeA, "1.1.1.1").WithProviderSpecific( + providerSpecificRoutingPolicy, providerSpecificRoutingPolicyWrr, + ).WithProviderSpecific( + providerSpecificWeight, "100.0", + ).WithSetIdentifier("0"), }, })) mockClients.EqualRecords(&map[*dns.ManagedZone][]*dns.ResourceRecordSet{ @@ -381,6 +669,146 @@ func TestGoogleApplyChanges(t *testing.T) { Ttl: googleRecordTTL, Rrdatas: []string{"create-test-ns."}, }, + &dns.ResourceRecordSet{ + Name: "geo-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "create", + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-update.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "update", + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-update-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "create", + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "geo-update-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Geo: &dns.RRSetRoutingPolicyGeoPolicy{ + Items: []*dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + &dns.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: "current", + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 0, + Rrdatas: []string{"0.0.0.0"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 100.0, + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update-create.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"2.2.2.2"}, + }, + }, + }, + }, + }, + &dns.ResourceRecordSet{ + Name: "wrr-update-delete.zone-1.local.", + Type: "A", + Ttl: googleRecordTTL, + RoutingPolicy: &dns.RRSetRoutingPolicy{ + Wrr: &dns.RRSetRoutingPolicyWrrPolicy{ + Items: []*dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + &dns.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Weight: 50.0, + Rrdatas: []string{"1.1.1.1"}, + }, + }, + }, + }, + }, }, &dns.ManagedZone{ Name: "zone-2", @@ -484,38 +912,6 @@ func TestGoogleApplyChangesEmpty(t *testing.T) { assert.NoError(t, p.ApplyChanges(context.Background(), &plan.Changes{})) } -func TestNewChange(t *testing.T) { - p := newGoogleProvider().WithMockClients(newMockClients(t)) - - records := []*dns.ResourceRecordSet{} - for _, ep := range []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-2.local", endpoint.RecordTypeA, 1, "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test.zone-2.local", endpoint.RecordTypeA, 120, "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.local", endpoint.RecordTypeCNAME, 4000, "update-test-cname"), - // test fallback to Ttl:300 when Ttl==0 : - endpoint.NewEndpointWithTTL("update-test.zone-1.local", endpoint.RecordTypeA, 0, "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test-mx.zone-1.local", endpoint.RecordTypeMX, 6000, "10 mail"), - endpoint.NewEndpoint("delete-test.zone-1.local", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpoint("delete-test-cname.zone-1.local", endpoint.RecordTypeCNAME, "delete-test-cname"), - } { - records = append(records, p.newChange(&plan.RRSetChange{ - Name: plan.RRName(provider.EnsureTrailingDot(ep.DNSName)), - Type: plan.RRType(ep.RecordType), - Create: []*endpoint.Endpoint{ep}, - }).Additions...) - } - - validateChangeRecords(t, records, []*dns.ResourceRecordSet{ - {Name: "update-test.zone-2.local.", Rrdatas: []string{"8.8.4.4"}, Type: "A", Ttl: 1}, - {Name: "delete-test.zone-2.local.", Rrdatas: []string{"8.8.4.4"}, Type: "A", Ttl: 120}, - {Name: "update-test-cname.zone-1.local.", Rrdatas: []string{"update-test-cname."}, Type: "CNAME", Ttl: 4000}, - {Name: "update-test.zone-1.local.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, - {Name: "update-test-mx.zone-1.local.", Rrdatas: []string{"10 mail."}, Type: "MX", Ttl: 6000}, - {Name: "delete-test.zone-1.local.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, - {Name: "delete-test-cname.zone-1.local.", Rrdatas: []string{"delete-test-cname."}, Type: "CNAME", Ttl: 300}, - }) -} - func TestGoogleBatchChangeSet(t *testing.T) { mockClients := newMockClients(t) mockClients.changesClient.maxChangeSize = 1 @@ -578,21 +974,6 @@ func sortChangesByName(cs *dns.Change) { }) } -func validateChangeRecords(t *testing.T, records []*dns.ResourceRecordSet, expected []*dns.ResourceRecordSet) { - require.Len(t, records, len(expected)) - - for i := range records { - validateChangeRecord(t, records[i], expected[i]) - } -} - -func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected *dns.ResourceRecordSet) { - assert.Equal(t, expected.Name, record.Name) - assert.Equal(t, expected.Rrdatas, record.Rrdatas) - assert.Equal(t, expected.Ttl, record.Ttl) - assert.Equal(t, expected.Type, record.Type) -} - func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected) } @@ -657,10 +1038,7 @@ func (m *mockChangesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Change, e Message: fmt.Sprintf("record not found: %v", deletion), } } - (*m.client.records)[managedZone] = append( - (*m.client.records)[managedZone][:index], - (*m.client.records)[managedZone][index+1:]... - ) + (*m.client.records)[managedZone] = slices.Delete((*m.client.records)[managedZone], index, index + 1) } for _, addition := range m.change.Additions { for _, record := range (*m.client.records)[managedZone] { @@ -685,6 +1063,32 @@ type mockResourceRecordSetsClient struct { recordsErr error } +type mockResourceRecordSetsGetCall struct { + client *mockResourceRecordSetsClient + managedZone string + name string + type_ string +} + +func (m *mockResourceRecordSetsGetCall) Do(opts ...googleapi.CallOption) (*dns.ResourceRecordSet, error) { + if m.client.recordsErr != nil { + return nil, m.client.recordsErr + } + var managedZone *dns.ManagedZone + for zone := range maps.Keys(*m.client.records) { + if zone.Name == m.managedZone { + managedZone = zone + break + } + } + for _, record := range (*m.client.records)[managedZone] { + if record.Name == m.name && record.Type == m.type_ { + return record, nil + } + } + return nil, &googleapi.Error{Code: http.StatusNotFound} +} + type mockResourceRecordSetsListCall struct { client *mockResourceRecordSetsClient managedZone string @@ -707,6 +1111,10 @@ func (m *mockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns. return f(&dns.ResourceRecordSetsListResponse{Rrsets: (*m.client.records)[managedZone]}) } +func (m *mockResourceRecordSetsClient) Get(managedZone string, name string, type_ string) resourceRecordSetsGetCallInterface { + return &mockResourceRecordSetsGetCall{client: m, managedZone: managedZone, name: name, type_: type_} +} + func (m *mockResourceRecordSetsClient) List(managedZone string) resourceRecordSetsListCallInterface { return &mockResourceRecordSetsListCall{client: m, managedZone: managedZone} } diff --git a/source/source.go b/source/source.go index 012cf82739..c1899ccde3 100644 --- a/source/source.go +++ b/source/source.go @@ -212,6 +212,12 @@ func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.Pro Name: fmt.Sprintf("aws/%s", attr), Value: v, }) + } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/google-") { + attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/") + providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ + Name: attr, + Value: v, + }) } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") { attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-") providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{