diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md index 876c30e79..c0db645f1 100644 --- a/docs/tutorials/dnsimple.md +++ b/docs/tutorials/dnsimple.md @@ -5,11 +5,15 @@ This tutorial describes how to setup ExternalDNS for usage with DNSimple. Make sure to use **>=0.4.6** version of ExternalDNS for this tutorial. -## Created a DNSimple API Access Token +## Create a DNSimple API Access Token A DNSimple API access token can be acquired by following the [provided documentation from DNSimple](https://support.dnsimple.com/articles/api-access-token/) -The environment variable `DNSIMPLE_OAUTH` must be set to the API token generated for to run ExternalDNS with DNSimple. +The environment variable `DNSIMPLE_OAUTH` must be set to the generated API token to run ExternalDNS with DNSimple. + +When the generated DNSimple API access token is a _User token_, as opposed to an _Account token_, the following environment variables must also be set: + - `DNSIMPLE_ACCOUNT_ID`: Set this to the account ID which the domains to be managed by ExternalDNS belong to (eg. `1001234`). + - `DNSIMPLE_ZONES`: Set this to a comma separated list of DNS zones to be managed by ExternalDNS (eg. `mydomain.com,example.com`). ## Deploy ExternalDNS @@ -44,6 +48,10 @@ spec: env: - name: DNSIMPLE_OAUTH value: "YOUR_DNSIMPLE_API_KEY" + - name: DNSIMPLE_ACCOUNT_ID + value: "SET THIS IF USING A DNSIMPLE USER ACCESS TOKEN" + - name: DNSIMPLE_ZONES + value: "SET THIS IF USING A DNSIMPLE USER ACCESS TOKEN" ``` ### Manifest (for clusters with RBAC enabled) @@ -109,6 +117,10 @@ spec: env: - name: DNSIMPLE_OAUTH value: "YOUR_DNSIMPLE_API_KEY" + - name: DNSIMPLE_ACCOUNT_ID + value: "SET THIS IF USING A DNSIMPLE USER ACCESS TOKEN" + - name: DNSIMPLE_ZONES + value: "SET THIS IF USING A DNSIMPLE USER ACCESS TOKEN" ``` diff --git a/provider/dnsimple/dnsimple.go b/provider/dnsimple/dnsimple.go index ee805695a..ff74246a3 100644 --- a/provider/dnsimple/dnsimple.go +++ b/provider/dnsimple/dnsimple.go @@ -118,11 +118,14 @@ func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provid dryRun: dryRun, } - whoamiResponse, err := provider.identity.Whoami(context.Background()) - if err != nil { - return nil, err + provider.accountID = os.Getenv("DNSIMPLE_ACCOUNT_ID") + if provider.accountID == "" { + whoamiResponse, err := provider.identity.Whoami(context.Background()) + if err != nil { + return nil, err + } + provider.accountID = int64ToString(whoamiResponse.Data.Account.ID) } - provider.accountID = int64ToString(whoamiResponse.Data.Account.ID) return provider, nil } @@ -136,9 +139,29 @@ func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, return int64ToString(whoamiResponse.Data.Account.ID), nil } +func ZonesFromZoneString(zonestring string) map[string]dnsimple.Zone { + zones := make(map[string]dnsimple.Zone) + zoneNames := strings.Split(zonestring, ",") + for indexId, zoneName := range zoneNames { + zone := dnsimple.Zone{Name: zoneName, ID: int64(indexId)} + zones[int64ToString(zone.ID)] = zone + } + return zones +} + // Returns a list of filtered Zones func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone, error) { zones := make(map[string]dnsimple.Zone) + + // If the DNSIMPLE_ZONES environment variable is specified, generate a list of Zones from it + // This is useful for when the DNSIMPLE_OAUTH environment variable is a User API token and + // not an Account API token as the User API token will not have permissions to list Zones + // belong to another account which the User has access permissions for. + envZonesStr := os.Getenv("DNSIMPLE_ZONES") + if envZonesStr != "" { + return ZonesFromZoneString(envZonesStr), nil + } + page := 1 listOptions := &dnsimple.ZoneListOptions{} for { diff --git a/provider/dnsimple/dnsimple_test.go b/provider/dnsimple/dnsimple_test.go index f89e08f77..c2106482b 100644 --- a/provider/dnsimple/dnsimple_test.go +++ b/provider/dnsimple/dnsimple_test.go @@ -33,9 +33,10 @@ import ( ) var ( - mockProvider dnsimpleProvider - dnsimpleListRecordsResponse dnsimple.ZoneRecordsResponse - dnsimpleListZonesResponse dnsimple.ZonesResponse + mockProvider dnsimpleProvider + dnsimpleListRecordsResponse dnsimple.ZoneRecordsResponse + dnsimpleListZonesResponse dnsimple.ZonesResponse + dnsimpleListZonesFromEnvResponse dnsimple.ZonesResponse ) func TestDnsimpleServices(t *testing.T) { @@ -55,6 +56,16 @@ func TestDnsimpleServices(t *testing.T) { Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}, Data: zones, } + firstEnvDefinedZone := dnsimple.Zone{ + ID: 0, + AccountID: 12345, + Name: "example-from-env.com", + } + envDefinedZones := []dnsimple.Zone{firstEnvDefinedZone} + dnsimpleListZonesFromEnvResponse = dnsimple.ZonesResponse{ + Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}, + Data: envDefinedZones, + } firstRecord := dnsimple.ZoneRecord{ ID: 2, ZoneID: "example.com", @@ -151,6 +162,15 @@ func testDnsimpleProviderZones(t *testing.T) { mockProvider.accountID = "2" _, err = mockProvider.Zones(ctx) assert.NotNil(t, err) + + mockProvider.accountID = "3" + os.Setenv("DNSIMPLE_ZONES", "example-from-env.com") + result, err = mockProvider.Zones(ctx) + assert.Nil(t, err) + validateDnsimpleZones(t, result, dnsimpleListZonesFromEnvResponse.Data) + + mockProvider.accountID = "2" + os.Unsetenv("DNSIMPLE_ZONES") } func testDnsimpleProviderRecords(t *testing.T) { @@ -207,6 +227,17 @@ func testDnsimpleSuitableZone(t *testing.T) { zone := dnsimpleSuitableZone("example-beta.example.com", zones) assert.Equal(t, zone.Name, "example.com") + + os.Setenv("DNSIMPLE_ZONES", "environment-example.com,example.environment-example.com") + mockProvider.accountID = "3" + zones, err = mockProvider.Zones(ctx) + assert.Nil(t, err) + + zone = dnsimpleSuitableZone("hello.example.environment-example.com", zones) + assert.Equal(t, zone.Name, "example.environment-example.com") + + os.Unsetenv("DNSIMPLE_ZONES") + mockProvider.accountID = "1" } func TestNewDnsimpleProvider(t *testing.T) { @@ -215,10 +246,23 @@ func TestNewDnsimpleProvider(t *testing.T) { if err == nil { t.Errorf("Expected to fail new provider on bad token") } + os.Unsetenv("DNSIMPLE_OAUTH") + _, err = NewDnsimpleProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true) if err == nil { t.Errorf("Expected to fail new provider on empty token") } + + os.Setenv("DNSIMPLE_OAUTH", "xxxxxxxxxxxxxxxxxxxxxxxxxx") + os.Setenv("DNSIMPLE_ACCOUNT_ID", "12345678") + providerTypedProvider, err := NewDnsimpleProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true) + dnsimpleTypedProvider := providerTypedProvider.(*dnsimpleProvider) + if err != nil { + t.Errorf("Unexpected error thrown when testing NewDnsimpleProvider with the DNSIMPLE_ACCOUNT_ID environment variable set") + } + assert.Equal(t, dnsimpleTypedProvider.accountID, "12345678") + os.Unsetenv("DNSIMPLE_OAUTH") + os.Unsetenv("DNSIMPLE_ACCOUNT_ID") } func testDnsimpleGetRecordID(t *testing.T) {