mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
New provider: Linode (#268)
This commit is contained in:
committed by
Tom Limoncelli
parent
25df50634d
commit
9a44e785ac
1
OWNERS
1
OWNERS
@ -5,6 +5,7 @@ providers/digitalocean @Deraen
|
||||
providers/dnsimple @aeden
|
||||
providers/gandi @TomOnTime
|
||||
# providers/gcloud
|
||||
providers/linode @koesie10
|
||||
providers/namecheap @captncraig
|
||||
# providers/namedotcom
|
||||
providers/ns1 @captncraig
|
||||
|
@ -20,6 +20,7 @@ Currently supported DNS providers:
|
||||
- DNSimple
|
||||
- Gandi
|
||||
- Google
|
||||
- Linode
|
||||
- Namecheap
|
||||
- Name.com
|
||||
- NS1
|
||||
|
@ -13,6 +13,7 @@
|
||||
<th class="rotate"><div><span>DNSIMPLE</span></div></th>
|
||||
<th class="rotate"><div><span>GANDI</span></div></th>
|
||||
<th class="rotate"><div><span>GCLOUD</span></div></th>
|
||||
<th class="rotate"><div><span>LINODE</span></div></th>
|
||||
<th class="rotate"><div><span>NAMECHEAP</span></div></th>
|
||||
<th class="rotate"><div><span>NAMEDOTCOM</span></div></th>
|
||||
<th class="rotate"><div><span>NS1</span></div></th>
|
||||
@ -49,6 +50,9 @@
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -91,6 +95,9 @@
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -157,6 +164,9 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider supports some kind of ALIAS, ANAME or flattened CNAME record type">ALIAS</th>
|
||||
@ -173,6 +183,7 @@
|
||||
</td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -214,6 +225,7 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="The namecheap web console allows you to make SRV records, but their api does not let you read or set them">
|
||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -253,6 +265,7 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -288,6 +301,7 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -317,6 +331,7 @@
|
||||
</td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td><i class="fa fa-minus dim"></i></td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -350,6 +365,9 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Doesn't allow control of apex NS records">
|
||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -391,6 +409,9 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Requires domain registered through their service">
|
||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
@ -436,6 +457,9 @@
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="success">
|
||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||
</td>
|
||||
<td class="danger">
|
||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||
</td>
|
||||
|
57
docs/_providers/linode.md
Normal file
57
docs/_providers/linode.md
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
name: Linode
|
||||
title: Linode Provider
|
||||
layout: default
|
||||
jsId: LINODE
|
||||
---
|
||||
# Linode Provider
|
||||
|
||||
## Configuration
|
||||
In your credentials file, you must provide your
|
||||
[Linode Personal Access Token](https://cloud.linode.com/profile/tokens)
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"linode": {
|
||||
"token": "your-linode-personal-access-token"
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
## Metadata
|
||||
This provider does not recognize any special metadata fields unique to Linode.
|
||||
|
||||
## Usage
|
||||
Example Javascript:
|
||||
|
||||
{% highlight js %}
|
||||
var REG_NONE = NewRegistrar('none', 'NONE')
|
||||
var LINODE = NewDnsProvider("linode", "LINODE");
|
||||
|
||||
D("example.tld", REG_NONE, DnsProvider(LINODE),
|
||||
A("test","1.2.3.4")
|
||||
);
|
||||
{%endhighlight%}
|
||||
|
||||
## Activation
|
||||
[Create Personal Access Token](https://cloud.linode.com/profile/tokens)
|
||||
|
||||
## Caveats
|
||||
Linode does not allow all TTLs, but only a specific subset of TTLs. The following TTLs are supported
|
||||
([source](https://github.com/linode/manager/blob/master/src/domains/components/SelectDNSSeconds.js)):
|
||||
|
||||
- 300
|
||||
- 3600
|
||||
- 7200
|
||||
- 14400
|
||||
- 28800
|
||||
- 57600
|
||||
- 86400
|
||||
- 172800
|
||||
- 345600
|
||||
- 604800
|
||||
- 1209600
|
||||
- 2419200
|
||||
|
||||
The provider will automatically round up your TTL to one of these values. For example, 600 seconds would become 3600
|
||||
seconds, but 300 seconds would stay 300 seconds.
|
@ -63,6 +63,7 @@ Maintainers of contributed providers:
|
||||
* digital ocean @Deraen
|
||||
* dnsimple @aeden
|
||||
* gandi @TomOnTime
|
||||
* Linode @koesie10
|
||||
* namecheap @captncraig
|
||||
* ns1 @captncraig
|
||||
* OVH @masterzen
|
||||
@ -83,7 +84,6 @@ code to support this provider, please re-open the issue. We'd be glad to help in
|
||||
<li>GoDaddy (<a href="https://github.com/StackExchange/dnscontrol/issues/145">#145</a>)</li>
|
||||
<li>Hurricane Electric (dns.he.net) (<a href="https://github.com/StackExchange/dnscontrol/issues/118">#118</a>)</li>
|
||||
<li>INWX (<a href="https://github.com/StackExchange/dnscontrol/issues/254">#254</a>)</li>
|
||||
<li>Linode (<a href="https://github.com/StackExchange/dnscontrol/issues/121">#121</a>)</li>
|
||||
<li>NameSilo (<a href="https://github.com/StackExchange/dnscontrol/issues/220">#220</a>)</li>
|
||||
<li>OVH (<a href="https://github.com/StackExchange/dnscontrol/issues/143">#143</a>)</li>
|
||||
</ul>
|
||||
|
@ -36,6 +36,12 @@
|
||||
"private_key": "$GCLOUD_PRIVATEKEY",
|
||||
"project_id": "$GCLOUD_PROJECT"
|
||||
},
|
||||
"LINODE": {
|
||||
"COMMENT": "25: Linode's hostname validation does not allow the target domain TLD",
|
||||
"token": "$LINODE_TOKEN",
|
||||
"domain": "$LINODE_DOMAIN",
|
||||
"knownFailures": "25"
|
||||
},
|
||||
"NS1": {
|
||||
"domain": "$NS1_DOMAIN",
|
||||
"api_token": "$NS1_TOKEN"
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
_ "github.com/StackExchange/dnscontrol/providers/dnsimple"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/gandi"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/gcloud"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/linode"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/namecheap"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/namedotcom"
|
||||
_ "github.com/StackExchange/dnscontrol/providers/ns1"
|
||||
|
245
providers/linode/api.go
Normal file
245
providers/linode/api.go
Normal file
@ -0,0 +1,245 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
mediaType = "application/json"
|
||||
defaultBaseURL = "https://api.linode.com/v4/"
|
||||
domainsPath = "domains"
|
||||
)
|
||||
|
||||
func (c *LinodeApi) fetchDomainList() error {
|
||||
c.domainIndex = map[string]int{}
|
||||
page := 1
|
||||
for {
|
||||
dr := &domainResponse{}
|
||||
endpoint := fmt.Sprintf("%s?page=%d", domainsPath, page)
|
||||
if err := c.get(endpoint, dr); err != nil {
|
||||
return fmt.Errorf("Error fetching domain list from Linode: %s", err)
|
||||
}
|
||||
for _, domain := range dr.Data {
|
||||
c.domainIndex[domain.Domain] = domain.ID
|
||||
}
|
||||
if len(dr.Data) == 0 || dr.Page >= dr.Pages {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) getRecords(id int) ([]domainRecord, error) {
|
||||
records := []domainRecord{}
|
||||
page := 1
|
||||
for {
|
||||
dr := &recordResponse{}
|
||||
endpoint := fmt.Sprintf("%s/%d/records?page=%d", domainsPath, id, page)
|
||||
if err := c.get(endpoint, dr); err != nil {
|
||||
return nil, fmt.Errorf("Error fetching record list from Linode: %s", err)
|
||||
}
|
||||
|
||||
records = append(records, dr.Data...)
|
||||
|
||||
if len(dr.Data) == 0 || dr.Page >= dr.Pages {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) createRecord(domainID int, rec *recordEditRequest) (*domainRecord, error) {
|
||||
endpoint := fmt.Sprintf("%s/%d/records", domainsPath, domainID)
|
||||
|
||||
req, err := c.newRequest(http.MethodPost, endpoint, rec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, c.handleErrors(resp)
|
||||
}
|
||||
|
||||
record := &domainRecord{}
|
||||
|
||||
defer resp.Body.Close()
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
if err := decoder.Decode(record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) modifyRecord(domainID, recordID int, rec *recordEditRequest) error {
|
||||
endpoint := fmt.Sprintf("%s/%d/records/%d", domainsPath, domainID, recordID)
|
||||
|
||||
req, err := c.newRequest(http.MethodPut, endpoint, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.handleErrors(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) deleteRecord(domainID, recordID int) error {
|
||||
endpoint := fmt.Sprintf("%s/%d/records/%d", domainsPath, domainID, recordID)
|
||||
req, err := c.newRequest(http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.handleErrors(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) newRequest(method, endpoint string, body interface{}) (*http.Request, error) {
|
||||
rel, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := c.baseURL.ResolveReference(rel)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", mediaType)
|
||||
req.Header.Add("Accept", mediaType)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *LinodeApi) get(endpoint string, target interface{}) error {
|
||||
req, err := c.newRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.handleErrors(resp)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
return decoder.Decode(target)
|
||||
}
|
||||
|
||||
func (c *LinodeApi) handleErrors(resp *http.Response) error {
|
||||
defer resp.Body.Close()
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
errs := &errorResponse{}
|
||||
|
||||
if err := decoder.Decode(errs); err != nil {
|
||||
return fmt.Errorf("Bad status code from Linode: %d not 200. Failed to decode response.", resp.StatusCode)
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString(fmt.Sprintf("Bad status code from Linode: %d not 200.", resp.StatusCode))
|
||||
|
||||
for _, err := range errs.Errors {
|
||||
buf.WriteString("\n- ")
|
||||
|
||||
if err.Field != "" {
|
||||
buf.WriteString(err.Field)
|
||||
buf.WriteString(": ")
|
||||
}
|
||||
|
||||
buf.WriteString(err.Reason)
|
||||
}
|
||||
|
||||
return errors.New(buf.String())
|
||||
}
|
||||
|
||||
type basicResponse struct {
|
||||
Results int `json:"results"`
|
||||
Pages int `json:"pages"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
type domainResponse struct {
|
||||
basicResponse
|
||||
Data []struct {
|
||||
ID int `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type recordResponse struct {
|
||||
basicResponse
|
||||
Data []domainRecord `json:"data"`
|
||||
}
|
||||
|
||||
type domainRecord struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Target string `json:"target"`
|
||||
Priority uint16 `json:"priority"`
|
||||
Weight uint16 `json:"weight"`
|
||||
Port uint16 `json:"port"`
|
||||
Service string `json:"service"`
|
||||
Protocol string `json:"protocol"`
|
||||
TTLSec uint32 `json:"ttl_sec"`
|
||||
}
|
||||
|
||||
type recordEditRequest struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
// Documented as field `ttl` in the documentation, but in reality `ttl_sec` should be used
|
||||
TTL int `json:"ttl_sec,omitempty"`
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
Errors []struct {
|
||||
Field string `json:"field"`
|
||||
Reason string `json:"reason"`
|
||||
} `json:"errors"`
|
||||
}
|
313
providers/linode/linodeProvider.go
Normal file
313
providers/linode/linodeProvider.go
Normal file
@ -0,0 +1,313 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Linode API DNS provider:
|
||||
|
||||
Info required in `creds.json`:
|
||||
- token
|
||||
|
||||
*/
|
||||
|
||||
var allowedTTLValues = []uint32{
|
||||
300, // 5 minutes
|
||||
3600, // 1 hour
|
||||
7200, // 2 hours
|
||||
14400, // 4 hours
|
||||
28800, // 8 hours
|
||||
57600, // 16 hours
|
||||
86400, // 1 day
|
||||
172800, // 2 days
|
||||
345600, // 4 days
|
||||
604800, // 1 week
|
||||
1209600, // 2 weeks
|
||||
2419200, // 4 weeks
|
||||
}
|
||||
|
||||
var srvRegexp = regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`)
|
||||
|
||||
type LinodeApi struct {
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
domainIndex map[string]int
|
||||
}
|
||||
|
||||
var defaultNameServerNames = []string{
|
||||
"ns1.linode.com",
|
||||
"ns2.linode.com",
|
||||
"ns3.linode.com",
|
||||
"ns4.linode.com",
|
||||
"ns5.linode.com",
|
||||
}
|
||||
|
||||
func NewLinode(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
if m["token"] == "" {
|
||||
return nil, fmt.Errorf("Linode Token must be provided.")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client := oauth2.NewClient(
|
||||
ctx,
|
||||
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
|
||||
)
|
||||
|
||||
baseURL, err := url.Parse(defaultBaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Linode base URL not valid")
|
||||
}
|
||||
|
||||
api := &LinodeApi{client: client, baseURL: baseURL}
|
||||
|
||||
// Get a domain to validate the token
|
||||
if err := api.fetchDomainList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
var docNotes = providers.DocumentationNotes{
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
providers.DocDualHost: providers.Cannot(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
// SRV support is in this provider, but Linode doesn't seem to support it properly
|
||||
providers.RegisterDomainServiceProviderType("LINODE", NewLinode, docNotes)
|
||||
}
|
||||
|
||||
func (api *LinodeApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
return models.StringsToNameservers(defaultNameServerNames), nil
|
||||
}
|
||||
|
||||
func (api *LinodeApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc, err := dc.Copy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dc.Punycode()
|
||||
|
||||
if api.domainIndex == nil {
|
||||
if err := api.fetchDomainList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
domainID, ok := api.domainIndex[dc.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s not listed in domains for Linode account", dc.Name)
|
||||
}
|
||||
|
||||
records, err := api.getRecords(domainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingRecords := make([]*models.RecordConfig, len(records), len(records)+len(defaultNameServerNames))
|
||||
for i := range records {
|
||||
existingRecords[i] = toRc(dc, &records[i])
|
||||
}
|
||||
|
||||
// Linode always has read-only NS servers, but these are not mentioned in the API response
|
||||
// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/constants.js#L184
|
||||
for _, name := range defaultNameServerNames {
|
||||
existingRecords = append(existingRecords, &models.RecordConfig{
|
||||
NameFQDN: dc.Name,
|
||||
Type: "NS",
|
||||
Target: name,
|
||||
Original: &domainRecord{},
|
||||
})
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.Downcase(existingRecords)
|
||||
|
||||
// Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values
|
||||
// We need to make sure we don't change it every time if it is as close as it's going to get
|
||||
// By experimentation, Linode always rounds up. 300 -> 300, 301 -> 3600.
|
||||
// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/domains/components/SelectDNSSeconds.js#L19
|
||||
for _, record := range dc.Records {
|
||||
record.TTL = fixTTL(record.TTL)
|
||||
}
|
||||
|
||||
differ := diff.New(dc)
|
||||
_, create, del, modify := differ.IncrementalDiff(existingRecords)
|
||||
|
||||
var corrections []*models.Correction
|
||||
|
||||
// Deletes first so changing type works etc.
|
||||
for _, m := range del {
|
||||
id := m.Existing.Original.(*domainRecord).ID
|
||||
if id == 0 { // Skip ID 0, these are the default nameservers always present
|
||||
continue
|
||||
}
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("%s, Linode ID: %d", m.String(), id),
|
||||
F: func() error {
|
||||
return api.deleteRecord(domainID, id)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
for _, m := range create {
|
||||
req, err := toReq(dc, m.Desired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
j, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("%s: %s", m.String(), string(j)),
|
||||
F: func() error {
|
||||
record, err := api.createRecord(domainID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TTL isn't saved when creating a record, so we will need to modify it immediately afterwards
|
||||
return api.modifyRecord(domainID, record.ID, req)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
for _, m := range modify {
|
||||
id := m.Existing.Original.(*domainRecord).ID
|
||||
if id == 0 { // Skip ID 0, these are the default nameservers always present
|
||||
continue
|
||||
}
|
||||
req, err := toReq(dc, m.Desired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
j, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("%s, Linode ID: %d: %s", m.String(), id, string(j)),
|
||||
F: func() error {
|
||||
return api.modifyRecord(domainID, id, req)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig {
|
||||
// This handles "@" etc.
|
||||
name := dnsutil.AddOrigin(r.Name, dc.Name)
|
||||
|
||||
target := r.Target
|
||||
// Make target FQDN (#rtype_variations)
|
||||
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
|
||||
target = dnsutil.AddOrigin(target+".", dc.Name)
|
||||
}
|
||||
|
||||
return &models.RecordConfig{
|
||||
NameFQDN: name,
|
||||
Type: r.Type,
|
||||
Target: target,
|
||||
TTL: r.TTLSec,
|
||||
MxPreference: r.Priority,
|
||||
SrvPriority: r.Priority,
|
||||
SrvWeight: r.Weight,
|
||||
SrvPort: uint16(r.Port),
|
||||
Original: r,
|
||||
}
|
||||
}
|
||||
|
||||
func toReq(dc *models.DomainConfig, rc *models.RecordConfig) (*recordEditRequest, error) {
|
||||
req := &recordEditRequest{
|
||||
Type: rc.Type,
|
||||
Name: dnsutil.TrimDomainName(rc.NameFQDN, dc.Name),
|
||||
Target: rc.Target,
|
||||
TTL: int(rc.TTL),
|
||||
Priority: 0,
|
||||
Port: int(rc.SrvPort),
|
||||
Weight: int(rc.SrvWeight),
|
||||
}
|
||||
|
||||
// Linode doesn't use "@", it uses an empty name
|
||||
if req.Name == "@" {
|
||||
req.Name = ""
|
||||
}
|
||||
|
||||
// Linode uses the same property for MX and SRV priority
|
||||
switch rc.Type { // #rtype_variations
|
||||
case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "TLSA", "CAA":
|
||||
// Nothing special.
|
||||
case "MX":
|
||||
req.Priority = int(rc.MxPreference)
|
||||
req.Target = fixTarget(req.Target, dc.Name)
|
||||
case "SRV":
|
||||
req.Priority = int(rc.SrvPriority)
|
||||
|
||||
// From softlayer provider
|
||||
// This is to support SRV, it doesn't work yet for Linode
|
||||
result := srvRegexp.FindStringSubmatch(req.Name)
|
||||
|
||||
if len(result) != 3 {
|
||||
return nil, fmt.Errorf("SRV Record must match format \"_service._protocol\" not %s", req.Name)
|
||||
}
|
||||
|
||||
var serviceName, protocol string = result[1], strings.ToLower(result[2])
|
||||
|
||||
req.Protocol = protocol
|
||||
req.Service = serviceName
|
||||
req.Name = ""
|
||||
case "CNAME":
|
||||
req.Target = fixTarget(req.Target, dc.Name)
|
||||
default:
|
||||
msg := fmt.Sprintf("linode.toReq rtype %v unimplemented", rc.Type)
|
||||
panic(msg)
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func fixTarget(target, domain string) string {
|
||||
// Linode always wants a fully qualified target name
|
||||
if target[len(target)-1] == '.' {
|
||||
return target[:len(target)-1]
|
||||
} else {
|
||||
return fmt.Sprintf("%s.%s", target, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func fixTTL(ttl uint32) uint32 {
|
||||
// if the TTL is larger than the largest allowed value, return the largest allowed value
|
||||
if ttl > allowedTTLValues[len(allowedTTLValues)-1] {
|
||||
return allowedTTLValues[len(allowedTTLValues)-1]
|
||||
}
|
||||
|
||||
for _, v := range allowedTTLValues {
|
||||
if v >= ttl {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return allowedTTLValues[0]
|
||||
}
|
23
providers/linode/linodeProvider_test.go
Normal file
23
providers/linode/linodeProvider_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixTTL(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
given, expected uint32
|
||||
}{
|
||||
{299, 300},
|
||||
{300, 300},
|
||||
{301, 3600},
|
||||
{2419202, 2419200},
|
||||
{600, 3600},
|
||||
{3600, 3600},
|
||||
} {
|
||||
found := fixTTL(test.given)
|
||||
if found != test.expected {
|
||||
t.Errorf("Test %d: Expected %d, but was %d", i, test.expected, found)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user