github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/dnsimple/dnsimpleProvider.go (about) 1 package dnsimple 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 10 "github.com/StackExchange/dnscontrol/models" 11 "github.com/StackExchange/dnscontrol/providers" 12 "github.com/StackExchange/dnscontrol/providers/diff" 13 "github.com/miekg/dns/dnsutil" 14 15 dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple" 16 ) 17 18 var docNotes = providers.DocumentationNotes{ 19 providers.DocDualHost: providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"), 20 providers.DocCreateDomains: providers.Cannot(), 21 providers.DocOfficiallySupported: providers.Cannot(), 22 providers.CanUseTLSA: providers.Cannot(), 23 } 24 25 func init() { 26 providers.RegisterRegistrarType("DNSIMPLE", newReg) 27 providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp, providers.CanUsePTR, providers.CanUseAlias, providers.CanUseCAA, providers.CanUseSRV, docNotes) 28 } 29 30 const stateRegistered = "registered" 31 32 var defaultNameServerNames = []string{ 33 "ns1.dnsimple.com", 34 "ns2.dnsimple.com", 35 "ns3.dnsimple.com", 36 "ns4.dnsimple.com", 37 } 38 39 type DnsimpleApi struct { 40 AccountToken string // The account access token 41 BaseURL string // An alternate base URI 42 accountId string // Account id cache 43 } 44 45 func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) { 46 return models.StringsToNameservers(defaultNameServerNames), nil 47 } 48 49 func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 50 corrections := []*models.Correction{} 51 dc.Punycode() 52 records, err := c.getRecords(dc.Name) 53 if err != nil { 54 return nil, err 55 } 56 57 var actual []*models.RecordConfig 58 for _, r := range records { 59 if r.Type == "SOA" || r.Type == "NS" { 60 continue 61 } 62 if r.Name == "" { 63 r.Name = "@" 64 } 65 if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "SRV" { 66 r.Content += "." 67 } 68 // dnsimple adds these odd txt records that mirror the alias records. 69 // they seem to manage them on deletes and things, so we'll just pretend they don't exist 70 if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") { 71 continue 72 } 73 rec := &models.RecordConfig{ 74 NameFQDN: dnsutil.AddOrigin(r.Name, dc.Name), 75 Type: r.Type, 76 Target: r.Content, 77 TTL: uint32(r.TTL), 78 MxPreference: uint16(r.Priority), 79 Original: r, 80 } 81 if r.Type == "CAA" || r.Type == "SRV" { 82 rec.CombinedTarget = true 83 } 84 actual = append(actual, rec) 85 } 86 removeOtherNS(dc) 87 dc.Filter(func(r *models.RecordConfig) bool { 88 if r.Type == "CAA" || r.Type == "SRV" { 89 r.MergeToTarget() 90 } 91 return true 92 }) 93 differ := diff.New(dc) 94 _, create, delete, modify := differ.IncrementalDiff(actual) 95 96 for _, del := range delete { 97 rec := del.Existing.Original.(dnsimpleapi.ZoneRecord) 98 corrections = append(corrections, &models.Correction{ 99 Msg: del.String(), 100 F: c.deleteRecordFunc(rec.ID, dc.Name), 101 }) 102 } 103 104 for _, cre := range create { 105 rec := cre.Desired 106 corrections = append(corrections, &models.Correction{ 107 Msg: cre.String(), 108 F: c.createRecordFunc(rec, dc.Name), 109 }) 110 } 111 112 for _, mod := range modify { 113 old := mod.Existing.Original.(dnsimpleapi.ZoneRecord) 114 new := mod.Desired 115 corrections = append(corrections, &models.Correction{ 116 Msg: mod.String(), 117 F: c.updateRecordFunc(&old, new, dc.Name), 118 }) 119 } 120 121 return corrections, nil 122 } 123 124 func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 125 corrections := []*models.Correction{} 126 127 nameServers, err := c.getNameservers(dc.Name) 128 if err != nil { 129 return nil, err 130 } 131 132 actual := strings.Join(nameServers, ",") 133 134 expectedSet := []string{} 135 for _, ns := range dc.Nameservers { 136 expectedSet = append(expectedSet, ns.Name) 137 } 138 sort.Strings(expectedSet) 139 expected := strings.Join(expectedSet, ",") 140 141 if actual != expected { 142 return []*models.Correction{ 143 { 144 Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected), 145 F: c.updateNameserversFunc(expectedSet, dc.Name), 146 }, 147 }, nil 148 } 149 150 return corrections, nil 151 } 152 153 // DNSimple calls 154 155 func (c *DnsimpleApi) getClient() *dnsimpleapi.Client { 156 client := dnsimpleapi.NewClient(dnsimpleapi.NewOauthTokenCredentials(c.AccountToken)) 157 if c.BaseURL != "" { 158 client.BaseURL = c.BaseURL 159 } 160 return client 161 } 162 163 func (c *DnsimpleApi) getAccountId() (string, error) { 164 if c.accountId == "" { 165 client := c.getClient() 166 whoamiResponse, err := client.Identity.Whoami() 167 if err != nil { 168 return "", err 169 } 170 if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil { 171 return "", fmt.Errorf("DNSimple token appears to be a user token. Please supply an account token") 172 } 173 c.accountId = strconv.Itoa(whoamiResponse.Data.Account.ID) 174 } 175 return c.accountId, nil 176 } 177 178 func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) { 179 client := c.getClient() 180 181 accountId, err := c.getAccountId() 182 if err != nil { 183 return nil, err 184 } 185 186 opts := &dnsimpleapi.ZoneRecordListOptions{} 187 recs := []dnsimpleapi.ZoneRecord{} 188 opts.Page = 1 189 for { 190 recordsResponse, err := client.Zones.ListRecords(accountId, domainName, opts) 191 if err != nil { 192 return nil, err 193 } 194 recs = append(recs, recordsResponse.Data...) 195 pg := recordsResponse.Pagination 196 if pg.CurrentPage == pg.TotalPages { 197 break 198 } 199 opts.Page++ 200 } 201 202 return recs, nil 203 } 204 205 // Returns the name server names that should be used. If the domain is registered 206 // then this method will return the delegation name servers. If this domain 207 // is hosted only, then it will return the default DNSimple name servers. 208 func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) { 209 client := c.getClient() 210 211 accountId, err := c.getAccountId() 212 if err != nil { 213 return nil, err 214 } 215 216 domainResponse, err := client.Domains.GetDomain(accountId, domainName) 217 if err != nil { 218 return nil, err 219 } 220 221 if domainResponse.Data.State == stateRegistered { 222 223 delegationResponse, err := client.Registrar.GetDomainDelegation(accountId, domainName) 224 if err != nil { 225 return nil, err 226 } 227 228 return *delegationResponse.Data, nil 229 } else { 230 return defaultNameServerNames, nil 231 } 232 } 233 234 // Returns a function that can be invoked to change the delegation of the domain to the given name server names. 235 func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error { 236 return func() error { 237 client := c.getClient() 238 239 accountId, err := c.getAccountId() 240 if err != nil { 241 return err 242 } 243 244 nameServers := dnsimpleapi.Delegation(nameServerNames) 245 246 _, err = client.Registrar.ChangeDomainDelegation(accountId, domainName, &nameServers) 247 if err != nil { 248 return err 249 } 250 251 return nil 252 } 253 } 254 255 // Returns a function that can be invoked to create a record in a zone. 256 func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error { 257 return func() error { 258 client := c.getClient() 259 260 accountId, err := c.getAccountId() 261 if err != nil { 262 return err 263 } 264 record := dnsimpleapi.ZoneRecord{ 265 Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName), 266 Type: rc.Type, 267 Content: rc.Target, 268 TTL: int(rc.TTL), 269 Priority: int(rc.MxPreference), 270 } 271 _, err = client.Zones.CreateRecord(accountId, domainName, record) 272 if err != nil { 273 return err 274 } 275 276 return nil 277 } 278 } 279 280 // Returns a function that can be invoked to delete a record in a zone. 281 func (c *DnsimpleApi) deleteRecordFunc(recordId int, domainName string) func() error { 282 return func() error { 283 client := c.getClient() 284 285 accountId, err := c.getAccountId() 286 if err != nil { 287 return err 288 } 289 290 _, err = client.Zones.DeleteRecord(accountId, domainName, recordId) 291 if err != nil { 292 return err 293 } 294 295 return nil 296 297 } 298 } 299 300 // Returns a function that can be invoked to update a record in a zone. 301 func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error { 302 return func() error { 303 client := c.getClient() 304 305 accountId, err := c.getAccountId() 306 if err != nil { 307 return err 308 } 309 310 record := dnsimpleapi.ZoneRecord{ 311 Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName), 312 Type: rc.Type, 313 Content: rc.Target, 314 TTL: int(rc.TTL), 315 Priority: int(rc.MxPreference), 316 } 317 318 _, err = client.Zones.UpdateRecord(accountId, domainName, old.ID, record) 319 if err != nil { 320 return err 321 } 322 323 return nil 324 } 325 } 326 327 // constructors 328 329 func newReg(conf map[string]string) (providers.Registrar, error) { 330 return newProvider(conf, nil) 331 } 332 333 func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 334 return newProvider(conf, metadata) 335 } 336 337 func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) { 338 api := &DnsimpleApi{} 339 api.AccountToken = m["token"] 340 if api.AccountToken == "" { 341 return nil, fmt.Errorf("DNSimple token must be provided.") 342 } 343 344 if m["baseurl"] != "" { 345 api.BaseURL = m["baseurl"] 346 } 347 348 return api, nil 349 } 350 351 // remove all non-dnsimple NS records from our desired state. 352 // if any are found, print a warning 353 func removeOtherNS(dc *models.DomainConfig) { 354 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 355 for _, rec := range dc.Records { 356 if rec.Type == "NS" { 357 // apex NS inside dnsimple are expected. 358 if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.Target, ".dnsimple.com.") { 359 continue 360 } 361 fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.Target) 362 continue 363 } 364 newList = append(newList, rec) 365 } 366 dc.Records = newList 367 }