github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/providers/vultr/vultrProvider.go (about) 1 package vultr 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 8 vultr "github.com/JamesClonk/vultr/lib" 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 16 /* 17 18 Vultr API DNS provider: 19 20 Info required in `creds.json`: 21 - token 22 23 */ 24 25 var features = providers.DocumentationNotes{ 26 providers.CanUseAlias: providers.Cannot(), 27 providers.CanUseCAA: providers.Can(), 28 providers.CanUsePTR: providers.Cannot(), 29 providers.CanUseSRV: providers.Can(), 30 providers.CanUseTLSA: providers.Cannot(), 31 providers.DocCreateDomains: providers.Can(), 32 providers.DocOfficiallySupported: providers.Cannot(), 33 } 34 35 func init() { 36 providers.RegisterDomainServiceProviderType("VULTR", NewVultr, features) 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, errors.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, errors.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 // Normalize 101 models.PostProcessRecords(curRecords) 102 103 differ := diff.New(dc) 104 _, create, delete, modify := differ.IncrementalDiff(curRecords) 105 106 corrections := []*models.Correction{} 107 108 for _, mod := range delete { 109 id := mod.Existing.Original.(*vultr.DNSRecord).RecordID 110 corrections = append(corrections, &models.Correction{ 111 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id), 112 F: func() error { 113 return api.client.DeleteDNSRecord(dc.Name, id) 114 }, 115 }) 116 } 117 118 for _, mod := range create { 119 r := toVultrRecord(dc, mod.Desired) 120 corrections = append(corrections, &models.Correction{ 121 Msg: mod.String(), 122 F: func() error { 123 return api.client.CreateDNSRecord(dc.Name, r.Name, r.Type, r.Data, r.Priority, r.TTL) 124 }, 125 }) 126 } 127 128 for _, mod := range modify { 129 id := mod.Existing.Original.(*vultr.DNSRecord).RecordID 130 r := toVultrRecord(dc, mod.Desired) 131 r.RecordID = id 132 corrections = append(corrections, &models.Correction{ 133 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id), 134 F: func() error { 135 return api.client.UpdateDNSRecord(dc.Name, *r) 136 }, 137 }) 138 } 139 140 return corrections, nil 141 } 142 143 // GetNameservers gets the Vultr nameservers for a domain 144 func (api *VultrApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 145 return models.StringsToNameservers(defaultNS), nil 146 } 147 148 // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist 149 func (api *VultrApi) EnsureDomainExists(domain string) error { 150 ok, err := api.isDomainInAccount(domain) 151 if err != nil { 152 return err 153 } 154 155 if !ok { 156 // Vultr requires an initial IP, use a dummy one 157 err := api.client.CreateDNSDomain(domain, "127.0.0.1") 158 if err != nil { 159 return err 160 } 161 162 ok, err := api.isDomainInAccount(domain) 163 if err != nil { 164 return err 165 } 166 if !ok { 167 return errors.Errorf("Unexpected error adding domain %s to Vultr account", domain) 168 } 169 } 170 171 return nil 172 } 173 174 func (api *VultrApi) isDomainInAccount(domain string) (bool, error) { 175 domains, err := api.client.GetDNSDomains() 176 if err != nil { 177 return false, err 178 } 179 180 var vd *vultr.DNSDomain 181 for _, d := range domains { 182 if d.Domain == domain { 183 vd = &d 184 } 185 } 186 187 if vd == nil { 188 return false, nil 189 } 190 191 return true, nil 192 } 193 194 // toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations 195 func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) { 196 origin := dc.Name 197 data := r.Data 198 rc := &models.RecordConfig{ 199 TTL: uint32(r.TTL), 200 Original: r, 201 } 202 rc.SetLabel(r.Name, dc.Name) 203 204 switch rtype := r.Type; rtype { 205 case "CNAME", "NS": 206 rc.Type = r.Type 207 // Make target into a FQDN if it is a CNAME, NS, MX, or SRV 208 if !strings.HasSuffix(data, ".") { 209 data = data + "." 210 } 211 // FIXME(tlim): the AddOrigin() might be unneeded. Please test. 212 return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin)) 213 case "CAA": 214 // Vultr returns in the format "[flag] [tag] [value]" 215 return rc, rc.SetTargetCAAString(data) 216 case "MX": 217 if !strings.HasSuffix(data, ".") { 218 data = data + "." 219 } 220 return rc, rc.SetTargetMX(uint16(r.Priority), data) 221 case "SRV": 222 // Vultr returns in the format "[weight] [port] [target]" 223 return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data) 224 case "TXT": 225 // Remove quotes if it is a TXT 226 if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) { 227 return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr") 228 } 229 return rc, rc.SetTargetTXT(data[1 : len(data)-1]) 230 default: 231 return rc, rc.PopulateFromString(rtype, r.Data, origin) 232 } 233 } 234 235 // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations 236 func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord { 237 name := rc.GetLabel() 238 // Vultr uses a blank string to represent the apex domain 239 if name == "@" { 240 name = "" 241 } 242 243 data := rc.GetTargetField() 244 245 // Vultr does not use a period suffix for the server for CNAME, NS, or MX 246 if strings.HasSuffix(data, ".") { 247 data = data[:len(data)-1] 248 } 249 // Vultr needs TXT record in quotes 250 if rc.Type == "TXT" { 251 data = fmt.Sprintf(`"%s"`, data) 252 } 253 254 priority := 0 255 256 if rc.Type == "MX" { 257 priority = int(rc.MxPreference) 258 } 259 if rc.Type == "SRV" { 260 priority = int(rc.SrvPriority) 261 } 262 263 r := &vultr.DNSRecord{ 264 Type: rc.Type, 265 Name: name, 266 Data: data, 267 TTL: int(rc.TTL), 268 Priority: priority, 269 } 270 switch rtype := rc.Type; rtype { // #rtype_variations 271 case "SRV": 272 target := rc.GetTargetField() 273 if strings.HasSuffix(target, ".") { 274 target = target[:len(target)-1] 275 } 276 r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target) 277 case "CAA": 278 r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField()) 279 default: 280 } 281 282 return r 283 }