github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/vultr/vultr.go (about) 1 package vultr 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 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 vultr "github.com/JamesClonk/vultr/lib" 16 ) 17 18 /* 19 20 Vultr API DNS provider: 21 22 Info required in `creds.json`: 23 - token 24 25 */ 26 27 var docNotes = providers.DocumentationNotes{ 28 providers.DocCreateDomains: providers.Can(), 29 providers.DocOfficiallySupported: providers.Cannot(), 30 providers.CanUseAlias: providers.Cannot(), 31 providers.CanUseTLSA: providers.Cannot(), 32 providers.CanUsePTR: providers.Cannot(), 33 } 34 35 func init() { 36 providers.RegisterDomainServiceProviderType("VULTR", NewVultr, providers.CanUseSRV, providers.CanUseCAA, docNotes) 37 } 38 39 // VultrApi represents the Vultr DNSServiceProvider 40 type VultrApi struct { 41 client *vultr.Client 42 token string 43 } 44 45 // defaultNS are the default nameservers for Vultr 46 var defaultNS = []string{ 47 "ns1.vultr.com", 48 "ns2.vultr.com", 49 } 50 51 // NewVultr initializes a Vultr DNSServiceProvider 52 func NewVultr(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 53 api := &VultrApi{ 54 token: m["token"], 55 } 56 57 if api.token == "" { 58 return nil, fmt.Errorf("Vultr API token is required") 59 } 60 61 api.client = vultr.NewClient(api.token, nil) 62 63 // Validate token 64 _, err := api.client.GetAccountInfo() 65 if err != nil { 66 return nil, err 67 } 68 69 return api, nil 70 } 71 72 // GetDomainCorrections gets the corrections for a DomainConfig 73 func (api *VultrApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 74 dc.Punycode() 75 76 ok, err := api.isDomainInAccount(dc.Name) 77 if err != nil { 78 return nil, err 79 } 80 81 if !ok { 82 return nil, fmt.Errorf("%s is not a domain in the Vultr account", dc.Name) 83 } 84 85 records, err := api.client.GetDNSRecords(dc.Name) 86 if err != nil { 87 return nil, err 88 } 89 90 curRecords := make([]*models.RecordConfig, len(records)) 91 for i := range records { 92 r, err := toRecordConfig(dc, &records[i]) 93 if err != nil { 94 return nil, err 95 } 96 97 curRecords[i] = r 98 } 99 100 differ := diff.New(dc) 101 _, create, delete, modify := differ.IncrementalDiff(curRecords) 102 103 corrections := []*models.Correction{} 104 105 for _, mod := range delete { 106 id := mod.Existing.Original.(*vultr.DNSRecord).RecordID 107 corrections = append(corrections, &models.Correction{ 108 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id), 109 F: func() error { 110 return api.client.DeleteDNSRecord(dc.Name, id) 111 }, 112 }) 113 } 114 115 for _, mod := range create { 116 r := toVultrRecord(dc, mod.Desired) 117 corrections = append(corrections, &models.Correction{ 118 Msg: mod.String(), 119 F: func() error { 120 return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL) 121 }, 122 }) 123 } 124 125 for _, mod := range modify { 126 id := mod.Existing.Original.(*vultr.DNSRecord).RecordID 127 r := toVultrRecord(dc, mod.Desired) 128 r.RecordID = id 129 corrections = append(corrections, &models.Correction{ 130 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id), 131 F: func() error { 132 return api.client.UpdateDNSRecord(dc.Name, *r) 133 }, 134 }) 135 } 136 137 return corrections, nil 138 } 139 140 // GetNameservers gets the Vultr nameservers for a domain 141 func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 142 return models.StringsToNameservers(defaultNS), nil 143 } 144 145 // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist 146 func (api *VultrApi) EnsureDomainExists(domain string) error { 147 ok, err := api.isDomainInAccount(domain) 148 if err != nil { 149 return err 150 } 151 152 if !ok { 153 // Vultr requires an initial IP, use a dummy one 154 err := api.client.CreateDNSDomain(domain, "127.0.0.1") 155 if err != nil { 156 return err 157 } 158 159 ok, err := api.isDomainInAccount(domain) 160 if err != nil { 161 return err 162 } 163 if !ok { 164 return fmt.Errorf("Unexpected error adding domain %s to Vultr account", domain) 165 } 166 } 167 168 return nil 169 } 170 171 func (api *VultrApi) isDomainInAccount(domain string) (bool, error) { 172 domains, err := api.client.GetDNSDomains() 173 if err != nil { 174 return false, err 175 } 176 177 var vd *vultr.DNSDomain 178 for _, d := range domains { 179 if d.Domain == domain { 180 vd = &d 181 } 182 } 183 184 if vd == nil { 185 return false, nil 186 } 187 188 return true, nil 189 } 190 191 // toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations 192 func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) { 193 // Turns r.Name into a FQDN 194 // Vultr uses "" as the apex domain, instead of "@", and this handles it fine. 195 name := dnsutil.AddOrigin(r.Name, dc.Name) 196 197 data := r.Data 198 // Make target into a FQDN if it is a CNAME, NS, MX, or SRV 199 if r.Type == "CNAME" || r.Type == "NS" || r.Type == "MX" { 200 if !strings.HasSuffix(data, ".") { 201 data = data + "." 202 } 203 data = dnsutil.AddOrigin(data, dc.Name) 204 } 205 // Remove quotes if it is a TXT 206 if r.Type == "TXT" { 207 if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) { 208 return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr") 209 } 210 data = data[1 : len(data)-1] 211 } 212 213 rc := &models.RecordConfig{ 214 NameFQDN: name, 215 Type: r.Type, 216 Target: data, 217 TTL: uint32(r.TTL), 218 Original: r, 219 } 220 221 if r.Type == "MX" { 222 rc.MxPreference = uint16(r.Priority) 223 } 224 225 if r.Type == "SRV" { 226 rc.SrvPriority = uint16(r.Priority) 227 228 // Vultr returns in the format "[weight] [port] [target]" 229 splitData := strings.SplitN(rc.Target, " ", 3) 230 if len(splitData) != 3 { 231 return nil, fmt.Errorf("Unexpected data for SRV record returned by Vultr") 232 } 233 234 weight, err := strconv.ParseUint(splitData[0], 10, 16) 235 if err != nil { 236 return nil, err 237 } 238 rc.SrvWeight = uint16(weight) 239 240 port, err := strconv.ParseUint(splitData[1], 10, 16) 241 if err != nil { 242 return nil, err 243 } 244 rc.SrvPort = uint16(port) 245 246 target := splitData[2] 247 if !strings.HasSuffix(target, ".") { 248 target = target + "." 249 } 250 rc.Target = dnsutil.AddOrigin(target, dc.Name) 251 } 252 253 if r.Type == "CAA" { 254 // Vultr returns in the format "[flag] [tag] [value]" 255 splitData := strings.SplitN(rc.Target, " ", 3) 256 if len(splitData) != 3 { 257 return nil, fmt.Errorf("Unexpected data for CAA record returned by Vultr") 258 } 259 260 flag, err := strconv.ParseUint(splitData[0], 10, 8) 261 if err != nil { 262 return nil, err 263 } 264 rc.CaaFlag = uint8(flag) 265 266 rc.CaaTag = splitData[1] 267 268 value := splitData[2] 269 if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { 270 value = value[1 : len(value)-1] 271 } 272 if strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`) { 273 value = value[1 : len(value)-1] 274 } 275 rc.Target = value 276 } 277 278 return rc, nil 279 } 280 281 // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations 282 func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord { 283 name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name) 284 285 // Vultr uses a blank string to represent the apex domain 286 if name == "@" { 287 name = "" 288 } 289 290 data := rc.Target 291 292 // Vultr does not use a period suffix for the server for CNAME, NS, or MX 293 if strings.HasSuffix(data, ".") { 294 data = data[:len(data)-1] 295 } 296 // Vultr needs TXT record in quotes 297 if rc.Type == "TXT" { 298 data = fmt.Sprintf(`"%s"`, data) 299 } 300 301 priority := 0 302 303 if rc.Type == "MX" { 304 priority = int(rc.MxPreference) 305 } 306 307 if rc.Type == "SRV" { 308 priority = int(rc.SrvPriority) 309 } 310 311 r := &vultr.DNSRecord{ 312 Type: rc.Type, 313 Name: name, 314 Data: data, 315 TTL: int(rc.TTL), 316 Priority: priority, 317 } 318 319 if rc.Type == "SRV" { 320 target := rc.Target 321 if strings.HasSuffix(target, ".") { 322 target = target[:len(target)-1] 323 } 324 325 r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target) 326 } 327 328 if rc.Type == "CAA" { 329 r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.Target) 330 } 331 332 return r 333 }