NEW PROVIDER: HETZNER DNS Console (#904)

* HETZNER: implement the provider for Hetzner DNS Console

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

* HETZNER: apply review feedback

- add domain into error messages
- insert sub-strings using `%q`
- insert sub-errors using `%w`
- change api.getZone() signature to return a (potentially `nil`) Zone
   pointer instead of a (potentially empty) Zone value
- sort imports and confirm with `$ goimports -w providers/hetzner/`
- use exact 'api_key' term in error message of settings validation
- add blank line for logic separation
- drop internal record id from correction messages

Co-Authored-By: Tom Limoncelli <tlimoncelli@stackoverflow.com>
Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

* HETZNER: add request rate-limiting handling

There are a limited number of data-points on how their rate-limiting
 works at this time. I deduce from my account to others and use a fixed/
 constant backoff of 1s as the initial delay. Thereafter exponential
 increase with factor 2 (not needed at this time).
Hetzner has not made any official statements on rate-limiting, so this
 is guesswork only.

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

* HETZNER: address golint complaints

- baseUrl -> baseURL
- mark Record as private -> record
- mark Zone as private -> zone
- mark RequestRateLimiter as private -> requestRateLimiter
- capitalize Id fields as ID
- keep delay logic on same level, move return out of branch

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

* HETZNER: rate_limited: init the response timestamp on requestRateLimiter

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

* HETZNER: requestRateLimiter: align local variable with struct name

Signed-off-by: Jakob Ackermann <das7pad@outlook.com>

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Jakob Ackermann 2020-10-22 15:44:21 +02:00 committed by GitHub
parent 3a2b1b2f7b
commit 2b50af0cbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 626 additions and 1 deletions

1
OWNERS
View file

@ -11,6 +11,7 @@ providers/dnsimple @aeden
providers/gandi_v5 @TomOnTime
# providers/gcloud
providers/hedns @rblenkinsopp
providers/hetzner @das7pad
providers/hexonet @papakai
providers/internetbs @pragmaton
providers/inwx @svenpeter42

View file

@ -27,6 +27,7 @@ Currently supported DNS providers:
- Exoscale
- Gandi
- Google DNS
- Hetzner
- HEXONET
- Hurricane Electric DNS
- INWX

View file

@ -21,6 +21,7 @@
<th class="rotate"><div><span>GANDI_V5</span></div></th>
<th class="rotate"><div><span>GCLOUD</span></div></th>
<th class="rotate"><div><span>HEDNS</span></div></th>
<th class="rotate"><div><span>HETZNER</span></div></th>
<th class="rotate"><div><span>HEXONET</span></div></th>
<th class="rotate"><div><span>INTERNETBS</span></div></th>
<th class="rotate"><div><span>INWX</span></div></th>
@ -86,6 +87,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="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Actively maintained provider module.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -182,6 +186,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>
@ -272,6 +279,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>
@ -353,6 +363,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="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -416,6 +429,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="info" data-toggle="tooltip" data-container="body" data-placement="top" title="Supported by INWX but not implemented yet.">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
@ -480,6 +494,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><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -550,6 +567,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="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -617,6 +637,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="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -676,6 +697,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="success" data-toggle="tooltip" data-container="body" data-placement="top" title="SRV records with empty targets are not supported">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
</td>
@ -753,6 +777,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><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
@ -814,6 +841,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>
@ -873,6 +903,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="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -918,6 +951,7 @@
<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><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -959,6 +993,7 @@
<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><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -1002,6 +1037,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><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="DS records are only supported at the apex and require a different API call that hasn&#39;t been implemented yet.">
@ -1062,6 +1100,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><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -1145,6 +1186,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>
@ -1247,6 +1291,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>
@ -1324,6 +1371,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="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>

View file

@ -0,0 +1,73 @@
---
name: Hetzner DNS Console
title: Hetzner DNS Console
layout: default
jsId: HETZNER
---
# Hetzner DNS Console Provider
## Configuration
In your credentials file, you must provide a
[Hetzner API Key](https://dns.hetzner.com/settings/api-token).
{% highlight json %}
{
"hetzner": {
"api_key": "your-api-key"
}
}
{% endhighlight %}
## Metadata
This provider does not recognize any special metadata fields unique to Hetzner
DNS Console.
## Usage
Example Javascript:
{% highlight js %}
var REG_NONE = NewRegistrar('none', 'NONE');
var HETZNER = NewDnsProvider("hetzner", "HETZNER");
D("example.tld", REG_NONE, DnsProvider(HETZNER),
A("test","1.2.3.4")
);
{%endhighlight%}
## Activation
Create a new API Key in the
[Hetzner DNS Console](https://dns.hetzner.com/settings/api-token).
## Caveats
### SOA
Hetzner DNS Console does not allow changing the SOA record via their API.
There is an alternative method using an import of a full BIND file, but this
approach does not play nice with incremental changes or ignored records.
At this time you cannot update SOA records via DNSControl.
### Rate Limiting
In case you are frequently seeing messages about being rate-limited:
{% highlight txt %}
WARNING: request rate-limited, constant back-off is now at 1s.
{% endhighlight %}
You may want to enable the `rate_limited` mode by default.
In your `creds.json` for all `HETZNER` provider entries:
{% highlight json %}
{
"hetzner": {
"rate_limited": "true",
"api_key": "your-api-key"
}
}
{% endhighlight %}

View file

@ -81,6 +81,7 @@ Maintainers of contributed providers:
* `EXOSCALE` @pierre-emmanuelJ
* `GANDI_V5` @TomOnTime
* `HEDNS` @rblenkinsopp
* `HETZNER` @das7pad
* `HEXONET` @papakai
* `INTERNETBS` @pragmaton
* `INWX` @svenpeter42

View file

@ -693,7 +693,7 @@ func makeTests(t *testing.T) []*TestGroup {
),
testgroup("empty TXT",
not("CLOUDFLAREAPI", "HEXONET", "INWX", "NETCUP"),
not("CLOUDFLAREAPI", "HETZNER", "HEXONET", "INWX", "NETCUP"),
tc("TXT with empty str", txt("foo1", "")),
// https://github.com/StackExchange/dnscontrol/issues/598
// We decided that permitting the TXT target to be an empty

View file

@ -69,6 +69,11 @@
"totp-key": "$HEDNS_TOTP_SECRET",
"username": "$HEDNS_USERNAME"
},
"HETZNER": {
"api_key": "$HETZNER_API_KEY",
"domain": "$HETZNER_DOMAIN",
"rate_limited": "true"
},
"HEXONET": {
"apientity": "$HEXONET_ENTITY",
"apilogin": "$HEXONET_UID",

View file

@ -18,6 +18,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v3/providers/gandi_v5"
_ "github.com/StackExchange/dnscontrol/v3/providers/gcloud"
_ "github.com/StackExchange/dnscontrol/v3/providers/hedns"
_ "github.com/StackExchange/dnscontrol/v3/providers/hetzner"
_ "github.com/StackExchange/dnscontrol/v3/providers/hexonet"
_ "github.com/StackExchange/dnscontrol/v3/providers/internetbs"
_ "github.com/StackExchange/dnscontrol/v3/providers/inwx"

232
providers/hetzner/api.go Normal file
View file

@ -0,0 +1,232 @@
package hetzner
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
)
const (
baseURL = "https://dns.hetzner.com/api/v1"
)
type api struct {
apiKey string
zones map[string]zone
requestRateLimiter requestRateLimiter
}
func checkIsLockedSystemRecord(record record) error {
if record.Type == "SOA" {
// The upload of a BIND zone file can change the SOA record.
// Implementing this edge case this is too complex for now.
return fmt.Errorf("SOA records are locked in HETZNER zones. They are hence not available for updating")
}
return nil
}
func (api *api) createRecord(record record) error {
if err := checkIsLockedSystemRecord(record); err != nil {
return err
}
request := createRecordRequest{
Name: record.Name,
TTL: *record.TTL,
Type: record.Type,
Value: record.Value,
ZoneID: record.ZoneID,
}
return api.request("/records", "POST", request, nil)
}
func (api *api) createZone(name string) error {
request := createZoneRequest{
Name: name,
}
return api.request("/zones", "POST", request, nil)
}
func (api *api) deleteRecord(record record) error {
if err := checkIsLockedSystemRecord(record); err != nil {
return err
}
url := fmt.Sprintf("/records/%s", record.ID)
return api.request(url, "DELETE", nil, nil)
}
func (api *api) getAllRecords(domain string) ([]record, error) {
zone, err := api.getZone(domain)
if err != nil {
return nil, err
}
page := 1
records := make([]record, 0)
for {
response := &getAllRecordsResponse{}
url := fmt.Sprintf("/records?zone_id=%s&per_page=100&page=%d", zone.ID, page)
if err := api.request(url, "GET", nil, response); err != nil {
return nil, fmt.Errorf("failed fetching zone records for %q: %w", domain, err)
}
for _, record := range response.Records {
if record.TTL == nil {
record.TTL = &zone.TTL
}
if checkIsLockedSystemRecord(record) != nil {
// Some records are not available for updating, hide them.
continue
}
records = append(records, record)
}
// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
if page >= response.Meta.Pagination.LastPage {
break
}
page++
}
return records, nil
}
func (api *api) getAllZones() error {
if api.zones != nil {
return nil
}
zones := map[string]zone{}
page := 1
for {
response := &getAllZonesResponse{}
url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
if err := api.request(url, "GET", nil, response); err != nil {
return fmt.Errorf("failed fetching zones: %w", err)
}
for _, zone := range response.Zones {
zones[zone.Name] = zone
}
// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
if page >= response.Meta.Pagination.LastPage {
break
}
page++
}
api.zones = zones
return nil
}
func (api *api) getZone(name string) (*zone, error) {
if err := api.getAllZones(); err != nil {
return nil, err
}
zone, ok := api.zones[name]
if !ok {
return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
}
return &zone, nil
}
func (api *api) request(endpoint string, method string, request interface{}, target interface{}) error {
var requestBody io.Reader
if request != nil {
requestBodySerialised, err := json.Marshal(request)
if err != nil {
return err
}
requestBody = bytes.NewBuffer(requestBodySerialised)
}
req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
if err != nil {
return err
}
req.Header.Add("Auth-API-Token", api.apiKey)
for {
api.requestRateLimiter.beforeRequest()
resp, err := http.DefaultClient.Do(req)
api.requestRateLimiter.afterRequest()
if err != nil {
return err
}
cleanupResponseBody := func() {
err := resp.Body.Close()
if err != nil {
fmt.Println(fmt.Sprintf("failed closing response body: %q", err))
}
}
if resp.StatusCode == 429 {
api.requestRateLimiter.handleRateLimitedRequest()
cleanupResponseBody()
continue
}
defer cleanupResponseBody()
if resp.StatusCode != 200 {
data, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(data))
return fmt.Errorf("bad status code from HETZNER: %d not 200", resp.StatusCode)
}
if target == nil {
return nil
}
decoder := json.NewDecoder(resp.Body)
return decoder.Decode(target)
}
}
func (api *api) startRateLimited() {
// Simulate a request that is getting a 429 response.
api.requestRateLimiter.afterRequest()
api.requestRateLimiter.bumpDelay()
}
func (api *api) updateRecord(record record) error {
if err := checkIsLockedSystemRecord(record); err != nil {
return err
}
url := fmt.Sprintf("/records/%s", record.ID)
return api.request(url, "PUT", record, nil)
}
type requestRateLimiter struct {
delay time.Duration
lastRequest time.Time
}
func (requestRateLimiter *requestRateLimiter) afterRequest() {
requestRateLimiter.lastRequest = time.Now()
}
func (requestRateLimiter *requestRateLimiter) beforeRequest() {
if requestRateLimiter.delay == 0 {
return
}
time.Sleep(time.Until(requestRateLimiter.lastRequest.Add(requestRateLimiter.delay)))
}
func (requestRateLimiter *requestRateLimiter) bumpDelay() string {
var backoffType string
if requestRateLimiter.delay == 0 {
// At the time this provider was implemented (2020-10-18),
// one request per second could go though when rate-limited.
requestRateLimiter.delay = time.Second
backoffType = "constant"
} else {
// The initial assumption of 1 req/s may no hold true forever.
// Future proof this provider, use exponential back-off.
requestRateLimiter.delay = requestRateLimiter.delay * 2
backoffType = "exponential"
}
return backoffType
}
func (requestRateLimiter *requestRateLimiter) handleRateLimitedRequest() {
backoffType := requestRateLimiter.bumpDelay()
fmt.Println(fmt.Sprintf("WARNING: request rate-limited, %s back-off is now at %s.", backoffType, requestRateLimiter.delay))
}

View file

@ -0,0 +1,173 @@
package hetzner
import (
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/providers"
)
var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.CanUseTXTMulti: providers.Cannot(),
}
func init() {
providers.RegisterDomainServiceProviderType("HETZNER", New, features)
}
// New creates a new API handle.
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
if settings["api_key"] == "" {
return nil, fmt.Errorf("missing HETZNER api_key")
}
api := &api{}
api.apiKey = settings["api_key"]
if settings["rate_limited"] == "true" {
api.startRateLimited()
}
return api, nil
}
// EnsureDomainExists creates the domain if it does not exist.
func (api *api) EnsureDomainExists(domain string) error {
domains, err := api.ListZones()
if err != nil {
return err
}
for _, d := range domains {
if d == domain {
return nil
}
}
return api.createZone(domain)
}
// GetDomainCorrections returns the corrections for a domain.
func (api *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc, err := dc.Copy()
if err != nil {
return nil, err
}
err = dc.Punycode()
if err != nil {
return nil, err
}
domain := dc.Name
// Get existing records
existingRecords, err := api.GetZoneRecords(domain)
if err != nil {
return nil, err
}
// Normalize
models.PostProcessRecords(existingRecords)
differ := diff.New(dc)
_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
if err != nil {
return nil, err
}
var corrections []*models.Correction
zone, err := api.getZone(domain)
if err != nil {
return nil, err
}
for _, m := range del {
record := m.Existing.Original.(*record)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
return api.deleteRecord(*record)
},
}
corrections = append(corrections, corr)
}
for _, m := range create {
record := fromRecordConfig(m.Desired, zone)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
return api.createRecord(*record)
},
}
corrections = append(corrections, corr)
}
for _, m := range modify {
id := m.Existing.Original.(*record).ID
record := fromRecordConfig(m.Desired, zone)
record.ID = id
corr := &models.Correction{
Msg: m.String(),
F: func() error {
return api.updateRecord(*record)
},
}
corrections = append(corrections, corr)
}
return corrections, nil
}
// GetNameservers returns the nameservers for a domain.
func (api *api) GetNameservers(domain string) ([]*models.Nameserver, error) {
zone, err := api.getZone(domain)
if err != nil {
return nil, err
}
nameserver := make([]*models.Nameserver, len(zone.NameServers))
for i := range zone.NameServers {
nameserver[i] = &models.Nameserver{Name: zone.NameServers[i]}
}
return nameserver, nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (api *api) GetZoneRecords(domain string) (models.Records, error) {
records, err := api.getAllRecords(domain)
if err != nil {
return nil, err
}
existingRecords := make([]*models.RecordConfig, len(records))
for i := range records {
existingRecords[i] = toRecordConfig(domain, &records[i])
}
return existingRecords, nil
}
// ListZones lists the zones on this account.
func (api *api) ListZones() ([]string, error) {
if err := api.getAllZones(); err != nil {
return nil, err
}
var zones []string
for i := range api.zones {
zones = append(zones, i)
}
return zones, nil
}

View file

@ -0,0 +1,88 @@
package hetzner
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
type createRecordRequest struct {
Name string `json:"name"`
TTL int `json:"ttl"`
Type string `json:"type"`
Value string `json:"value"`
ZoneID string `json:"zone_id"`
}
type createZoneRequest struct {
Name string `json:"name"`
}
type getAllRecordsResponse struct {
Records []record `json:"records"`
Meta struct {
Pagination struct {
LastPage int `json:"last_page"`
} `json:"pagination"`
} `json:"meta"`
}
type getAllZonesResponse struct {
Zones []zone `json:"zones"`
Meta struct {
Pagination struct {
LastPage int `json:"last_page"`
} `json:"pagination"`
} `json:"meta"`
}
type record struct {
ID string `json:"id"`
Name string `json:"name"`
TTL *int `json:"ttl"`
Type string `json:"type"`
Value string `json:"value"`
ZoneID string `json:"zone_id"`
}
type zone struct {
ID string `json:"id"`
Name string `json:"name"`
NameServers []string `json:"ns"`
TTL int `json:"ttl"`
}
func fromRecordConfig(in *models.RecordConfig, zone *zone) *record {
ttl := int(in.TTL)
record := &record{
Name: in.GetLabel(),
Type: in.Type,
Value: in.GetTargetField(),
TTL: &ttl,
ZoneID: zone.ID,
}
switch record.Type {
case "TXT":
// Cannot use `in.GetTargetCombined()` for TXTs:
// Their validation would complain about a missing `;`.
// Test case: single_TXT:Create_a_255-byte_TXT
// {"error":{"message":"422 Unprocessable Entity: missing: ; ","code":422}}
record.Value = in.GetTargetField()
default:
record.Value = in.GetTargetCombined()
}
return record
}
func toRecordConfig(domain string, record *record) *models.RecordConfig {
rc := &models.RecordConfig{
Type: record.Type,
TTL: uint32(*record.TTL),
Original: record,
}
rc.SetLabel(record.Name, domain)
_ = rc.PopulateFromString(record.Type, record.Value, domain)
return rc
}