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 provider #171

Merged
merged 14 commits into from Aug 10, 2017
Merged
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -16,6 +16,7 @@ Currently supported DNS providers:
- Active Directory
- BIND
- CloudFlare
- Digitalocean
- DNSimple
- Gandi
- Google
Expand Down
40 changes: 40 additions & 0 deletions docs/_providers/digitalocean.md
@@ -0,0 +1,40 @@
---
name: Digitalocean
layout: default
jsId: DIGITALOCEAN
---
# Digitalocean Provider

## Configuration

In your providers config json file you must provide your
[Digitalocean OAuth Token](https://cloud.digitalocean.com/settings/applications)

{% highlight json %}
{
"digitalocean":{
"token": "your-digitalocean-ouath-token"
}
}
{% endhighlight %}

## Metadata

This provider does not recognize any special metadata fields unique to route 53.

## Usage

Example javascript:

{% highlight js %}
var REG_NAMECOM = NewRegistrar("name.com","NAMEDOTCOM");
var DO = NewDnsProvider("do", "DIGITALOCEAN");

D("example.tld", REG_NAMECOM, DnsProvider(DO),
A("test","1.2.3.4")
);
{%endhighlight%}

## Activation

[Create OAuth Token](https://cloud.digitalocean.com/settings/applications)
1 change: 0 additions & 1 deletion docs/provider-list.html
Expand Up @@ -23,7 +23,6 @@ <h1> Service providers </h1>
<li>AWS R53 (DNS works. Request is to add Registrar support) (<a href="https://github.com/StackExchange/dnscontrol/issues/68">#68</a>)</li>
<li>Azure (<a href="https://github.com/StackExchange/dnscontrol/issues/42">#42</a>)</li>
<li>ClouDNS (<a href="https://github.com/StackExchange/dnscontrol/issues/114">#114</a>)</li>
<li>Digital Ocean (<a href="https://github.com/StackExchange/dnscontrol/issues/125">#125</a>)</li>
<li>Dyn (<a href="https://github.com/StackExchange/dnscontrol/issues/61">#61</a>)</li>
<li>Gandi (DNS works. Request is to add Registrar support) (<a href="https://github.com/StackExchange/dnscontrol/issues/87">#87</a>)</li>
<li>GoDaddy (<a href="https://github.com/StackExchange/dnscontrol/issues/145">#145</a>)</li>
Expand Down
16 changes: 8 additions & 8 deletions integrationTest/integration_test.go
Expand Up @@ -325,14 +325,14 @@ var tests = []*TestCase{

//SRV
tc("Empty").IfHasCapability(providers.CanUseSRV),
tc("SRV record", srv("@", 5, 6, 7, "foo.com.")).IfHasCapability(providers.CanUseSRV),
tc("Second SRV record, same prio", srv("@", 5, 6, 7, "foo.com."), srv("@", 5, 60, 70, "foo2.com.")).IfHasCapability(providers.CanUseSRV),
tc("3 SRV", srv("@", 5, 6, 7, "foo.com."), srv("@", 5, 60, 70, "foo2.com."), srv("@", 15, 65, 75, "foo3.com.")).IfHasCapability(providers.CanUseSRV),
tc("Delete one", srv("@", 5, 6, 7, "foo.com."), srv("@", 15, 65, 75, "foo3.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Target", srv("@", 5, 6, 7, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Priority", srv("@", 52, 6, 7, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Weight", srv("@", 52, 62, 7, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Port", srv("@", 52, 62, 72, "foo.com."), srv("@", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("SRV record", srv("_service._protocol", 5, 6, 7, "foo.com.")).IfHasCapability(providers.CanUseSRV),
tc("Second SRV record, same prio", srv("_service._protocol", 5, 6, 7, "foo.com."), srv("_service._protocol", 5, 60, 70, "foo2.com.")).IfHasCapability(providers.CanUseSRV),
tc("3 SRV", srv("_service._protocol", 5, 6, 7, "foo.com."), srv("_service._protocol", 5, 60, 70, "foo2.com."), srv("_service._protocol", 15, 65, 75, "foo3.com.")).IfHasCapability(providers.CanUseSRV),
tc("Delete one", srv("_service._protocol", 5, 6, 7, "foo.com."), srv("_service._protocol", 15, 65, 75, "foo3.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Target", srv("_service._protocol", 5, 6, 7, "foo.com."), srv("_service._protocol", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Priority", srv("_service._protocol", 52, 6, 7, "foo.com."), srv("_service._protocol", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Weight", srv("_service._protocol", 52, 62, 7, "foo.com."), srv("_service._protocol", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),
tc("Change Port", srv("_service._protocol", 52, 62, 72, "foo.com."), srv("_service._protocol", 15, 65, 75, "foo4.com.")).IfHasCapability(providers.CanUseSRV),

//CAA
tc("Empty").IfHasCapability(providers.CanUseCAA),
Expand Down
4 changes: 4 additions & 0 deletions integrationTest/providers.json
Expand Up @@ -12,6 +12,10 @@
"apiuser": "$CF_USER",
"domain": "$CF_DOMAIN"
},
"DIGITALOCEAN": {
"token": "$DO_TOKEN",
"domain": "$DO_DOMAIN"
},
"DNSIMPLE": {
"COMMENT": "16/17: no ns records managable. Not even for subdomains.",
"baseurl": "https://api.sandbox.dnsimple.com",
Expand Down
1 change: 1 addition & 0 deletions providers/_all/all.go
Expand Up @@ -6,6 +6,7 @@ import (
_ "github.com/StackExchange/dnscontrol/providers/activedir"
_ "github.com/StackExchange/dnscontrol/providers/bind"
_ "github.com/StackExchange/dnscontrol/providers/cloudflare"
_ "github.com/StackExchange/dnscontrol/providers/digitalocean"
_ "github.com/StackExchange/dnscontrol/providers/dnsimple"
_ "github.com/StackExchange/dnscontrol/providers/gandi"
_ "github.com/StackExchange/dnscontrol/providers/google"
Expand Down
197 changes: 197 additions & 0 deletions providers/digitalocean/digitaloceanProvider.go
@@ -0,0 +1,197 @@
package digitalocean

import (
"context"
"encoding/json"
"fmt"

"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/miekg/dns/dnsutil"

"github.com/digitalocean/godo"
"golang.org/x/oauth2"
)

/*

Digitalocean API DNS provider:

Info required in `creds.json`:
- token

*/

type DoApi struct {
client *godo.Client
}

var defaultNameServerNames = []string{
"ns1.digitalocean.com",
"ns2.digitalocean.com",
"ns3.digitalocean.com",
}

func newDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if m["token"] == "" {
return nil, fmt.Errorf("Digitalocean Token must be provided.")
}

oauthClient := oauth2.NewClient(
context.Background(),
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
)
client := godo.NewClient(oauthClient)

return &DoApi{client: client}, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to try and enforce going forward that providers should make some api call in the initializer to verify credentials work. Perhaps list domains once up front, and cache the list.

}

func init() {
providers.RegisterDomainServiceProviderType("DIGITALOCEAN", newDo, providers.CanUseSRV)
}

func (api *DoApi) EnsureDomain(domain string) error {
ctx := context.Background()
_, resp, err := api.client.Domains.Get(ctx, domain)
if resp.Status == "404" {
Copy link
Contributor

Choose a reason for hiding this comment

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

We try not to automatically silently create domains as we go, because it makes the preview/push semantics kinda odd. Instead, implement this interface and use dnscontrol create-domains to call it.

Copy link
Contributor Author

@Deraen Deraen Aug 7, 2017

Choose a reason for hiding this comment

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

Done. Just had wrong name.

_, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{
Name: domain,
IPAddress: "",
})
return err
} else {
return err
}
}

func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
return models.StringsToNameservers(defaultNameServerNames), nil
}

func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
ctx := context.Background()
dc.Punycode()

records := []godo.DomainRecord{}
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole block might be extracted into a GetRecords(name) function. Considering some future api changes that would be esiser if we broke that out now.

opt := &godo.ListOptions{}
for {
result, resp, err := api.client.Domains.Records(ctx, dc.Name, opt)
if err != nil {
return nil, err
}

for _, d := range result {
records = append(records, d)
}

if resp.Links == nil || resp.Links.IsLastPage() {
break
}

page, err := resp.Links.CurrentPage()
if err != nil {
return nil, err
}

opt.Page = page + 1
}

existingRecords := make([]*models.RecordConfig, len(records))
for i := range records {
existingRecords[i] = toRc(dc, &records[i])
}

differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)

var corrections = []*models.Correction{}

// Deletes first so changing type works etc.
for _, m := range delete {
id := m.Existing.Original.(*godo.DomainRecord).ID
corr := &models.Correction{
Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
F: func() error {
_, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id)
return err
},
}
corrections = append(corrections, corr)
}
for _, m := range create {
req := toReq(dc, m.Desired)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
_, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req)
return err
},
}
corrections = append(corrections, corr)
}
for _, m := range modify {
id := m.Existing.Original.(*godo.DomainRecord).ID
req := toReq(dc, m.Desired)
corr := &models.Correction{
Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
F: func() error {
_, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req)
return err
},
}
corrections = append(corrections, corr)
}

return corrections, nil
}

func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
// This handles "@" etc.
name := dnsutil.AddOrigin(r.Name, dc.Name)

target := r.Data
// Make target FQDN (#rtype_variations)
if r.Type == "CNAME" && target == "@" {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not quite sure on the logic here. Is the first case for something like CNAME('foo', '@')? I'm not sure I've considered that very often. Why are the other record types not included in the first if?

I feel like maybe that is something worth adding an integration test for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is case for CNAME('www', 'foo.com') where foo.com is the domain. Digitalocean API will return the target this as @. Yes, Integration test for this makes sense. Might also be problem with the other types, but didn't have any such records and didn't think about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any tips of how to create tests for this? The test case would need to be CNAME("foo", domainName) but tests can't access domainName.

target = dc.Name
}
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
target = dnsutil.AddOrigin(target+".", dc.Name)
}

return &models.RecordConfig{
NameFQDN: name,
Type: r.Type,
Target: target,
TTL: uint32(r.TTL),
MxPreference: uint16(r.Priority),
SrvPriority: uint16(r.Priority),
SrvWeight: uint16(r.Weight),
SrvPort: uint16(r.Port),
Original: r,
}
}

func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
// DO wants the short name, e.g. @
name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)

// DO uses the same property for MX and SRV priority
priority := 0
switch rc.Type { // #rtype_variations
case "MX":
priority = int(rc.MxPreference)
case "SRV":
priority = int(rc.SrvPriority)
}

return &godo.DomainRecordEditRequest{
Type: rc.Type,
Name: name,
Data: rc.Target,
TTL: int(rc.TTL),
Priority: priority,
Port: int(rc.SrvPort),
Weight: int(rc.SrvWeight),
}
}
20 changes: 20 additions & 0 deletions vendor/github.com/digitalocean/godo/CHANGELOG.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions vendor/github.com/digitalocean/godo/CONTRIBUTING.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions vendor/github.com/digitalocean/godo/LICENSE.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.