Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Google CloudDNS): add routing policy support #4928

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
| `--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) |
| `--zone-id-filter=` | Filter target zones by hosted zone id; specify multiple times for multiple zones (optional) |
| `--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. |
| `--google-location=""` | When using the Google provider, current location is auto-detected, when running on GCP. Specify other location with this. May be specified when running outside GCP. |
| `--google-batch-change-size=1000` | When using the Google provider, set the maximum number of changes that will be applied in each batch. |
| `--google-batch-change-interval=1s` | When using the Google provider, set the interval between batch changes. |
| `--google-zone-visibility=` | When using the Google provider, filter for zones with this visibility (optional, options: public, private) |
Expand Down
58 changes: 58 additions & 0 deletions docs/tutorials/google.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Google Cloud DNS

## 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"
```
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,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":
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type Config struct {
Provider string
ProviderCacheTime time.Duration
GoogleProject string
GoogleLocation string
GoogleBatchChangeSize int
GoogleBatchChangeInterval time.Duration
GoogleZoneVisibility string
Expand Down Expand Up @@ -238,6 +239,7 @@ var defaultConfig = &Config{
Provider: "",
ProviderCacheTime: 0,
GoogleProject: "",
GoogleLocation: "",
GoogleBatchChangeSize: 1000,
GoogleBatchChangeInterval: time.Second,
GoogleZoneVisibility: "",
Expand Down Expand Up @@ -483,6 +485,7 @@ func App(cfg *Config) *kingpin.Application {
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 other 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")
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var (
Compatibility: "",
Provider: "google",
GoogleProject: "",
GoogleLocation: "",
GoogleBatchChangeSize: 1000,
GoogleBatchChangeInterval: time.Second,
GoogleZoneVisibility: "",
Expand Down Expand Up @@ -146,6 +147,7 @@ var (
Compatibility: "mate",
Provider: "google",
GoogleProject: "project",
GoogleLocation: "location",
GoogleBatchChangeSize: 100,
GoogleBatchChangeInterval: time.Second * 2,
GoogleZoneVisibility: "private",
Expand Down Expand Up @@ -280,6 +282,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",
Expand Down Expand Up @@ -406,6 +409,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",
Expand Down
94 changes: 94 additions & 0 deletions plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package plan

import (
"fmt"
"iter"
"maps"
"strings"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -51,6 +53,39 @@ type Plan struct {
OwnerID string
}

// RRName is a canonical name associated with a resource record (i.e. lower-case with trailing period)
type RRName string

// RRType is a type associated with a resource record (e.g., "A", "AAAA", "TXT", etc.)
type RRType string

// RRKey is a key for entries in maps of resource records
type RRKey struct {
Name RRName
Type RRType
}

func newRRKey(ep *endpoint.Endpoint) RRKey {
return RRKey{
Name: RRName(normalizeDNSName(ep.DNSName)),
Type: RRType(ep.RecordType),
}
}

// RRSetChange represents changes to be applied to a single resource record set
//
// | Action | Create | Delete |
// |--------+--------+--------|
// | Create | * | nil |
// | Delete | nil | * |
// | Update | * | * |
type RRSetChange struct {
Name RRName
Type RRType
Create []*endpoint.Endpoint
Delete []*endpoint.Endpoint
}

// Changes holds lists of actions to be executed by dns providers
type Changes struct {
// Records that need to be created
Expand Down Expand Up @@ -154,6 +189,65 @@ func (t *planTable) newPlanKey(e *endpoint.Endpoint) planKey {
return key
}

// Return an iterator of changes to resource records sets
func (c *Changes) All() iter.Seq[*RRSetChange] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?
AWS has also Weighted DNS support and did not need the changes in plan.go

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the description; AWS represent the various sub-records associated with a given DNS name as distinct objects, distinguished by SetIdentifier. Google represent similar configuration via a single, hierarchical, record.

When modifying components of an AWS weighted configuration the various records may be modified without concern for the ordering of mutations, because they are aggregated by AWS' DNS servers. When modifying components of a Google weighted configuration it is necessary to gather all of the mutations together in order to update the single record. The plan.Changes.All() function proposed achieves the necessary grouping and ordering. It would be possible to rework most/all usage of plan.Change to be in the form of plan.Changes.All(), but it is not necessary to do so.

return func(yield func(change *RRSetChange) bool) {
rrSetCreates := map[RRKey]*RRSetChange{}
rrSetDeletes := map[RRKey]*RRSetChange{}
rrSetUpdates := map[RRKey]*RRSetChange{}
actions := []struct {
endpoints *[]*endpoint.Endpoint
rrSetChanges *map[RRKey]*RRSetChange
}{
{endpoints: &c.UpdateNew, rrSetChanges: &rrSetUpdates},
{endpoints: &c.UpdateOld, rrSetChanges: &rrSetUpdates},
{endpoints: &c.Create, rrSetChanges: &rrSetCreates},
{endpoints: &c.Delete, rrSetChanges: &rrSetDeletes},
}
for _, action := range actions {
for _, ep := range *action.endpoints {
rrKey := newRRKey(ep)
change, ok := rrSetUpdates[rrKey]
if !ok && action.rrSetChanges != &rrSetUpdates {
if action.rrSetChanges != &rrSetCreates {
change, ok = rrSetCreates[rrKey]
if ok {
delete(rrSetCreates, rrKey)
rrSetUpdates[rrKey] = change
}
}
if !ok {
change, ok = (*action.rrSetChanges)[rrKey]
}
}
if !ok {
change = &RRSetChange{
Name: rrKey.Name,
Type: rrKey.Type,
}
(*action.rrSetChanges)[rrKey] = change
}
switch action.endpoints {
case &c.Create, &c.UpdateNew:
change.Create = append(change.Create, ep)
case &c.Delete, &c.UpdateOld:
change.Delete = append(change.Delete, ep)
}
}
}
changes := []*map[RRKey]*RRSetChange{
&rrSetDeletes, &rrSetUpdates, &rrSetCreates,
}
for _, rrSetChanges := range changes {
for rrSetChange := range maps.Values(*rrSetChanges) {
if !yield(rrSetChange) {
return
}
}
}
}
}

func (c *Changes) HasChanges() bool {
if len(c.Create) > 0 || len(c.Delete) > 0 {
return true
Expand Down
Loading
Loading