github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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 "github.com/pkg/errors" 14 15 "github.com/digitalocean/godo" 16 "golang.org/x/oauth2" 17 ) 18 19 /* 20 21 Digitalocean API DNS provider: 22 23 Info required in `creds.json`: 24 - token 25 26 */ 27 28 // DoApi is the handle for operations. 29 type DoApi struct { 30 client *godo.Client 31 } 32 33 var defaultNameServerNames = []string{ 34 "ns1.digitalocean.com", 35 "ns2.digitalocean.com", 36 "ns3.digitalocean.com", 37 } 38 39 // NewDo creates a DO-specific DNS provider. 40 func NewDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 41 if m["token"] == "" { 42 return nil, errors.Errorf("no Digitalocean token provided") 43 } 44 45 ctx := context.Background() 46 oauthClient := oauth2.NewClient( 47 ctx, 48 oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}), 49 ) 50 client := godo.NewClient(oauthClient) 51 52 api := &DoApi{client: client} 53 54 // Get a domain to validate the token 55 _, resp, err := api.client.Domains.List(ctx, &godo.ListOptions{PerPage: 1}) 56 if err != nil { 57 return nil, err 58 } 59 if resp.StatusCode != http.StatusOK { 60 return nil, errors.Errorf("token for digitalocean is not valid") 61 } 62 63 return api, nil 64 } 65 66 var features = providers.DocumentationNotes{ 67 providers.DocCreateDomains: providers.Can(), 68 providers.DocOfficiallySupported: providers.Cannot(), 69 providers.CanUseSRV: providers.Can(), 70 } 71 72 func init() { 73 providers.RegisterDomainServiceProviderType("DIGITALOCEAN", NewDo, features) 74 } 75 76 // EnsureDomainExists returns an error if domain doesn't exist. 77 func (api *DoApi) EnsureDomainExists(domain string) error { 78 ctx := context.Background() 79 _, resp, err := api.client.Domains.Get(ctx, domain) 80 if resp.StatusCode == http.StatusNotFound { 81 _, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{ 82 Name: domain, 83 IPAddress: "", 84 }) 85 return err 86 } 87 return err 88 } 89 90 // GetNameservers returns the nameservers for domain. 91 func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 92 return models.StringsToNameservers(defaultNameServerNames), nil 93 } 94 95 // GetDomainCorrections returns a list of corretions for the domain. 96 func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 97 ctx := context.Background() 98 dc.Punycode() 99 100 records, err := getRecords(api, dc.Name) 101 if err != nil { 102 return nil, err 103 } 104 105 existingRecords := make([]*models.RecordConfig, len(records)) 106 for i := range records { 107 existingRecords[i] = toRc(dc, &records[i]) 108 } 109 110 // Normalize 111 models.PostProcessRecords(existingRecords) 112 113 differ := diff.New(dc) 114 _, create, delete, modify := differ.IncrementalDiff(existingRecords) 115 116 var corrections = []*models.Correction{} 117 118 // Deletes first so changing type works etc. 119 for _, m := range delete { 120 id := m.Existing.Original.(*godo.DomainRecord).ID 121 corr := &models.Correction{ 122 Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id), 123 F: func() error { 124 _, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id) 125 return err 126 }, 127 } 128 corrections = append(corrections, corr) 129 } 130 for _, m := range create { 131 req := toReq(dc, m.Desired) 132 corr := &models.Correction{ 133 Msg: m.String(), 134 F: func() error { 135 _, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req) 136 return err 137 }, 138 } 139 corrections = append(corrections, corr) 140 } 141 for _, m := range modify { 142 id := m.Existing.Original.(*godo.DomainRecord).ID 143 req := toReq(dc, m.Desired) 144 corr := &models.Correction{ 145 Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id), 146 F: func() error { 147 _, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req) 148 return err 149 }, 150 } 151 corrections = append(corrections, corr) 152 } 153 154 return corrections, nil 155 } 156 157 func getRecords(api *DoApi, name string) ([]godo.DomainRecord, error) { 158 ctx := context.Background() 159 160 records := []godo.DomainRecord{} 161 opt := &godo.ListOptions{} 162 for { 163 result, resp, err := api.client.Domains.Records(ctx, name, opt) 164 if err != nil { 165 return nil, err 166 } 167 168 for _, d := range result { 169 records = append(records, d) 170 } 171 172 if resp.Links == nil || resp.Links.IsLastPage() { 173 break 174 } 175 176 page, err := resp.Links.CurrentPage() 177 if err != nil { 178 return nil, err 179 } 180 181 opt.Page = page + 1 182 } 183 184 return records, nil 185 } 186 187 func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig { 188 // This handles "@" etc. 189 name := dnsutil.AddOrigin(r.Name, dc.Name) 190 191 target := r.Data 192 // Make target FQDN (#rtype_variations) 193 if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" { 194 // If target is the domainname, e.g. cname foo.example.com -> example.com, 195 // DO returns "@" on read even if fqdn was written. 196 if target == "@" { 197 target = dc.Name 198 } 199 target = dnsutil.AddOrigin(target+".", dc.Name) 200 // FIXME(tlim): The AddOrigin should be a no-op. 201 // Test whether or not it is actually needed. 202 } 203 204 t := &models.RecordConfig{ 205 Type: r.Type, 206 TTL: uint32(r.TTL), 207 MxPreference: uint16(r.Priority), 208 SrvPriority: uint16(r.Priority), 209 SrvWeight: uint16(r.Weight), 210 SrvPort: uint16(r.Port), 211 Original: r, 212 } 213 t.SetLabelFromFQDN(name, dc.Name) 214 t.SetTarget(target) 215 switch rtype := r.Type; rtype { 216 case "TXT": 217 t.SetTargetTXTString(target) 218 default: 219 // nothing additional required 220 } 221 return t 222 } 223 224 func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest { 225 name := rc.GetLabel() // DO wants the short name or "@" for apex. 226 target := rc.GetTargetField() // DO uses the target field only for a single value 227 priority := 0 // DO uses the same property for MX and SRV priority 228 229 switch rc.Type { // #rtype_variations 230 case "MX": 231 priority = int(rc.MxPreference) 232 case "SRV": 233 priority = int(rc.SrvPriority) 234 case "TXT": 235 // TXT records are the one place where DO combines many items into one field. 236 target = rc.GetTargetCombined() 237 default: 238 // no action required 239 } 240 241 return &godo.DomainRecordEditRequest{ 242 Type: rc.Type, 243 Name: name, 244 Data: target, 245 TTL: int(rc.TTL), 246 Priority: priority, 247 Port: int(rc.SrvPort), 248 Weight: int(rc.SrvWeight), 249 } 250 }