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

digitalocean: add API rate limit #4573

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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