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