forked from TrueCloudLab/lego
cloudns: Improve reliability (#1331)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
b65dfb8661
commit
8b8be6f21e
4 changed files with 636 additions and 172 deletions
|
@ -8,7 +8,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/go-acme/lego/v4/platform/wait"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudns/internal"
|
||||
)
|
||||
|
||||
|
@ -41,8 +43,8 @@ type Config struct {
|
|||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, 60),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
|
@ -112,10 +114,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
return fmt.Errorf("ClouDNS: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return d.waitNameservers(domain, zone)
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
// CleanUp removes the TXT records matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
|
@ -124,19 +126,22 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
return fmt.Errorf("ClouDNS: %w", err)
|
||||
}
|
||||
|
||||
record, err := d.client.FindTxtRecord(zone.Name, fqdn)
|
||||
records, err := d.client.ListTxtRecords(zone.Name, fqdn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ClouDNS: %w", err)
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = d.client.RemoveTxtRecord(record.ID, zone.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ClouDNS: %w", err)
|
||||
for _, record := range records {
|
||||
err = d.client.RemoveTxtRecord(record.ID, zone.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ClouDNS: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -145,3 +150,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
// waitNameservers At the time of writing 4 servers are found as authoritative, but 8 are reported during the sync.
|
||||
// If this is not done, the secondary verification done by Let's Encrypt server will fail quire a bit.
|
||||
func (d *DNSProvider) waitNameservers(domain string, zone *internal.Zone) error {
|
||||
return wait.For("Nameserver sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
|
||||
syncProgress, err := d.client.GetUpdateStatus(zone.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] Sync %d/%d complete", domain, syncProgress.Updated, syncProgress.Total)
|
||||
|
||||
return syncProgress.Complete, nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -15,31 +16,15 @@ import (
|
|||
|
||||
const defaultBaseURL = "https://api.cloudns.net/dns/"
|
||||
|
||||
type apiResponse struct {
|
||||
Status string `json:"status"`
|
||||
StatusDescription string `json:"statusDescription"`
|
||||
// Client the ClouDNS client.
|
||||
type Client struct {
|
||||
authID string
|
||||
subAuthID string
|
||||
authPassword string
|
||||
HTTPClient *http.Client
|
||||
BaseURL *url.URL
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
Name string
|
||||
Type string
|
||||
Zone string
|
||||
Status string // is an integer, but cast as string
|
||||
}
|
||||
|
||||
// TXTRecord a TXT record.
|
||||
type TXTRecord struct {
|
||||
ID int `json:"id,string"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Record string `json:"record"`
|
||||
Failover int `json:"failover,string"`
|
||||
TTL int `json:"ttl,string"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type TXTRecords map[string]TXTRecord
|
||||
|
||||
// NewClient creates a ClouDNS client.
|
||||
func NewClient(authID, subAuthID, authPassword string) (*Client, error) {
|
||||
if authID == "" && subAuthID == "" {
|
||||
|
@ -64,15 +49,6 @@ func NewClient(authID, subAuthID, authPassword string) (*Client, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Client ClouDNS client.
|
||||
type Client struct {
|
||||
authID string
|
||||
subAuthID string
|
||||
authPassword string
|
||||
HTTPClient *http.Client
|
||||
BaseURL *url.URL
|
||||
}
|
||||
|
||||
// GetZone Get domain name information for a FQDN.
|
||||
func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
||||
authZone, err := dns01.FindZoneByFqdn(authFQDN)
|
||||
|
@ -82,14 +58,16 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
|||
|
||||
authZoneName := dns01.UnFqdn(authZone)
|
||||
|
||||
reqURL := *c.BaseURL
|
||||
reqURL.Path += "get-zone-info.json"
|
||||
endpoint, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "get-zone-info.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Add("domain-name", authZoneName)
|
||||
reqURL.RawQuery = q.Encode()
|
||||
q := endpoint.Query()
|
||||
q.Set("domain-name", authZoneName)
|
||||
endpoint.RawQuery = q.Encode()
|
||||
|
||||
result, err := c.doRequest(http.MethodGet, &reqURL)
|
||||
result, err := c.doRequest(http.MethodGet, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -98,7 +76,7 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
|||
|
||||
if len(result) > 0 {
|
||||
if err = json.Unmarshal(result, &zone); err != nil {
|
||||
return nil, fmt.Errorf("zone unmarshaling error: %w", err)
|
||||
return nil, fmt.Errorf("failed to unmarshal zone: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,20 +87,22 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
|||
return nil, fmt.Errorf("zone %s not found for authFQDN %s", authZoneName, authFQDN)
|
||||
}
|
||||
|
||||
// FindTxtRecord return the TXT record a zone ID and a FQDN.
|
||||
// FindTxtRecord returns the TXT record a zone ID and a FQDN.
|
||||
func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
||||
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||
|
||||
reqURL := *c.BaseURL
|
||||
reqURL.Path += "records.json"
|
||||
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Add("domain-name", zoneName)
|
||||
q.Add("host", host)
|
||||
q.Add("type", "TXT")
|
||||
q.Set("domain-name", zoneName)
|
||||
q.Set("host", host)
|
||||
q.Set("type", "TXT")
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
result, err := c.doRequest(http.MethodGet, &reqURL)
|
||||
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -132,9 +112,9 @@ func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
var records TXTRecords
|
||||
var records map[string]TXTRecord
|
||||
if err = json.Unmarshal(result, &records); err != nil {
|
||||
return nil, fmt.Errorf("TXT record unmarshaling error: %w: %s", err, string(result))
|
||||
return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result))
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
|
@ -146,65 +126,145 @@ func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// AddTxtRecord add a TXT record.
|
||||
// ListTxtRecords returns the TXT records a zone ID and a FQDN.
|
||||
func (c *Client) ListTxtRecords(zoneName, fqdn string) ([]TXTRecord, error) {
|
||||
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||
|
||||
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Set("domain-name", zoneName)
|
||||
q.Set("host", host)
|
||||
q.Set("type", "TXT")
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the API returns [] when there is no records.
|
||||
if string(result) == "[]" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var raw map[string]TXTRecord
|
||||
if err = json.Unmarshal(result, &raw); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result))
|
||||
}
|
||||
|
||||
var records []TXTRecord
|
||||
for _, record := range raw {
|
||||
if record.Host == host && record.Type == "TXT" {
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// AddTxtRecord adds a TXT record.
|
||||
func (c *Client) AddTxtRecord(zoneName, fqdn, value string, ttl int) error {
|
||||
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||
|
||||
reqURL := *c.BaseURL
|
||||
reqURL.Path += "add-record.json"
|
||||
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "add-record.json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Add("domain-name", zoneName)
|
||||
q.Add("host", host)
|
||||
q.Add("record", value)
|
||||
q.Add("ttl", strconv.Itoa(ttlRounder(ttl)))
|
||||
q.Add("record-type", "TXT")
|
||||
q.Set("domain-name", zoneName)
|
||||
q.Set("host", host)
|
||||
q.Set("record", value)
|
||||
q.Set("ttl", strconv.Itoa(ttlRounder(ttl)))
|
||||
q.Set("record-type", "TXT")
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
raw, err := c.doRequest(http.MethodPost, &reqURL)
|
||||
raw, err := c.doRequest(http.MethodPost, reqURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := apiResponse{}
|
||||
if err = json.Unmarshal(raw, &resp); err != nil {
|
||||
return fmt.Errorf("apiResponse unmarshaling error: %w: %s", err, string(raw))
|
||||
return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw))
|
||||
}
|
||||
|
||||
if resp.Status != "Success" {
|
||||
return fmt.Errorf("fail to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||
return fmt.Errorf("failed to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTxtRecord remove a TXT record.
|
||||
// RemoveTxtRecord removes a TXT record.
|
||||
func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error {
|
||||
reqURL := *c.BaseURL
|
||||
reqURL.Path += "delete-record.json"
|
||||
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "delete-record.json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Add("domain-name", zoneName)
|
||||
q.Add("record-id", strconv.Itoa(recordID))
|
||||
q.Set("domain-name", zoneName)
|
||||
q.Set("record-id", strconv.Itoa(recordID))
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
raw, err := c.doRequest(http.MethodPost, &reqURL)
|
||||
raw, err := c.doRequest(http.MethodPost, reqURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := apiResponse{}
|
||||
if err = json.Unmarshal(raw, &resp); err != nil {
|
||||
return fmt.Errorf("apiResponse unmarshaling error: %w: %s", err, string(raw))
|
||||
return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw))
|
||||
}
|
||||
|
||||
if resp.Status != "Success" {
|
||||
return fmt.Errorf("fail to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||
return fmt.Errorf("failed to remove TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUpdateStatus gets sync progress of all CloudDNS NS servers.
|
||||
func (c *Client) GetUpdateStatus(zoneName string) (*SyncProgress, error) {
|
||||
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "update-status.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||
}
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Set("domain-name", zoneName)
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the API returns [] when there is no records.
|
||||
if string(result) == "[]" {
|
||||
return nil, errors.New("no nameservers records returned")
|
||||
}
|
||||
|
||||
var records []UpdateRecord
|
||||
if err = json.Unmarshal(result, &records); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal UpdateRecord: %w: %s", err, string(result))
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
for _, record := range records {
|
||||
if record.Updated {
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return &SyncProgress{Complete: updatedCount == len(records), Updated: updatedCount, Total: len(records)}, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error) {
|
||||
req, err := c.buildRequest(method, url)
|
||||
if err != nil {
|
||||
|
@ -224,8 +284,9 @@ func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error)
|
|||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid code (%v), error: %s", resp.StatusCode, content)
|
||||
return nil, fmt.Errorf("invalid code (%d), error: %s", resp.StatusCode, content)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
|
@ -233,12 +294,12 @@ func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error
|
|||
q := url.Query()
|
||||
|
||||
if c.subAuthID != "" {
|
||||
q.Add("sub-auth-id", c.subAuthID)
|
||||
q.Set("sub-auth-id", c.subAuthID)
|
||||
} else {
|
||||
q.Add("auth-id", c.authID)
|
||||
q.Set("auth-id", c.authID)
|
||||
}
|
||||
|
||||
q.Add("auth-password", c.authPassword)
|
||||
q.Set("auth-password", c.authPassword)
|
||||
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
|
@ -269,7 +330,6 @@ func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
|
|||
// - 604800 = 1 week
|
||||
// - 1209600 = 2 weeks
|
||||
// - 2592000 = 1 month
|
||||
// - 2592000 = 1 month
|
||||
// See https://www.cloudns.net/wiki/article/58/ for details.
|
||||
func ttlRounder(ttl int) int {
|
||||
for _, validTTL := range []int{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600} {
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func handlerMock(method string, jsonData []byte) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
func handlerMock(method string, jsonData []byte) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != method {
|
||||
http.Error(rw, "Incorrect method used", http.StatusBadRequest)
|
||||
return
|
||||
|
@ -23,25 +23,71 @@ func handlerMock(method string, jsonData []byte) http.Handler {
|
|||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetZone(t *testing.T) {
|
||||
type result struct {
|
||||
zone *Zone
|
||||
error bool
|
||||
func TestNewClient(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authID string
|
||||
subAuthID string
|
||||
authPassword string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "all provided",
|
||||
authID: "1000",
|
||||
subAuthID: "1111",
|
||||
authPassword: "no-secret",
|
||||
},
|
||||
{
|
||||
desc: "missing authID & subAuthID",
|
||||
authID: "",
|
||||
subAuthID: "",
|
||||
authPassword: "no-secret",
|
||||
expected: "credentials missing: authID or subAuthID",
|
||||
},
|
||||
{
|
||||
desc: "missing authID & subAuthID",
|
||||
authID: "",
|
||||
subAuthID: "present",
|
||||
authPassword: "",
|
||||
expected: "credentials missing: authPassword",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
client, err := NewClient(test.authID, test.subAuthID, test.authPassword)
|
||||
|
||||
if test.expected != "" {
|
||||
assert.Nil(t, client)
|
||||
require.EqualError(t, err, test.expected)
|
||||
} else {
|
||||
assert.NotNil(t, client)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetZone(t *testing.T) {
|
||||
type expected struct {
|
||||
zone *Zone
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authFQDN string
|
||||
apiResponse []byte
|
||||
expected result
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "zone found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
apiResponse: []byte(`{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`),
|
||||
expected: result{
|
||||
apiResponse: `{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`,
|
||||
expected: expected{
|
||||
zone: &Zone{
|
||||
Name: "foo.com",
|
||||
Type: "master",
|
||||
|
@ -53,23 +99,35 @@ func TestClientGetZone(t *testing.T) {
|
|||
{
|
||||
desc: "zone not found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
apiResponse: []byte(``),
|
||||
expected: result{error: true},
|
||||
apiResponse: ``,
|
||||
expected: expected{
|
||||
errorMsg: "zone foo.com not found for authFQDN _acme-challenge.foo.com.",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
apiResponse: `[{}]`,
|
||||
expected: expected{
|
||||
errorMsg: "failed to unmarshal zone: json: cannot unmarshal array into Go value of type internal.Zone",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, _ := NewClient("myAuthID", "", "myAuthPassword")
|
||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
||||
client.BaseURL = mockBaseURL
|
||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
zone, err := client.GetZone(test.authFQDN)
|
||||
|
||||
if test.expected.error {
|
||||
require.Error(t, err)
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected.zone, zone)
|
||||
|
@ -78,24 +136,24 @@ func TestClientGetZone(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientFindTxtRecord(t *testing.T) {
|
||||
type result struct {
|
||||
func TestClient_FindTxtRecord(t *testing.T) {
|
||||
type expected struct {
|
||||
txtRecord *TXTRecord
|
||||
error bool
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authFQDN string
|
||||
zoneName string
|
||||
apiResponse []byte
|
||||
expected result
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "record found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: []byte(`{
|
||||
apiResponse: `{
|
||||
"5769228": {
|
||||
"id": "5769228",
|
||||
"type": "TXT",
|
||||
|
@ -114,8 +172,8 @@ func TestClientFindTxtRecord(t *testing.T) {
|
|||
"ttl": "300",
|
||||
"status": 1
|
||||
}
|
||||
}`),
|
||||
expected: result{
|
||||
}`,
|
||||
expected: expected{
|
||||
txtRecord: &TXTRecord{
|
||||
ID: 5769228,
|
||||
Type: "TXT",
|
||||
|
@ -128,28 +186,61 @@ func TestClientFindTxtRecord(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "record not found",
|
||||
desc: "no record found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `{
|
||||
"5769228": {
|
||||
"id": "5769228",
|
||||
"type": "TXT",
|
||||
"host": "_other-challenge",
|
||||
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||
"failover": "0",
|
||||
"ttl": "3600",
|
||||
"status": 1
|
||||
},
|
||||
"181805209": {
|
||||
"id": "181805209",
|
||||
"type": "TXT",
|
||||
"host": "_github-challenge",
|
||||
"record": "b66b8324b5",
|
||||
"failover": "0",
|
||||
"ttl": "300",
|
||||
"status": 1
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
desc: "zero records",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: []byte(`[]`),
|
||||
expected: result{txtRecord: nil},
|
||||
apiResponse: `[]`,
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: `[{}]`,
|
||||
expected: expected{
|
||||
errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
||||
client.BaseURL = mockBaseURL
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
txtRecord, err := client.FindTxtRecord(test.zoneName, test.authFQDN)
|
||||
|
||||
if test.expected.error {
|
||||
require.Error(t, err)
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected.txtRecord, txtRecord)
|
||||
|
@ -158,91 +249,198 @@ func TestClientFindTxtRecord(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientAddTxtRecord(t *testing.T) {
|
||||
func TestClient_ListTxtRecord(t *testing.T) {
|
||||
type expected struct {
|
||||
Query string
|
||||
Error string
|
||||
txtRecords []TXTRecord
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authFQDN string
|
||||
zoneName string
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "record found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `{
|
||||
"5769228": {
|
||||
"id": "5769228",
|
||||
"type": "TXT",
|
||||
"host": "_acme-challenge",
|
||||
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||
"failover": "0",
|
||||
"ttl": "3600",
|
||||
"status": 1
|
||||
},
|
||||
"181805209": {
|
||||
"id": "181805209",
|
||||
"type": "TXT",
|
||||
"host": "_github-challenge",
|
||||
"record": "b66b8324b5",
|
||||
"failover": "0",
|
||||
"ttl": "300",
|
||||
"status": 1
|
||||
}
|
||||
}`,
|
||||
expected: expected{
|
||||
txtRecords: []TXTRecord{
|
||||
{
|
||||
ID: 5769228,
|
||||
Type: "TXT",
|
||||
Host: "_acme-challenge",
|
||||
Record: "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||
Failover: 0,
|
||||
TTL: 3600,
|
||||
Status: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no record found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `{
|
||||
"5769228": {
|
||||
"id": "5769228",
|
||||
"type": "TXT",
|
||||
"host": "_other-challenge",
|
||||
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||
"failover": "0",
|
||||
"ttl": "3600",
|
||||
"status": 1
|
||||
},
|
||||
"181805209": {
|
||||
"id": "181805209",
|
||||
"type": "TXT",
|
||||
"host": "_github-challenge",
|
||||
"record": "b66b8324b5",
|
||||
"failover": "0",
|
||||
"ttl": "300",
|
||||
"status": 1
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
desc: "zero records",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: `[]`,
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: `[{}]`,
|
||||
expected: expected{
|
||||
errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
txtRecords, err := client.ListTxtRecords(test.zoneName, test.authFQDN)
|
||||
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected.txtRecords, txtRecords)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AddTxtRecord(t *testing.T) {
|
||||
type expected struct {
|
||||
query string
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authID string
|
||||
subAuthID string
|
||||
zone *Zone
|
||||
zoneName string
|
||||
authFQDN string
|
||||
value string
|
||||
ttl int
|
||||
apiResponse []byte
|
||||
expected expected
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "sub-zone",
|
||||
authID: "myAuthID",
|
||||
zone: &Zone{
|
||||
Name: "bar.com",
|
||||
Type: "master",
|
||||
Zone: "domain",
|
||||
Status: "1",
|
||||
},
|
||||
desc: "sub-zone",
|
||||
authID: "myAuthID",
|
||||
zoneName: "bar.com",
|
||||
authFQDN: "_acme-challenge.foo.bar.com.",
|
||||
value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||
ttl: 60,
|
||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
||||
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||
expected: expected{
|
||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "main zone (authID)",
|
||||
authID: "myAuthID",
|
||||
zone: &Zone{
|
||||
Name: "bar.com",
|
||||
Type: "master",
|
||||
Zone: "domain",
|
||||
Status: "1",
|
||||
},
|
||||
desc: "main zone (authID)",
|
||||
authID: "myAuthID",
|
||||
zoneName: "bar.com",
|
||||
authFQDN: "_acme-challenge.bar.com.",
|
||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||
ttl: 60,
|
||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
||||
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||
expected: expected{
|
||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "main zone (subAuthID)",
|
||||
authID: "myAuthID",
|
||||
subAuthID: "mySubAuthID",
|
||||
zone: &Zone{
|
||||
Name: "bar.com",
|
||||
Type: "master",
|
||||
Zone: "domain",
|
||||
Status: "1",
|
||||
},
|
||||
desc: "main zone (subAuthID)",
|
||||
subAuthID: "mySubAuthID",
|
||||
zoneName: "bar.com",
|
||||
authFQDN: "_acme-challenge.bar.com.",
|
||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||
ttl: 60,
|
||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
||||
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||
expected: expected{
|
||||
Query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
|
||||
query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid status",
|
||||
authID: "myAuthID",
|
||||
zone: &Zone{
|
||||
Name: "bar.com",
|
||||
Type: "master",
|
||||
Zone: "domain",
|
||||
Status: "1",
|
||||
},
|
||||
desc: "invalid status",
|
||||
authID: "myAuthID",
|
||||
zoneName: "bar.com",
|
||||
authFQDN: "_acme-challenge.bar.com.",
|
||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||
ttl: 120,
|
||||
apiResponse: []byte(`{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`),
|
||||
apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`,
|
||||
expected: expected{
|
||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
||||
Error: "fail to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.",
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
||||
errorMsg: "failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
authID: "myAuthID",
|
||||
zoneName: "bar.com",
|
||||
authFQDN: "_acme-challenge.bar.com.",
|
||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||
ttl: 120,
|
||||
apiResponse: `[{}]`,
|
||||
expected: expected{
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
||||
errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -250,25 +448,172 @@ func TestClientAddTxtRecord(t *testing.T) {
|
|||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.NotNil(t, req.URL.RawQuery)
|
||||
assert.Equal(t, test.expected.Query, req.URL.RawQuery)
|
||||
if test.expected.query != req.URL.RawQuery {
|
||||
msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
|
||||
http.Error(rw, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
handlerMock(http.MethodPost, test.apiResponse).ServeHTTP(rw, req)
|
||||
handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
|
||||
}))
|
||||
|
||||
client, err := NewClient(test.authID, test.subAuthID, "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
||||
client.BaseURL = mockBaseURL
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
err = client.AddTxtRecord(test.zone.Name, test.authFQDN, test.value, test.ttl)
|
||||
err = client.AddTxtRecord(test.zoneName, test.authFQDN, test.value, test.ttl)
|
||||
|
||||
if test.expected.Error != "" {
|
||||
require.EqualError(t, err, test.expected.Error)
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_RemoveTxtRecord(t *testing.T) {
|
||||
type expected struct {
|
||||
query string
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
id int
|
||||
zoneName string
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "record found",
|
||||
id: 5769228,
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `{ "status": "Success", "statusDescription": "The record was deleted successfully." }`,
|
||||
expected: expected{
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769228`,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "record not found",
|
||||
id: 5769000,
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `{ "status": "Failed", "statusDescription": "Invalid record-id param." }`,
|
||||
expected: expected{
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769000`,
|
||||
errorMsg: "failed to remove TXT record: Failed Invalid record-id param.",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
id: 44,
|
||||
zoneName: "foo-plus.com",
|
||||
apiResponse: `[{}]`,
|
||||
expected: expected{
|
||||
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo-plus.com&record-id=44`,
|
||||
errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if test.expected.query != req.URL.RawQuery {
|
||||
msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
|
||||
http.Error(rw, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
err = client.RemoveTxtRecord(test.id, test.zoneName)
|
||||
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetUpdateStatus(t *testing.T) {
|
||||
type expected struct {
|
||||
progress *SyncProgress
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authFQDN string
|
||||
zoneName string
|
||||
apiResponse string
|
||||
expected
|
||||
}{
|
||||
{
|
||||
desc: "50% sync",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `[
|
||||
{"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true },
|
||||
{"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": false }
|
||||
]`,
|
||||
expected: expected{progress: &SyncProgress{Updated: 1, Total: 2}},
|
||||
},
|
||||
{
|
||||
desc: "100% sync",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "foo.com",
|
||||
apiResponse: `[
|
||||
{"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true },
|
||||
{"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": true }
|
||||
]`,
|
||||
expected: expected{progress: &SyncProgress{Complete: true, Updated: 2, Total: 2}},
|
||||
},
|
||||
{
|
||||
desc: "record not found",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: `[]`,
|
||||
expected: expected{errorMsg: "no nameservers records returned"},
|
||||
},
|
||||
{
|
||||
desc: "invalid json response",
|
||||
authFQDN: "_acme-challenge.foo.com.",
|
||||
zoneName: "test-zone",
|
||||
apiResponse: `[x]`,
|
||||
expected: expected{errorMsg: "failed to unmarshal UpdateRecord: invalid character 'x' looking for beginning of value: [x]"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
syncProgress, err := client.GetUpdateStatus(test.zoneName)
|
||||
|
||||
if test.expected.errorMsg != "" {
|
||||
require.EqualError(t, err, test.expected.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected.progress, syncProgress)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
39
providers/dns/cloudns/internal/types.go
Normal file
39
providers/dns/cloudns/internal/types.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package internal
|
||||
|
||||
type apiResponse struct {
|
||||
Status string `json:"status"`
|
||||
StatusDescription string `json:"statusDescription"`
|
||||
}
|
||||
|
||||
// Zone is a zone.
|
||||
type Zone struct {
|
||||
Name string
|
||||
Type string
|
||||
Zone string
|
||||
Status string // is an integer, but cast as string
|
||||
}
|
||||
|
||||
// TXTRecord is a TXT record.
|
||||
type TXTRecord struct {
|
||||
ID int `json:"id,string"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Record string `json:"record"`
|
||||
Failover int `json:"failover,string"`
|
||||
TTL int `json:"ttl,string"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateRecord is a Server Sync Record.
|
||||
type UpdateRecord struct {
|
||||
Server string `json:"server"`
|
||||
IP4 string `json:"ip4"`
|
||||
IP6 string `json:"ip6"`
|
||||
Updated bool `json:"updated"`
|
||||
}
|
||||
|
||||
type SyncProgress struct {
|
||||
Complete bool
|
||||
Updated int
|
||||
Total int
|
||||
}
|
Loading…
Reference in a new issue