github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/vultr/vultrProvider.go (about) 1 package vultr 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 11 "github.com/miekg/dns/dnsutil" 12 "github.com/vultr/govultr" 13 14 "github.com/StackExchange/dnscontrol/v2/models" 15 "github.com/StackExchange/dnscontrol/v2/providers" 16 "github.com/StackExchange/dnscontrol/v2/providers/diff" 17 ) 18 19 /* 20 21 Vultr API DNS provider: 22 23 Info required in `creds.json`: 24 - token 25 26 */ 27 28 var features = providers.DocumentationNotes{ 29 providers.CanUseAlias: providers.Cannot(), 30 providers.CanUseCAA: providers.Can(), 31 providers.CanUsePTR: providers.Cannot(), 32 providers.CanUseSRV: providers.Can(), 33 providers.CanUseTLSA: providers.Cannot(), 34 providers.CanUseSSHFP: providers.Can(), 35 providers.DocCreateDomains: providers.Can(), 36 providers.DocOfficiallySupported: providers.Cannot(), 37 providers.CanGetZones: providers.Unimplemented(), 38 } 39 40 func init() { 41 providers.RegisterDomainServiceProviderType("VULTR", NewProvider, features) 42 } 43 44 // Provider represents the Vultr DNSServiceProvider. 45 type Provider struct { 46 client *govultr.Client 47 token string 48 } 49 50 // defaultNS contains the default nameservers for Vultr. 51 var defaultNS = []string{ 52 "ns1.vultr.com", 53 "ns2.vultr.com", 54 } 55 56 // NewProvider initializes a Vultr DNSServiceProvider. 57 func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 58 token := m["token"] 59 if token == "" { 60 return nil, fmt.Errorf("Vultr API token is required") 61 } 62 63 client := govultr.NewClient(nil, token) 64 client.SetUserAgent("dnscontrol") 65 66 _, err := client.Account.GetInfo(context.Background()) 67 return &Provider{client, token}, err 68 } 69 70 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 71 func (client *Provider) GetZoneRecords(domain string) (models.Records, error) { 72 return nil, fmt.Errorf("not implemented") 73 // This enables the get-zones subcommand. 74 // Implement this by extracting the code from GetDomainCorrections into 75 // a single function. For most providers this should be relatively easy. 76 } 77 78 // GetDomainCorrections gets the corrections for a DomainConfig. 79 func (api *Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 80 dc.Punycode() 81 82 records, err := api.client.DNSRecord.List(context.Background(), dc.Name) 83 if err != nil { 84 return nil, err 85 } 86 87 curRecords := make([]*models.RecordConfig, len(records)) 88 for i := range records { 89 r, err := toRecordConfig(dc, &records[i]) 90 if err != nil { 91 return nil, err 92 } 93 curRecords[i] = r 94 } 95 96 models.PostProcessRecords(curRecords) 97 98 differ := diff.New(dc) 99 _, create, delete, modify := differ.IncrementalDiff(curRecords) 100 101 var corrections []*models.Correction 102 103 for _, mod := range delete { 104 id := mod.Existing.Original.(*govultr.DNSRecord).RecordID 105 corrections = append(corrections, &models.Correction{ 106 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id), 107 F: func() error { 108 return api.client.DNSRecord.Delete(context.Background(), dc.Name, strconv.Itoa(id)) 109 }, 110 }) 111 } 112 113 for _, mod := range create { 114 r := toVultrRecord(dc, mod.Desired, 0) 115 corrections = append(corrections, &models.Correction{ 116 Msg: mod.String(), 117 F: func() error { 118 return api.client.DNSRecord.Create(context.Background(), dc.Name, r.Type, r.Name, r.Data, r.TTL, r.Priority) 119 }, 120 }) 121 } 122 123 for _, mod := range modify { 124 r := toVultrRecord(dc, mod.Desired, mod.Existing.Original.(*govultr.DNSRecord).RecordID) 125 corrections = append(corrections, &models.Correction{ 126 Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), r.RecordID), 127 F: func() error { 128 return api.client.DNSRecord.Update(context.Background(), dc.Name, r) 129 }, 130 }) 131 } 132 133 return corrections, nil 134 } 135 136 // GetNameservers gets the Vultr nameservers for a domain 137 func (api *Provider) GetNameservers(domain string) ([]*models.Nameserver, error) { 138 return models.StringsToNameservers(defaultNS), nil 139 } 140 141 // EnsureDomainExists adds a domain to the Vutr DNS service if it does not exist 142 func (api *Provider) EnsureDomainExists(domain string) error { 143 if ok, err := api.isDomainInAccount(domain); err != nil { 144 return err 145 } else if ok { 146 return nil 147 } 148 149 // Vultr requires an initial IP, use a dummy one. 150 return api.client.DNSDomain.Create(context.Background(), domain, "0.0.0.0") 151 } 152 153 func (api *Provider) isDomainInAccount(domain string) (bool, error) { 154 domains, err := api.client.DNSDomain.List(context.Background()) 155 if err != nil { 156 return false, err 157 } 158 for _, d := range domains { 159 if d.Domain == domain { 160 return true, nil 161 } 162 } 163 return false, nil 164 } 165 166 // toRecordConfig converts a Vultr DNSRecord to a RecordConfig. #rtype_variations 167 func toRecordConfig(dc *models.DomainConfig, r *govultr.DNSRecord) (*models.RecordConfig, error) { 168 origin, data := dc.Name, r.Data 169 rc := &models.RecordConfig{ 170 TTL: uint32(r.TTL), 171 Original: r, 172 } 173 rc.SetLabel(r.Name, dc.Name) 174 175 switch rtype := r.Type; rtype { 176 case "CNAME", "NS": 177 rc.Type = r.Type 178 // Make target into a FQDN if it is a CNAME, NS, MX, or SRV. 179 if !strings.HasSuffix(data, ".") { 180 data = data + "." 181 } 182 // FIXME(tlim): the AddOrigin() might be unneeded. Please test. 183 return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin)) 184 case "CAA": 185 // Vultr returns CAA records in the format "[flag] [tag] [value]". 186 return rc, rc.SetTargetCAAString(data) 187 case "MX": 188 if !strings.HasSuffix(data, ".") { 189 data = data + "." 190 } 191 return rc, rc.SetTargetMX(uint16(r.Priority), data) 192 case "SRV": 193 // Vultr returns SRV records in the format "[weight] [port] [target]". 194 return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data) 195 case "TXT": 196 // Remove quotes if it is a TXT record. 197 if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) { 198 return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr") 199 } 200 return rc, rc.SetTargetTXT(data[1 : len(data)-1]) 201 default: 202 return rc, rc.PopulateFromString(rtype, r.Data, origin) 203 } 204 } 205 206 // toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord. #rtype_variations 207 func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig, vultrID int) *govultr.DNSRecord { 208 name := rc.GetLabel() 209 // Vultr uses a blank string to represent the apex domain. 210 if name == "@" { 211 name = "" 212 } 213 214 data := rc.GetTargetField() 215 216 // Vultr does not use a period suffix for CNAME, NS, or MX. 217 if strings.HasSuffix(data, ".") { 218 data = data[:len(data)-1] 219 } 220 // Vultr needs TXT record in quotes. 221 if rc.Type == "TXT" { 222 data = fmt.Sprintf(`"%s"`, data) 223 } 224 225 priority := 0 226 227 if rc.Type == "MX" { 228 priority = int(rc.MxPreference) 229 } 230 if rc.Type == "SRV" { 231 priority = int(rc.SrvPriority) 232 } 233 234 r := &govultr.DNSRecord{ 235 RecordID: vultrID, 236 Type: rc.Type, 237 Name: name, 238 Data: data, 239 TTL: int(rc.TTL), 240 Priority: priority, 241 } 242 switch rtype := rc.Type; rtype { // #rtype_variations 243 case "SRV": 244 r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) 245 case "CAA": 246 r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField()) 247 case "SSHFP": 248 r.Data = fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField()) 249 default: 250 } 251 252 return r 253 }