├── LICENSE ├── README.md ├── _example └── main.go ├── client.go ├── go.mod ├── go.sum └── provider.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CSIRO / Sven Dowideit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DigitalOcean for `libdns` 2 | 3 | [![godoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/libdns/digitalocean) 4 | 5 | 6 | This package implements the libdns interfaces for the [DigitalOcean API](https://developers.digitalocean.com/documentation/v2/#domains) (using the Go implementation from: https://github.com/digitalocean/godo) 7 | 8 | ## Authenticating 9 | 10 | To authenticate you need to supply a DigitalOcean API token. 11 | 12 | ## Example 13 | 14 | Here's a minimal example of how to get all your DNS records using this `libdns` provider (see `_example/main.go`) 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "time" 24 | 25 | "github.com/libdns/libdns" 26 | "github.com/libdns/digitalocean" 27 | ) 28 | 29 | func main() { 30 | token := os.Getenv("DO_AUTH_TOKEN") 31 | if token == "" { 32 | fmt.Printf("DO_AUTH_TOKEN not set\n") 33 | return 34 | } 35 | zone := os.Getenv("ZONE") 36 | if zone == "" { 37 | fmt.Printf("ZONE not set\n") 38 | return 39 | } 40 | provider := digitalocean.Provider{APIToken: token} 41 | 42 | records, err := provider.GetRecords(context.TODO(), zone) 43 | if err != nil { 44 | fmt.Printf("ERROR: %s\n", err.Error()) 45 | } 46 | 47 | testName := "libdns-test" 48 | testId := "" 49 | for _, record := range records { 50 | fmt.Printf("%s (.%s): %s, %s\n", record.Name, zone, record.Value, record.Type) 51 | if record.Name == testName { 52 | testId = record.ID 53 | } 54 | 55 | } 56 | 57 | if testId != "" { 58 | // fmt.Printf("Delete entry for %s (id:%s)\n", testName, testId) 59 | // _, err = provider.DeleteRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 60 | // ID: testId, 61 | // }}) 62 | // if err != nil { 63 | // fmt.Printf("ERROR: %s\n", err.Error()) 64 | // } 65 | // Set only works if we have a record.ID 66 | fmt.Printf("Replacing entry for %s\n", testName) 67 | _, err = provider.SetRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 68 | Type: "TXT", 69 | Name: testName, 70 | Value: fmt.Sprintf("Replacement test entry created by libdns %s", time.Now()), 71 | TTL: time.Duration(30) * time.Second, 72 | ID: testId, 73 | }}) 74 | if err != nil { 75 | fmt.Printf("ERROR: %s\n", err.Error()) 76 | } 77 | } else { 78 | fmt.Printf("Creating new entry for %s\n", testName) 79 | _, err = provider.AppendRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 80 | Type: "TXT", 81 | Name: testName, 82 | Value: fmt.Sprintf("This is a test entry created by libdns %s", time.Now()), 83 | TTL: time.Duration(30) * time.Second, 84 | }}) 85 | if err != nil { 86 | fmt.Printf("ERROR: %s\n", err.Error()) 87 | } 88 | } 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/libdns/libdns" 10 | "github.com/libdns/digitalocean" 11 | ) 12 | 13 | func main() { 14 | token := os.Getenv("DO_AUTH_TOKEN") 15 | if token == "" { 16 | fmt.Printf("DO_AUTH_TOKEN not set\n") 17 | return 18 | } 19 | zone := os.Getenv("ZONE") 20 | if zone == "" { 21 | fmt.Printf("ZONE not set\n") 22 | return 23 | } 24 | provider := digitalocean.Provider{APIToken: token} 25 | 26 | records, err := provider.GetRecords(context.TODO(), zone) 27 | if err != nil { 28 | fmt.Printf("ERROR: %s\n", err.Error()) 29 | } 30 | 31 | testName := "libdns-test" 32 | testId := "" 33 | for _, record := range records { 34 | fmt.Printf("%s (.%s): %s, %s\n", record.Name, zone, record.Value, record.Type) 35 | if record.Name == testName { 36 | testId = record.ID 37 | } 38 | 39 | } 40 | 41 | if testId != "" { 42 | // fmt.Printf("Delete entry for %s (id:%s)\n", testName, testId) 43 | // _, err = provider.DeleteRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 44 | // ID: testId, 45 | // }}) 46 | // if err != nil { 47 | // fmt.Printf("ERROR: %s\n", err.Error()) 48 | // } 49 | // Set only works if we have a record.ID 50 | fmt.Printf("Replacing entry for %s\n", testName) 51 | _, err = provider.SetRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 52 | Type: "TXT", 53 | Name: testName, 54 | Value: fmt.Sprintf("Replacement test entry created by libdns %s", time.Now()), 55 | TTL: time.Duration(30) * time.Second, 56 | ID: testId, 57 | }}) 58 | if err != nil { 59 | fmt.Printf("ERROR: %s\n", err.Error()) 60 | } 61 | } else { 62 | fmt.Printf("Creating new entry for %s\n", testName) 63 | _, err = provider.AppendRecords(context.TODO(), zone, []libdns.Record{libdns.Record{ 64 | Type: "TXT", 65 | Name: testName, 66 | Value: fmt.Sprintf("This is a test entry created by libdns %s", time.Now()), 67 | TTL: time.Duration(30) * time.Second, 68 | }}) 69 | if err != nil { 70 | fmt.Printf("ERROR: %s\n", err.Error()) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package digitalocean 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "sync" 7 | "time" 8 | 9 | "github.com/digitalocean/godo" 10 | "github.com/libdns/libdns" 11 | ) 12 | 13 | type Client struct { 14 | client *godo.Client 15 | mutex sync.Mutex 16 | } 17 | 18 | func (p *Provider) getClient() error { 19 | if p.client == nil { 20 | p.client = godo.NewFromToken(p.APIToken) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (p *Provider) getDNSEntries(ctx context.Context, zone string) ([]libdns.Record, error) { 27 | p.mutex.Lock() 28 | defer p.mutex.Unlock() 29 | 30 | p.getClient() 31 | 32 | opt := &godo.ListOptions{} 33 | var records []libdns.Record 34 | for { 35 | domains, resp, err := p.client.Domains.Records(ctx, zone, opt) 36 | if err != nil { 37 | return records, err 38 | } 39 | 40 | for _, entry := range domains { 41 | record := libdns.Record{ 42 | Name: entry.Name, 43 | Value: entry.Data, 44 | Type: entry.Type, 45 | TTL: time.Duration(entry.TTL) * time.Second, 46 | ID: strconv.Itoa(entry.ID), 47 | } 48 | records = append(records, record) 49 | } 50 | 51 | // if we are at the last page, break out the for loop 52 | if resp.Links == nil || resp.Links.IsLastPage() { 53 | break 54 | } 55 | 56 | page, err := resp.Links.CurrentPage() 57 | if err != nil { 58 | return records, err 59 | } 60 | 61 | // set the page we want for the next request 62 | opt.Page = page + 1 63 | } 64 | 65 | return records, nil 66 | } 67 | 68 | func (p *Provider) addDNSEntry(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) { 69 | p.mutex.Lock() 70 | defer p.mutex.Unlock() 71 | 72 | p.getClient() 73 | 74 | entry := godo.DomainRecordEditRequest{ 75 | Name: record.Name, 76 | Data: record.Value, 77 | Type: record.Type, 78 | TTL: int(record.TTL.Seconds()), 79 | } 80 | 81 | rec, _, err := p.client.Domains.CreateRecord(ctx, zone, &entry) 82 | if err != nil { 83 | return record, err 84 | } 85 | record.ID = strconv.Itoa(rec.ID) 86 | 87 | return record, nil 88 | } 89 | 90 | func (p *Provider) removeDNSEntry(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) { 91 | p.mutex.Lock() 92 | defer p.mutex.Unlock() 93 | 94 | p.getClient() 95 | 96 | id, err := strconv.Atoi(record.ID) 97 | if err != nil { 98 | return record, err 99 | } 100 | 101 | _, err = p.client.Domains.DeleteRecord(ctx, zone, id) 102 | if err != nil { 103 | return record, err 104 | } 105 | 106 | return record, nil 107 | } 108 | 109 | func (p *Provider) updateDNSEntry(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) { 110 | p.mutex.Lock() 111 | defer p.mutex.Unlock() 112 | 113 | p.getClient() 114 | 115 | id, err := strconv.Atoi(record.ID) 116 | if err != nil { 117 | return record, err 118 | } 119 | 120 | entry := godo.DomainRecordEditRequest{ 121 | Name: record.Name, 122 | Data: record.Value, 123 | Type: record.Type, 124 | TTL: int(record.TTL.Seconds()), 125 | } 126 | 127 | _, _, err = p.client.Domains.EditRecord(ctx, zone, id, &entry) 128 | if err != nil { 129 | return record, err 130 | } 131 | 132 | return record, nil 133 | } 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/libdns/digitalocean 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/digitalocean/godo v1.41.0 8 | github.com/libdns/libdns v0.2.1 9 | github.com/stretchr/testify v1.5.1 // indirect 10 | gopkg.in/yaml.v2 v2.2.8 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/digitalocean/godo v1.41.0 h1:WYy7MIVVhTMZUNB+UA3irl2V9FyDJeDttsifYyn7jYA= 6 | github.com/digitalocean/godo v1.41.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 10 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 11 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 12 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 13 | github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= 14 | github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 19 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 20 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 22 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 23 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 24 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 25 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 26 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 27 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 28 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 29 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 33 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 36 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 37 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 41 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 42 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package digitalocean 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/libdns/libdns" 9 | ) 10 | 11 | // Provider implements the libdns interfaces for DigitalOcean 12 | type Provider struct { 13 | Client 14 | // APIToken is the DigitalOcean API token - see https://www.digitalocean.com/docs/apis-clis/api/create-personal-access-token/ 15 | APIToken string `json:"auth_token"` 16 | } 17 | 18 | // unFQDN trims any trailing "." from fqdn. DigitalOcean's API does not use FQDNs. 19 | func (p *Provider) unFQDN(fqdn string) string { 20 | return strings.TrimSuffix(fqdn, ".") 21 | } 22 | 23 | // GetRecords lists all the records in the zone. 24 | func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) { 25 | records, err := p.getDNSEntries(ctx, p.unFQDN(zone)) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return records, nil 31 | } 32 | 33 | // AppendRecords adds records to the zone. It returns the records that were added. 34 | func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { 35 | var appendedRecords []libdns.Record 36 | 37 | for _, record := range records { 38 | newRecord, err := p.addDNSEntry(ctx, p.unFQDN(zone), record) 39 | if err != nil { 40 | return nil, err 41 | } 42 | newRecord.TTL = time.Duration(newRecord.TTL) * time.Second 43 | appendedRecords = append(appendedRecords, newRecord) 44 | } 45 | 46 | return appendedRecords, nil 47 | } 48 | 49 | // DeleteRecords deletes the records from the zone. 50 | func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { 51 | var deletedRecords []libdns.Record 52 | 53 | for _, record := range records { 54 | deletedRecord, err := p.removeDNSEntry(ctx, p.unFQDN(zone), record) 55 | if err != nil { 56 | return nil, err 57 | } 58 | deletedRecord.TTL = time.Duration(deletedRecord.TTL) * time.Second 59 | deletedRecords = append(deletedRecords, deletedRecord) 60 | } 61 | 62 | return deletedRecords, nil 63 | } 64 | 65 | // SetRecords sets the records in the zone, either by updating existing records 66 | // or creating new ones. It returns the updated records. 67 | func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { 68 | var setRecords []libdns.Record 69 | 70 | for _, record := range records { 71 | // TODO: if there is no ID, look up the Name, and fill it in, or call 72 | // newRecord, err := p.addDNSEntry(ctx, zone, record) 73 | setRecord, err := p.updateDNSEntry(ctx, p.unFQDN(zone), record) 74 | if err != nil { 75 | return setRecords, err 76 | } 77 | setRecord.TTL = time.Duration(setRecord.TTL) * time.Second 78 | setRecords = append(setRecords, setRecord) 79 | } 80 | 81 | return setRecords, nil 82 | } 83 | 84 | // Interface guards 85 | var ( 86 | _ libdns.RecordGetter = (*Provider)(nil) 87 | _ libdns.RecordAppender = (*Provider)(nil) 88 | _ libdns.RecordSetter = (*Provider)(nil) 89 | _ libdns.RecordDeleter = (*Provider)(nil) 90 | ) 91 | --------------------------------------------------------------------------------