Skip to content

Commit

Permalink
digitalocean: add API rate limit
Browse files Browse the repository at this point in the history
  • Loading branch information
tja committed Jun 25, 2024
1 parent 8245b89 commit ab6194b
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 12 deletions.
7 changes: 7 additions & 0 deletions docs/tutorials/digitalocean.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,10 @@ rate limiting because of the number of API calls that external-dns must make to
the current DNS configuration during every reconciliation loop. If this is the case, use the
`--digitalocean-api-page-size` option to increase the size of the pages used when querying the DigitalOcean API.
(Note: external-dns uses a default of 50.)

### API Rate Limit

If changing the API page size is not sufficient to avoid API rate limiting, use the
`--digitalocean-api-rate-limit` option to set the maximum number of requests per minute allowed when querying
the DigitalOcean API.
(Note: external-dns uses a default of 200.)
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func main() {
case "google":
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
case "digitalocean":
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize, cfg.DigitalOceanAPIRateLimit)
case "ovh":
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun)
case "linode":
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 @@ -191,6 +191,7 @@ type Config struct {
TransIPAccountName string
TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
DigitalOceanAPIRateLimit int
ManagedDNSRecordTypes []string
ExcludeDNSRecordTypes []string
GoDaddyAPIKey string `secure:"yes"`
Expand Down Expand Up @@ -342,6 +343,7 @@ var defaultConfig = &Config{
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
DigitalOceanAPIRateLimit: 200,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
ExcludeDNSRecordTypes: []string{},
GoDaddyAPIKey: "",
Expand Down Expand Up @@ -529,6 +531,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
app.Flag("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.NS1MinTTLSeconds)
app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
app.Flag("digitalocean-api-rate-limit", "Configure the rate limit used when querying the DigitalOcean API, in operations per minute.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIRateLimit)).IntVar(&cfg.DigitalOceanAPIRateLimit)
app.Flag("ibmcloud-config-file", "When using the IBM Cloud provider, specify the IBM Cloud configuration file (required when --provider=ibmcloud").Default(defaultConfig.IBMCloudConfigFile).StringVar(&cfg.IBMCloudConfigFile)
app.Flag("ibmcloud-proxied", "When using the IBM provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.IBMCloudProxied)
// GoDaddy flags
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 @@ -121,6 +121,7 @@ var (
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
DigitalOceanAPIRateLimit: 200,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
RFC2136BatchChangeSize: 50,
OCPRouterName: "default",
Expand Down Expand Up @@ -234,6 +235,7 @@ var (
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
DigitalOceanAPIRateLimit: 200,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
RFC2136BatchChangeSize: 100,
IBMCloudProxied: true,
Expand Down Expand Up @@ -376,6 +378,7 @@ func TestParseFlags(t *testing.T) {
"--transip-account=transip",
"--transip-keyfile=/path/to/transip.key",
"--digitalocean-api-page-size=100",
"--digitalocean-api-rate-limit=200",
"--managed-record-types=A",
"--managed-record-types=AAAA",
"--managed-record-types=CNAME",
Expand Down Expand Up @@ -494,6 +497,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
"EXTERNAL_DNS_DIGITALOCEAN_API_RATE_LIMIT": "200",
"EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS",
"EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_IBMCLOUD_PROXIED": "1",
Expand Down
47 changes: 38 additions & 9 deletions provider/digitalocean/digital_ocean.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/digitalocean/godo"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"golang.org/x/time/rate"

"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
Expand All @@ -45,7 +46,9 @@ type DigitalOceanProvider struct {
domainFilter endpoint.DomainFilter
// page size when querying paginated APIs
apiPageSize int
DryRun bool
// rate limiter when querying the API
apiRateLimiter *rate.Limiter
DryRun bool
}

type digitalOceanChangeCreate struct {
Expand Down Expand Up @@ -76,7 +79,7 @@ func (c *digitalOceanChanges) Empty() bool {
}

// NewDigitalOceanProvider initializes a new DigitalOcean DNS based Provider.
func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool, apiPageSize int) (*DigitalOceanProvider, error) {
func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool, apiPageSize int, apiRateLimit int) (*DigitalOceanProvider, error) {
token, ok := os.LookupEnv("DO_TOKEN")
if !ok {
return nil, fmt.Errorf("no token found")
Expand All @@ -90,10 +93,11 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi
}

p := &DigitalOceanProvider{
Client: client.Domains,
domainFilter: domainFilter,
apiPageSize: apiPageSize,
DryRun: dryRun,
Client: client.Domains,
domainFilter: domainFilter,
apiPageSize: apiPageSize,
apiRateLimiter: rate.NewLimiter(rate.Limit(apiRateLimit)/60.0, 1),
DryRun: dryRun,
}
return p, nil
}
Expand Down Expand Up @@ -195,6 +199,11 @@ func (p *DigitalOceanProvider) fetchRecords(ctx context.Context, zoneName string
allRecords := []godo.DomainRecord{}
listOptions := &godo.ListOptions{PerPage: p.apiPageSize}
for {
err := p.apiRateLimiter.Wait(ctx)
if err != nil {
return nil, err
}

records, resp, err := p.Client.Records(ctx, zoneName, listOptions)
if err != nil {
return nil, err
Expand All @@ -220,6 +229,11 @@ func (p *DigitalOceanProvider) fetchZones(ctx context.Context) ([]godo.Domain, e
allZones := []godo.Domain{}
listOptions := &godo.ListOptions{PerPage: p.apiPageSize}
for {
err := p.apiRateLimiter.Wait(ctx)
if err != nil {
return nil, err
}

zones, resp, err := p.Client.List(ctx, listOptions)
if err != nil {
return nil, err
Expand Down Expand Up @@ -315,7 +329,12 @@ func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digit
continue
}

_, _, err := p.Client.CreateRecord(ctx, c.Domain, c.Options)
err := p.apiRateLimiter.Wait(ctx)
if err != nil {
return err
}

_, _, err = p.Client.CreateRecord(ctx, c.Domain, c.Options)
if err != nil {
return err
}
Expand All @@ -334,7 +353,12 @@ func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digit
continue
}

_, _, err := p.Client.EditRecord(ctx, u.Domain, u.DomainRecord.ID, u.Options)
err := p.apiRateLimiter.Wait(ctx)
if err != nil {
return err
}

_, _, err = p.Client.EditRecord(ctx, u.Domain, u.DomainRecord.ID, u.Options)
if err != nil {
return err
}
Expand All @@ -350,7 +374,12 @@ func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digit
continue
}

_, err := p.Client.DeleteRecord(ctx, d.Domain, d.RecordID)
err := p.apiRateLimiter.Wait(ctx)
if err != nil {
return err
}

_, err = p.Client.DeleteRecord(ctx, d.Domain, d.RecordID)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions provider/digitalocean/digital_ocean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,12 +560,12 @@ func TestDigitalOceanProcessDeleteActions(t *testing.T) {

func TestNewDigitalOceanProvider(t *testing.T) {
_ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx")
_, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50)
_, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50, 200)
if err != nil {
t.Errorf("should not fail, %s", err)
}
_ = os.Unsetenv("DO_TOKEN")
_, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50)
_, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50, 200)
if err == nil {
t.Errorf("expected to fail")
}
Expand Down

0 comments on commit ab6194b

Please sign in to comment.