github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/linode/linodeProvider.go (about) 1 package linode 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "regexp" 10 "strings" 11 12 "github.com/miekg/dns/dnsutil" 13 "golang.org/x/oauth2" 14 15 "github.com/StackExchange/dnscontrol/v2/models" 16 "github.com/StackExchange/dnscontrol/v2/providers" 17 "github.com/StackExchange/dnscontrol/v2/providers/diff" 18 ) 19 20 /* 21 22 Linode API DNS provider: 23 24 Info required in `creds.json`: 25 - token 26 27 */ 28 29 var allowedTTLValues = []uint32{ 30 300, // 5 minutes 31 3600, // 1 hour 32 7200, // 2 hours 33 14400, // 4 hours 34 28800, // 8 hours 35 57600, // 16 hours 36 86400, // 1 day 37 172800, // 2 days 38 345600, // 4 days 39 604800, // 1 week 40 1209600, // 2 weeks 41 2419200, // 4 weeks 42 } 43 44 var srvRegexp = regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`) 45 46 // LinodeApi is the handle for this provider. 47 type LinodeApi struct { 48 client *http.Client 49 baseURL *url.URL 50 domainIndex map[string]int 51 } 52 53 var defaultNameServerNames = []string{ 54 "ns1.linode.com", 55 "ns2.linode.com", 56 "ns3.linode.com", 57 "ns4.linode.com", 58 "ns5.linode.com", 59 } 60 61 // NewLinode creates the provider. 62 func NewLinode(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 63 if m["token"] == "" { 64 return nil, fmt.Errorf("Missing Linode token") 65 } 66 67 ctx := context.Background() 68 client := oauth2.NewClient( 69 ctx, 70 oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}), 71 ) 72 73 baseURL, err := url.Parse(defaultBaseURL) 74 if err != nil { 75 return nil, fmt.Errorf("Linode base URL not valid") 76 } 77 78 api := &LinodeApi{client: client, baseURL: baseURL} 79 80 // Get a domain to validate the token 81 if err := api.fetchDomainList(); err != nil { 82 return nil, err 83 } 84 85 return api, nil 86 } 87 88 var features = providers.DocumentationNotes{ 89 providers.DocDualHost: providers.Cannot(), 90 providers.DocOfficiallySupported: providers.Cannot(), 91 providers.CanGetZones: providers.Unimplemented(), 92 } 93 94 func init() { 95 // SRV support is in this provider, but Linode doesn't seem to support it properly 96 providers.RegisterDomainServiceProviderType("LINODE", NewLinode, features) 97 } 98 99 // GetNameservers returns the nameservers for a domain. 100 func (api *LinodeApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 101 return models.StringsToNameservers(defaultNameServerNames), nil 102 } 103 104 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 105 func (client *LinodeApi) GetZoneRecords(domain string) (models.Records, error) { 106 return nil, fmt.Errorf("not implemented") 107 // This enables the get-zones subcommand. 108 // Implement this by extracting the code from GetDomainCorrections into 109 // a single function. For most providers this should be relatively easy. 110 } 111 112 // GetDomainCorrections returns the corrections for a domain. 113 func (api *LinodeApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 114 dc, err := dc.Copy() 115 if err != nil { 116 return nil, err 117 } 118 119 dc.Punycode() 120 121 if api.domainIndex == nil { 122 if err := api.fetchDomainList(); err != nil { 123 return nil, err 124 } 125 } 126 domainID, ok := api.domainIndex[dc.Name] 127 if !ok { 128 return nil, fmt.Errorf("'%s' not a zone in Linode account", dc.Name) 129 } 130 131 records, err := api.getRecords(domainID) 132 if err != nil { 133 return nil, err 134 } 135 136 existingRecords := make([]*models.RecordConfig, len(records), len(records)+len(defaultNameServerNames)) 137 for i := range records { 138 existingRecords[i] = toRc(dc, &records[i]) 139 } 140 141 // Linode always has read-only NS servers, but these are not mentioned in the API response 142 // https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/constants.js#L184 143 for _, name := range defaultNameServerNames { 144 rc := &models.RecordConfig{ 145 Type: "NS", 146 Original: &domainRecord{}, 147 } 148 rc.SetLabelFromFQDN(dc.Name, dc.Name) 149 rc.SetTarget(name) 150 151 existingRecords = append(existingRecords, rc) 152 } 153 154 // Normalize 155 models.PostProcessRecords(existingRecords) 156 157 // Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values 158 // We need to make sure we don't change it every time if it is as close as it's going to get 159 // By experimentation, Linode always rounds up. 300 -> 300, 301 -> 3600. 160 // https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/domains/components/SelectDNSSeconds.js#L19 161 for _, record := range dc.Records { 162 record.TTL = fixTTL(record.TTL) 163 } 164 165 differ := diff.New(dc) 166 _, create, del, modify := differ.IncrementalDiff(existingRecords) 167 168 var corrections []*models.Correction 169 170 // Deletes first so changing type works etc. 171 for _, m := range del { 172 id := m.Existing.Original.(*domainRecord).ID 173 if id == 0 { // Skip ID 0, these are the default nameservers always present 174 continue 175 } 176 corr := &models.Correction{ 177 Msg: fmt.Sprintf("%s, Linode ID: %d", m.String(), id), 178 F: func() error { 179 return api.deleteRecord(domainID, id) 180 }, 181 } 182 corrections = append(corrections, corr) 183 } 184 for _, m := range create { 185 req, err := toReq(dc, m.Desired) 186 if err != nil { 187 return nil, err 188 } 189 j, err := json.Marshal(req) 190 if err != nil { 191 return nil, err 192 } 193 corr := &models.Correction{ 194 Msg: fmt.Sprintf("%s: %s", m.String(), string(j)), 195 F: func() error { 196 record, err := api.createRecord(domainID, req) 197 if err != nil { 198 return err 199 } 200 // TTL isn't saved when creating a record, so we will need to modify it immediately afterwards 201 return api.modifyRecord(domainID, record.ID, req) 202 }, 203 } 204 corrections = append(corrections, corr) 205 } 206 for _, m := range modify { 207 id := m.Existing.Original.(*domainRecord).ID 208 if id == 0 { // Skip ID 0, these are the default nameservers always present 209 continue 210 } 211 req, err := toReq(dc, m.Desired) 212 if err != nil { 213 return nil, err 214 } 215 j, err := json.Marshal(req) 216 if err != nil { 217 return nil, err 218 } 219 corr := &models.Correction{ 220 Msg: fmt.Sprintf("%s, Linode ID: %d: %s", m.String(), id, string(j)), 221 F: func() error { 222 return api.modifyRecord(domainID, id, req) 223 }, 224 } 225 corrections = append(corrections, corr) 226 } 227 228 return corrections, nil 229 } 230 231 func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig { 232 rc := &models.RecordConfig{ 233 Type: r.Type, 234 TTL: r.TTLSec, 235 MxPreference: r.Priority, 236 SrvPriority: r.Priority, 237 SrvWeight: r.Weight, 238 SrvPort: uint16(r.Port), 239 Original: r, 240 } 241 rc.SetLabel(r.Name, dc.Name) 242 243 switch rtype := r.Type; rtype { // #rtype_variations 244 case "TXT": 245 rc.SetTargetTXT(r.Target) 246 case "CNAME", "MX", "NS", "SRV": 247 rc.SetTarget(dnsutil.AddOrigin(r.Target+".", dc.Name)) 248 default: 249 rc.SetTarget(r.Target) 250 } 251 252 return rc 253 } 254 255 func toReq(dc *models.DomainConfig, rc *models.RecordConfig) (*recordEditRequest, error) { 256 req := &recordEditRequest{ 257 Type: rc.Type, 258 Name: rc.GetLabel(), 259 Target: rc.GetTargetField(), 260 TTL: int(rc.TTL), 261 Priority: 0, 262 Port: int(rc.SrvPort), 263 Weight: int(rc.SrvWeight), 264 } 265 266 // Linode doesn't use "@", it uses an empty name 267 if req.Name == "@" { 268 req.Name = "" 269 } 270 271 // Linode uses the same property for MX and SRV priority 272 switch rc.Type { // #rtype_variations 273 case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "TLSA", "CAA": 274 // Nothing special. 275 case "MX": 276 req.Priority = int(rc.MxPreference) 277 req.Target = fixTarget(req.Target, dc.Name) 278 case "SRV": 279 req.Priority = int(rc.SrvPriority) 280 281 // From softlayer provider 282 // This is to support SRV, it doesn't work yet for Linode 283 result := srvRegexp.FindStringSubmatch(req.Name) 284 285 if len(result) != 3 { 286 return nil, fmt.Errorf("SRV Record must match format \"_service._protocol\" not %s", req.Name) 287 } 288 289 var serviceName, protocol string = result[1], strings.ToLower(result[2]) 290 291 req.Protocol = protocol 292 req.Service = serviceName 293 req.Name = "" 294 case "CNAME": 295 req.Target = fixTarget(req.Target, dc.Name) 296 default: 297 msg := fmt.Sprintf("linode.toReq rtype %v unimplemented", rc.Type) 298 panic(msg) 299 // We panic so that we quickly find any switch statements 300 // that have not been updated for a new RR type. 301 } 302 303 return req, nil 304 } 305 306 func fixTarget(target, domain string) string { 307 // Linode always wants a fully qualified target name 308 if target[len(target)-1] == '.' { 309 return target[:len(target)-1] 310 } 311 return fmt.Sprintf("%s.%s", target, domain) 312 } 313 314 func fixTTL(ttl uint32) uint32 { 315 // if the TTL is larger than the largest allowed value, return the largest allowed value 316 if ttl > allowedTTLValues[len(allowedTTLValues)-1] { 317 return allowedTTLValues[len(allowedTTLValues)-1] 318 } 319 320 for _, v := range allowedTTLValues { 321 if v >= ttl { 322 return v 323 } 324 } 325 326 return allowedTTLValues[0] 327 }