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
Changes from 8 commits
015004e
b2823b9
861a276
333e175
fbc26ad
44064b6
bd901b0
ea300b9
db0021d
d40337d
e5817f7
61fb6c4
5298815
50e10e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
||
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" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole block might be extracted into a |
||
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 == "@" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I feel like maybe that is something worth adding an integration test for. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is case for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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), | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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.