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(DNSimple): User API tokens #4274

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
9b3c285
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 20, 2024
ea2d259
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 26, 2024
26694e4
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 26, 2024
d0c121b
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 26, 2024
d475eb6
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 26, 2024
3babcbc
Fix provider/dnsimple/dnsimple_test.go: Add the missing call to NewDn…
IntegralProgrammer Feb 26, 2024
acf188f
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Feb 26, 2024
eb59b9b
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
c54a9a8
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
9c24ecc
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
660b20b
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
3eb852f
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
487501d
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
ed3efdb
Allow for DNSimple User API tokens to be used by implementing the DNS…
IntegralProgrammer Mar 15, 2024
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
16 changes: 14 additions & 2 deletions docs/tutorials/dnsimple.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

If the generated DNSimple API access token is a _User token_, as opposed to an _Account token_, the following environment variables must also be set:
IntegralProgrammer marked this conversation as resolved.
Show resolved Hide resolved
- `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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
```


Expand Down
43 changes: 39 additions & 4 deletions provider/dnsimple/dnsimple.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ const (

// NewDnsimpleProvider initializes a new Dnsimple based provider
func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) {
return BuildDnsimpleProvider(domainFilter, zoneIDFilter, dryRun, false)
}

// Create a new Dnsimple based provider but return a *dnsimpleProvider and allow for the call to provider.identity.Whoami to be bypassed - both for testing purposes
func BuildDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, skipWhoami bool) (*dnsimpleProvider, error) {
oauthToken := os.Getenv("DNSIMPLE_OAUTH")
dnsimpleAccountId := os.Getenv("DNSIMPLE_ACCOUNT_ID")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
dnsimpleAccountId := os.Getenv("DNSIMPLE_ACCOUNT_ID")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 487501d

if len(oauthToken) == 0 {
return nil, fmt.Errorf("no dnsimple oauth token provided")
}
Expand All @@ -118,11 +124,20 @@ func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provid
dryRun: dryRun,
}

whoamiResponse, err := provider.identity.Whoami(context.Background())
if err != nil {
return nil, err
whoamiResponse := &dnsimple.WhoamiResponse{}
if !skipWhoami {
var err error
whoamiResponse, err = provider.identity.Whoami(context.Background())
if err != nil {
return nil, err
}
}

if dnsimpleAccountId == "" {
provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)
} else {
provider.accountID = dnsimpleAccountId
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
whoamiResponse := &dnsimple.WhoamiResponse{}
if !skipWhoami {
var err error
whoamiResponse, err = provider.identity.Whoami(context.Background())
if err != nil {
return nil, err
}
}
if dnsimpleAccountId == "" {
provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)
} else {
provider.accountID = dnsimpleAccountId
provider.accountID = dnsimpleAccountId
if provider.accountID == "" {
whoamiResponse, err := provider.identity.Whoami(context.Background())
if err != nil {
return nil, err
}
provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)

I suggest to remove skipWhoami bool and also to avoid if ... else.
If you want to test also this part, you'll need to use mock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion - I have implemented this in 9c24ecc and 660b20b

}
provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)
return provider, nil
}

Expand All @@ -136,9 +151,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 {
Expand Down
49 changes: 46 additions & 3 deletions provider/dnsimple/dnsimple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -215,10 +246,22 @@ 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change wasn't really within the scope of this PR but this should be added in any case as without it, NewDnsimpleProvider isn't actually tested with an unset DNSIMPLE_OAUTH environment variable.

if err == nil {
t.Errorf("Expected to fail new provider on empty token")
}

os.Setenv("DNSIMPLE_OAUTH", "xxxxxxxxxxxxxxxxxxxxxxxxxx")
os.Setenv("DNSIMPLE_ACCOUNT_ID", "12345678")
builtProvider, err := BuildDnsimpleProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, true)
if err != nil {
t.Errorf("Unexpected error thrown when testing BuildDnsimpleProvider with the DNSIMPLE_ACCOUNT_ID environment variable set")
}
assert.Equal(t, builtProvider.accountID, "12345678")
os.Unsetenv("DNSIMPLE_OAUTH")
os.Unsetenv("DNSIMPLE_ACCOUNT_ID")
}

func testDnsimpleGetRecordID(t *testing.T) {
Expand Down
Loading