github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/digitalocean/digitaloceanProvider.go (about) 1 package digitalocean 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 9 "github.com/StackExchange/dnscontrol/models" 10 "github.com/StackExchange/dnscontrol/providers" 11 "github.com/StackExchange/dnscontrol/providers/diff" 12 "github.com/miekg/dns/dnsutil" 13 14 "github.com/digitalocean/godo" 15 "golang.org/x/oauth2" 16 ) 17 18 /* 19 20 Digitalocean API DNS provider: 21 22 Info required in `creds.json`: 23 - token 24 25 */ 26 27 type DoApi struct { 28 client *godo.Client 29 } 30 31 var defaultNameServerNames = []string{ 32 "ns1.digitalocean.com", 33 "ns2.digitalocean.com", 34 "ns3.digitalocean.com", 35 } 36 37 func NewDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 38 if m["token"] == "" { 39 return nil, fmt.Errorf("Digitalocean Token must be provided.") 40 } 41 42 ctx := context.Background() 43 oauthClient := oauth2.NewClient( 44 ctx, 45 oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}), 46 ) 47 client := godo.NewClient(oauthClient) 48 49 api := &DoApi{client: client} 50 51 // Get a domain to validate the token 52 _, resp, err := api.client.Domains.List(ctx, &godo.ListOptions{PerPage: 1}) 53 if err != nil { 54 return nil, err 55 } 56 if resp.StatusCode != http.StatusOK { 57 return nil, fmt.Errorf("Digitalocean Token is not valid.") 58 } 59 60 return api, nil 61 } 62 63 var docNotes = providers.DocumentationNotes{ 64 providers.DocCreateDomains: providers.Can(), 65 providers.DocOfficiallySupported: providers.Cannot(), 66 } 67 68 func init() { 69 providers.RegisterDomainServiceProviderType("DIGITALOCEAN", NewDo, providers.CanUseSRV, docNotes) 70 } 71 72 func (api *DoApi) EnsureDomainExists(domain string) error { 73 ctx := context.Background() 74 _, resp, err := api.client.Domains.Get(ctx, domain) 75 if resp.StatusCode == http.StatusNotFound { 76 _, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{ 77 Name: domain, 78 IPAddress: "", 79 }) 80 return err 81 } else { 82 return err 83 } 84 } 85 86 func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 87 return models.StringsToNameservers(defaultNameServerNames), nil 88 } 89 90 func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 91 ctx := context.Background() 92 dc.Punycode() 93 94 records, err := getRecords(api, dc.Name) 95 if err != nil { 96 return nil, err 97 } 98 99 existingRecords := make([]*models.RecordConfig, len(records)) 100 for i := range records { 101 existingRecords[i] = toRc(dc, &records[i]) 102 } 103 104 differ := diff.New(dc) 105 _, create, delete, modify := differ.IncrementalDiff(existingRecords) 106 107 var corrections = []*models.Correction{} 108 109 // Deletes first so changing type works etc. 110 for _, m := range delete { 111 id := m.Existing.Original.(*godo.DomainRecord).ID 112 corr := &models.Correction{ 113 Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id), 114 F: func() error { 115 _, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id) 116 return err 117 }, 118 } 119 corrections = append(corrections, corr) 120 } 121 for _, m := range create { 122 req := toReq(dc, m.Desired) 123 corr := &models.Correction{ 124 Msg: m.String(), 125 F: func() error { 126 _, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req) 127 return err 128 }, 129 } 130 corrections = append(corrections, corr) 131 } 132 for _, m := range modify { 133 id := m.Existing.Original.(*godo.DomainRecord).ID 134 req := toReq(dc, m.Desired) 135 corr := &models.Correction{ 136 Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id), 137 F: func() error { 138 _, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req) 139 return err 140 }, 141 } 142 corrections = append(corrections, corr) 143 } 144 145 return corrections, nil 146 } 147 148 func getRecords(api *DoApi, name string) ([]godo.DomainRecord, error) { 149 ctx := context.Background() 150 151 records := []godo.DomainRecord{} 152 opt := &godo.ListOptions{} 153 for { 154 result, resp, err := api.client.Domains.Records(ctx, name, opt) 155 if err != nil { 156 return nil, err 157 } 158 159 for _, d := range result { 160 records = append(records, d) 161 } 162 163 if resp.Links == nil || resp.Links.IsLastPage() { 164 break 165 } 166 167 page, err := resp.Links.CurrentPage() 168 if err != nil { 169 return nil, err 170 } 171 172 opt.Page = page + 1 173 } 174 175 return records, nil 176 } 177 178 func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig { 179 // This handles "@" etc. 180 name := dnsutil.AddOrigin(r.Name, dc.Name) 181 182 target := r.Data 183 // Make target FQDN (#rtype_variations) 184 if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" { 185 // If target is the domainname, e.g. cname foo.example.com -> example.com, 186 // DO returns "@" on read even if fqdn was written. 187 if target == "@" { 188 target = dc.Name 189 } 190 target = dnsutil.AddOrigin(target+".", dc.Name) 191 } 192 193 return &models.RecordConfig{ 194 NameFQDN: name, 195 Type: r.Type, 196 Target: target, 197 TTL: uint32(r.TTL), 198 MxPreference: uint16(r.Priority), 199 SrvPriority: uint16(r.Priority), 200 SrvWeight: uint16(r.Weight), 201 SrvPort: uint16(r.Port), 202 Original: r, 203 } 204 } 205 206 func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest { 207 // DO wants the short name, e.g. @ 208 name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name) 209 210 // DO uses the same property for MX and SRV priority 211 priority := 0 212 switch rc.Type { // #rtype_variations 213 case "MX": 214 priority = int(rc.MxPreference) 215 case "SRV": 216 priority = int(rc.SrvPriority) 217 } 218 219 return &godo.DomainRecordEditRequest{ 220 Type: rc.Type, 221 Name: name, 222 Data: rc.Target, 223 TTL: int(rc.TTL), 224 Priority: priority, 225 Port: int(rc.SrvPort), 226 Weight: int(rc.SrvWeight), 227 } 228 }