mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
NEW PROVIDER: CSCGLOBAL as DNS Service Provider (#1516)
* Move the registrar features to a separate file * Prepare the testing framework * Roughed out functions * Fix up structs * WIP! * First tests pass * wip! * Flesh out remaining rTypes, get nameservers, etc * Fix TXT records * Clean up code * More cleanups. Fix CAA/SRV * Linting * Cleanups/linting * Fix CAA [more] and more cleanups * CSC does not like very long txt records * Use timer only when interactive * Disable CAA for now * Update docs * Remove debug printf * add go-isatty * cleanups
This commit is contained in:
@@ -8,6 +8,11 @@ jsId: CSCGLOBAL
|
|||||||
|
|
||||||
DNSControl's CSC Global provider supports being a Registrar. Support for being a DNS Provider is not included, although CSC Global's API does provide for this so it could be implemented in the future.
|
DNSControl's CSC Global provider supports being a Registrar. Support for being a DNS Provider is not included, although CSC Global's API does provide for this so it could be implemented in the future.
|
||||||
|
|
||||||
|
NOTE: Experimental support for being a DNS Provider is available.
|
||||||
|
However it is not recommended as updates take 5-7 minutes, and the
|
||||||
|
next update is not permitted until the previous update is complete.
|
||||||
|
Use it at your own risk. Consider it experimental and undocumented.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
To use this provider, add an entry to `creds.json` with `TYPE` set to `CSCGLOBAL`.
|
To use this provider, add an entry to `creds.json` with `TYPE` set to `CSCGLOBAL`.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ into three general categories:
|
|||||||
has A and MX records), you have to replace all the records at that
|
has A and MX records), you have to replace all the records at that
|
||||||
label. (GANDI_V5)
|
label. (GANDI_V5)
|
||||||
* **incremental-label-type:** Like incremental-record, but updates to any records at a label have to be done by type. For example, if a label (www.example.com) has many A and MX records, even the smallest change to one of the A records requires replacing all the A records. Any changes to the MX records requires replacing all the MX records. If an A record is converted to a CNAME, one must remove all the A records in one call, and add the CNAME record with another call. This is deceptively difficult to get right; if you have the choice between incremental-label-type and incremental-label, pick incremental-label. (DESEC, ROUTE53)
|
* **incremental-label-type:** Like incremental-record, but updates to any records at a label have to be done by type. For example, if a label (www.example.com) has many A and MX records, even the smallest change to one of the A records requires replacing all the A records. Any changes to the MX records requires replacing all the MX records. If an A record is converted to a CNAME, one must remove all the A records in one call, and add the CNAME record with another call. This is deceptively difficult to get right; if you have the choice between incremental-label-type and incremental-label, pick incremental-label. (DESEC, ROUTE53)
|
||||||
* **registrar only:** These providers are registrars but do not provide DNS service. (CSCGLOBAL, EASYNAME, INTERNETBS, OPENSRS)
|
* **registrar only:** These providers are registrars but do not provide DNS service. (EASYNAME, INTERNETBS, OPENSRS)
|
||||||
|
|
||||||
All DNS providers use the "diff" module to detect differences. It takes
|
All DNS providers use the "diff" module to detect differences. It takes
|
||||||
two zones and returns records that are unchanged, created, deleted,
|
two zones and returns records that are unchanged, created, deleted,
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -62,6 +62,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
|
github.com/mattn/go-isatty v0.0.14
|
||||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +130,6 @@ require (
|
|||||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect
|
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
|
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
|
||||||
|
|||||||
@@ -532,6 +532,10 @@ func ttl(r *models.RecordConfig, t uint32) *models.RecordConfig {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gentxt generates TXTmulti test cases. The input string is used to
|
||||||
|
// dictate the output, each char represents the substring in the
|
||||||
|
// resulting TXTmulti. 0 or s outputs a short string, h outputs a 128-octet
|
||||||
|
// string, 1 or l outputs a long (255-octet) string.
|
||||||
func gentxt(s string) *TestCase {
|
func gentxt(s string) *TestCase {
|
||||||
title := fmt.Sprintf("Create TXT %s", s)
|
title := fmt.Sprintf("Create TXT %s", s)
|
||||||
label := fmt.Sprintf("foo%d", len(s))
|
label := fmt.Sprintf("foo%d", len(s))
|
||||||
@@ -760,6 +764,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
not(
|
not(
|
||||||
"AUTODNS",
|
"AUTODNS",
|
||||||
"AZURE_DNS",
|
"AZURE_DNS",
|
||||||
|
"CSCGLOBAL", // Last verified 2022-06-07
|
||||||
"DIGITALOCEAN",
|
"DIGITALOCEAN",
|
||||||
"DNSIMPLE",
|
"DNSIMPLE",
|
||||||
"GANDI_V5",
|
"GANDI_V5",
|
||||||
@@ -900,7 +905,10 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
tc("Create TXT with double-quote", txt("foodq", `quo"te`)),
|
tc("Create TXT with double-quote", txt("foodq", `quo"te`)),
|
||||||
clear(),
|
clear(),
|
||||||
tc("Create TXT with ws at end", txt("foows1", "with space at end ")),
|
tc("Create TXT with ws at end", txt("foows1", "with space at end ")),
|
||||||
clear(),
|
),
|
||||||
|
|
||||||
|
//
|
||||||
|
testgroup("gentxt TXT",
|
||||||
gentxt("0"),
|
gentxt("0"),
|
||||||
gentxt("1"),
|
gentxt("1"),
|
||||||
gentxt("10"),
|
gentxt("10"),
|
||||||
@@ -1030,6 +1038,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
// - DIGITALOCEAN: page size is 100 (default: 20)
|
// - DIGITALOCEAN: page size is 100 (default: 20)
|
||||||
not(
|
not(
|
||||||
"CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping.
|
"CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping.
|
||||||
|
"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
|
||||||
"GANDI_V5", // Their API is so damn slow. We'll add it back as needed.
|
"GANDI_V5", // Their API is so damn slow. We'll add it back as needed.
|
||||||
"MSDNS", // No paging done. No need to test.
|
"MSDNS", // No paging done. No need to test.
|
||||||
"NAMEDOTCOM", // Their API is so damn slow. We'll add it back as needed.
|
"NAMEDOTCOM", // Their API is so damn slow. We'll add it back as needed.
|
||||||
@@ -1042,10 +1051,11 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
|
|
||||||
testgroup("pager601",
|
testgroup("pager601",
|
||||||
only(
|
only(
|
||||||
//"MSDNS", // No paging done. No need to test.
|
|
||||||
//"AZURE_DNS", // Currently failing.
|
//"AZURE_DNS", // Currently failing.
|
||||||
"HEXONET",
|
//"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
|
||||||
"GCLOUD",
|
"GCLOUD",
|
||||||
|
"HEXONET",
|
||||||
|
//"MSDNS", // No paging done. No need to test.
|
||||||
"ROUTE53",
|
"ROUTE53",
|
||||||
),
|
),
|
||||||
tc("601 records", manyA("rec%04d", "1.2.3.4", 600)...),
|
tc("601 records", manyA("rec%04d", "1.2.3.4", 600)...),
|
||||||
@@ -1057,6 +1067,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
//"AKAMAIEDGEDNS", // No paging done. No need to test.
|
//"AKAMAIEDGEDNS", // No paging done. No need to test.
|
||||||
//"AZURE_DNS", // Currently failing. See https://github.com/StackExchange/dnscontrol/issues/770
|
//"AZURE_DNS", // Currently failing. See https://github.com/StackExchange/dnscontrol/issues/770
|
||||||
//"CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/StackExchange/dnscontrol/issues/1440
|
//"CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/StackExchange/dnscontrol/issues/1440
|
||||||
|
//"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
|
||||||
"HEXONET",
|
"HEXONET",
|
||||||
"HOSTINGDE",
|
"HOSTINGDE",
|
||||||
//"MSDNS", // No paging done. No need to test.
|
//"MSDNS", // No paging done. No need to test.
|
||||||
@@ -1144,6 +1155,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
),
|
),
|
||||||
testgroup("SRV w/ null target", requires(providers.CanUseSRV),
|
testgroup("SRV w/ null target", requires(providers.CanUseSRV),
|
||||||
not(
|
not(
|
||||||
|
"CSCGLOBAL", // Not supported.
|
||||||
"EXOSCALE", // Not supported.
|
"EXOSCALE", // Not supported.
|
||||||
"HEXONET", // Not supported.
|
"HEXONET", // Not supported.
|
||||||
"INWX", // Not supported.
|
"INWX", // Not supported.
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
"BIND": {
|
"BIND": {
|
||||||
"domain": "$BIND_DOMAIN"
|
"domain": "$BIND_DOMAIN"
|
||||||
},
|
},
|
||||||
|
"CSCGLOBAL": {
|
||||||
|
"api-key": "$CSCGLOBAL_APIKEY",
|
||||||
|
"user-token": "$CSCGLOBAL_USERTOKEN",
|
||||||
|
"notification_emails": "$CSCGLOBAL_NOTIFICATION",
|
||||||
|
"domain": "$CSCGLOBAL_DOMAIN"
|
||||||
|
},
|
||||||
"CLOUDFLAREAPI": {
|
"CLOUDFLAREAPI": {
|
||||||
"apikey": "$CLOUDFLAREAPI_KEY",
|
"apikey": "$CLOUDFLAREAPI_KEY",
|
||||||
"apitoken": "$CLOUDFLAREAPI_TOKEN",
|
"apitoken": "$CLOUDFLAREAPI_TOKEN",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package models
|
|||||||
// DNSProvider is an interface for DNS Provider plug-ins.
|
// DNSProvider is an interface for DNS Provider plug-ins.
|
||||||
type DNSProvider interface {
|
type DNSProvider interface {
|
||||||
GetNameservers(domain string) ([]*Nameserver, error)
|
GetNameservers(domain string) ([]*Nameserver, error)
|
||||||
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
|
|
||||||
GetZoneRecords(domain string) (Records, error)
|
GetZoneRecords(domain string) (Records, error)
|
||||||
|
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registrar is an interface for Registrar plug-ins.
|
// Registrar is an interface for Registrar plug-ins.
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ so that it is easy to do things the right way in preparation.
|
|||||||
// GetTargetField returns the target. There may be other fields (for example
|
// GetTargetField returns the target. There may be other fields (for example
|
||||||
// an MX record also has a .MxPreference field.
|
// an MX record also has a .MxPreference field.
|
||||||
func (rc *RecordConfig) GetTargetField() string {
|
func (rc *RecordConfig) GetTargetField() string {
|
||||||
|
//if rc.Type == "TXT" {
|
||||||
|
// fmt.Printf("DEBUG: WARNING: GetTargetField called on TXT record is usually wrong: %q\n", rc.target)
|
||||||
|
// //debug.PrintStack()
|
||||||
|
//}
|
||||||
return rc.target
|
return rc.target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package config provides functions for reading and parsing the provider credentials json file.
|
// Package credsfile provides functions for reading and parsing the provider credentials json file.
|
||||||
// It cleans nonstandard json features (comments and trailing commas), as well as replaces environment variable placeholders with
|
// It cleans nonstandard json features (comments and trailing commas), as well as replaces environment variable placeholders with
|
||||||
// their environment variable equivalents. To reference an environment variable in your json file, simply use values in this format:
|
// their environment variable equivalents. To reference an environment variable in your json file, simply use values in this format:
|
||||||
// "key"="$ENV_VAR_NAME"
|
// "key"="$ENV_VAR_NAME"
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ func checkLabelHasMultipleTTLs(records []*models.RecordConfig) (errs []error) {
|
|||||||
for label := range m {
|
for label := range m {
|
||||||
// if after the uniq() pass we still have more than one ttl, it means we have multiple TTLs for that label
|
// if after the uniq() pass we still have more than one ttl, it means we have multiple TTLs for that label
|
||||||
if len(uniq(m[label])) > 1 {
|
if len(uniq(m[label])) > 1 {
|
||||||
errs = append(errs, Warning{fmt.Errorf("multiple TTLs detected for: %s. This should be avoided.", label)})
|
errs = append(errs, Warning{fmt.Errorf("multiple TTLs detected for: %s. This should be avoided", label)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errs
|
return errs
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiBase = "https://apis.cscglobal.com/dbs/api/v2"
|
const apiBase = "https://apis.cscglobal.com/dbs/api/v2"
|
||||||
|
|
||||||
// Api layer for CSC Global
|
// Api layer for CSC Global
|
||||||
|
|
||||||
type cscglobalProvider struct {
|
|
||||||
key string
|
|
||||||
token string
|
|
||||||
notifyEmails []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestParams map[string]string
|
type requestParams map[string]string
|
||||||
|
|
||||||
type errorResponse struct {
|
type errorResponse struct {
|
||||||
@@ -55,8 +54,154 @@ type domainRecord struct {
|
|||||||
Nameserver []string `json:"nameservers"`
|
Nameserver []string `json:"nameservers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
|
// Get zone
|
||||||
var bodyString, err = c.get("/domains/" + domain)
|
|
||||||
|
type nativeRecordA = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
type nativeRecordCNAME = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
type nativeRecordAAAA = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
type nativeRecordTXT = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
type nativeRecordMX = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority uint16 `json:"priority"`
|
||||||
|
}
|
||||||
|
type nativeRecordNS = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
type nativeRecordSRV = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority uint16 `json:"priority"`
|
||||||
|
Weight uint16 `json:"weight"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
}
|
||||||
|
type nativeRecordCAA = struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Flag uint8 `json:"flag"`
|
||||||
|
}
|
||||||
|
type nativeRecordSOA = struct {
|
||||||
|
Serial int `json:"serial"`
|
||||||
|
Refresh int `json:"refresh"`
|
||||||
|
Retry int `json:"retry"`
|
||||||
|
Expire int `json:"expire"`
|
||||||
|
TTL uint32 `json:"ttlMin"`
|
||||||
|
TTLNeg int `json:"ttlNeg"`
|
||||||
|
TTLZone int `json:"ttlZone"`
|
||||||
|
TechEmail string `json:"techEmail"`
|
||||||
|
MasterHost string `json:"masterHost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type zoneResponse struct {
|
||||||
|
ZoneName string `json:"zoneName"`
|
||||||
|
HostingType string `json:"hostingType"`
|
||||||
|
A []nativeRecordA `json:"a"`
|
||||||
|
Cname []nativeRecordCNAME `json:"cname"`
|
||||||
|
Aaaa []nativeRecordAAAA `json:"aaaa"`
|
||||||
|
Txt []nativeRecordTXT `json:"txt"`
|
||||||
|
Mx []nativeRecordMX `json:"mx"`
|
||||||
|
Ns []nativeRecordNS `json:"ns"`
|
||||||
|
Srv []nativeRecordSRV `json:"srv"`
|
||||||
|
Caa []nativeRecordCAA `json:"caa"`
|
||||||
|
Soa nativeRecordSOA `json:"soa"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone edits
|
||||||
|
|
||||||
|
type zoneResourceRecordEdit = struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
RecordType string `json:"recordType"`
|
||||||
|
CurrentKey string `json:"currentKey,omitempty"`
|
||||||
|
CurrentValue string `json:"currentValue,omitempty"`
|
||||||
|
NewKey string `json:"newKey,omitempty"`
|
||||||
|
NewValue string `json:"newValue,omitempty"`
|
||||||
|
NewTTL uint32 `json:"newTtl,omitempty"`
|
||||||
|
// MX and SRV:
|
||||||
|
NewPriority uint16 `json:"newPriority,omitempty"`
|
||||||
|
// SRV:
|
||||||
|
NewWeight uint16 `json:"newWeight,omitempty"`
|
||||||
|
NewPort uint16 `json:"newPort,omitempty"`
|
||||||
|
// CAA:
|
||||||
|
// These are pointers so that we can display the zero-value on demand. If
|
||||||
|
// they were not pointers, the zero-value ("" and 0) would result in no JSON
|
||||||
|
// output for those fields. Sometimes we want to generate fields with
|
||||||
|
// zero-values, such as `"newTag":""`. Thus we make these pointers. The
|
||||||
|
// zero-value is now "nil". If we want the field to appear in the JSON, we
|
||||||
|
// set the pointer to a value. It is no longer nil, and will be output even
|
||||||
|
// if the value at the pointer is zero-value.
|
||||||
|
// See: https://emretanriverdi.medium.com/json-serialization-in-go-a27aeeb968de
|
||||||
|
CurrentTag *string `json:"currentTag,omitempty"`
|
||||||
|
NewTag *string `json:"newTag,omitempty"` // "" needs to be sent explicitly.
|
||||||
|
NewFlag *uint8 `json:"newFlag,omitempty"` // 0 needs to be sent explictly.
|
||||||
|
}
|
||||||
|
|
||||||
|
type zoneEditRequest = struct {
|
||||||
|
ZoneName string `json:"zoneName"`
|
||||||
|
Edits *[]zoneResourceRecordEdit `json:"edits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type zoneEditRequestResultZoneEditRequestResult struct {
|
||||||
|
Content struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"content"`
|
||||||
|
Links struct {
|
||||||
|
Self string `json:"self"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type zoneEditStatusResultZoneEditStatusResult struct {
|
||||||
|
Content struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
ErrorDescription string `json:"errorDescription"`
|
||||||
|
} `json:"content"`
|
||||||
|
Links struct {
|
||||||
|
Cancel string `json:"cancel"`
|
||||||
|
} `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) getNameservers(domain string) ([]string, error) {
|
||||||
|
var bodyString, err = client.get("/domains/" + domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,16 +214,16 @@ func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
|
|||||||
return ns, nil
|
return ns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error {
|
func (client *providerClient) updateNameservers(ns []string, domain string) error {
|
||||||
req := nsModRequest{
|
req := nsModRequest{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
NameServers: ns,
|
NameServers: ns,
|
||||||
DNSType: "OTHER_DNS",
|
DNSType: "OTHER_DNS",
|
||||||
ShowPrice: false,
|
ShowPrice: false,
|
||||||
}
|
}
|
||||||
if c.notifyEmails != nil {
|
if client.notifyEmails != nil {
|
||||||
req.Notifications.Enabled = true
|
req.Notifications.Enabled = true
|
||||||
req.Notifications.Emails = c.notifyEmails
|
req.Notifications.Emails = client.notifyEmails
|
||||||
}
|
}
|
||||||
req.CustomFields = []string{}
|
req.CustomFields = []string{}
|
||||||
|
|
||||||
@@ -87,7 +232,7 @@ func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyString, err := c.put("/domains/nsmodification", requestBody)
|
bodyString, err := client.put("/domains/nsmodification", requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CSC Global: Error update NS : %w", err)
|
return fmt.Errorf("CSC Global: Error update NS : %w", err)
|
||||||
}
|
}
|
||||||
@@ -101,17 +246,273 @@ func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, error) {
|
// domainsResult is the JSON returned by "/domains". Fields we don't
|
||||||
client := &http.Client{}
|
// use are commented out.
|
||||||
|
type domainsResult struct {
|
||||||
|
Meta struct {
|
||||||
|
NumResults int `json:"numResults"`
|
||||||
|
Pages int `json:"pages"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Domains []struct {
|
||||||
|
QualifiedDomainName string `json:"qualifiedDomainName"`
|
||||||
|
// Domain string `json:"domain"`
|
||||||
|
// Idn string `json:"idn"`
|
||||||
|
// Extension string `json:"extension"`
|
||||||
|
// NewGtld bool `json:"newGtld"`
|
||||||
|
// ManagedStatus string `json:"managedStatus"`
|
||||||
|
// RegistrationDate string `json:"registrationDate"`
|
||||||
|
// RegistryExpiryDate string `json:"registryExpiryDate"`
|
||||||
|
// PaidThroughDate string `json:"paidThroughDate"`
|
||||||
|
// CountryCode string `json:"countryCode"`
|
||||||
|
// ServerDeleteProhibited bool `json:"serverDeleteProhibited"`
|
||||||
|
// ServerTransferProhibited bool `json:"serverTransferProhibited"`
|
||||||
|
// ServerUpdateProhibited bool `json:"serverUpdateProhibited"`
|
||||||
|
// DNSType string `json:"dnsType"`
|
||||||
|
// WhoisPrivacy bool `json:"whoisPrivacy"`
|
||||||
|
// LocalAgent bool `json:"localAgent"`
|
||||||
|
// DnssecActivated string `json:"dnssecActivated"`
|
||||||
|
// CriticalDomain bool `json:"criticalDomain"`
|
||||||
|
// BusinessUnit string `json:"businessUnit"`
|
||||||
|
// BrandName string `json:"brandName"`
|
||||||
|
// IdnReferenceName string `json:"idnReferenceName"`
|
||||||
|
// CustomFields []interface{} `json:"customFields"`
|
||||||
|
// Account struct {
|
||||||
|
// AccountNumber string `json:"accountNumber"`
|
||||||
|
// AccountName string `json:"accountName"`
|
||||||
|
// } `json:"account"`
|
||||||
|
// Urlf struct {
|
||||||
|
// RedirectType string `json:"redirectType"`
|
||||||
|
// URLForwarding bool `json:"urlForwarding"`
|
||||||
|
// } `json:"urlf"`
|
||||||
|
// NameServers []string `json:"nameServers"`
|
||||||
|
// WhoisContacts []struct {
|
||||||
|
// ContactType string `json:"contactType"`
|
||||||
|
// FirstName string `json:"firstName"`
|
||||||
|
// LastName string `json:"lastName"`
|
||||||
|
// Organization string `json:"organization"`
|
||||||
|
// Street1 string `json:"street1"`
|
||||||
|
// Street2 string `json:"street2"`
|
||||||
|
// City string `json:"city"`
|
||||||
|
// StateProvince string `json:"stateProvince"`
|
||||||
|
// Country string `json:"country"`
|
||||||
|
// PostalCode string `json:"postalCode"`
|
||||||
|
// Email string `json:"email"`
|
||||||
|
// Phone string `json:"phone"`
|
||||||
|
// PhoneExtn string `json:"phoneExtn"`
|
||||||
|
// Fax string `json:"fax"`
|
||||||
|
// } `json:"whoisContacts"`
|
||||||
|
// LastModifiedDate string `json:"lastModifiedDate"`
|
||||||
|
// LastModifiedReason string `json:"lastModifiedReason"`
|
||||||
|
// LastModifiedDescription string `json:"lastModifiedDescription"`
|
||||||
|
} `json:"domains"`
|
||||||
|
// Links struct {
|
||||||
|
// Self string `json:"self"`
|
||||||
|
// } `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) getDomains() ([]string, error) {
|
||||||
|
var bodyString, err = client.get("/domains")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
//fmt.Printf("DEBUG: GETDOMAINS bodystring = %s\n", bodyString)
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
|
||||||
|
var dr domainsResult
|
||||||
|
json.Unmarshal(bodyString, &dr)
|
||||||
|
|
||||||
|
if dr.Meta.Pages > 1 {
|
||||||
|
return nil, fmt.Errorf("cscglobal getDomains: unimplemented paganation")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []string
|
||||||
|
for _, d := range dr.Domains {
|
||||||
|
r = append(r, d.QualifiedDomainName)
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
//fmt.Printf("DEBUG: GETDOMAINS dr = %+v\n", dr)
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) getZoneRecordsAll(zone string) (*zoneResponse, error) {
|
||||||
|
var bodyString, err = client.get("/zones/" + zone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cscDebug {
|
||||||
|
fmt.Printf("------------------\n")
|
||||||
|
fmt.Printf("DEBUG: ZONE RESPONSE = %s\n", bodyString)
|
||||||
|
fmt.Printf("------------------\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dr zoneResponse
|
||||||
|
json.Unmarshal(bodyString, &dr)
|
||||||
|
|
||||||
|
return &dr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) sendZoneEditRequest(domainname string, edits []zoneResourceRecordEdit) error {
|
||||||
|
|
||||||
|
req := zoneEditRequest{
|
||||||
|
ZoneName: domainname,
|
||||||
|
Edits: &edits,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cscDebug {
|
||||||
|
fmt.Printf("DEBUG: edit request = %s\n", requestBody)
|
||||||
|
}
|
||||||
|
responseBody, err := client.post("/zones/edits", requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errResp zoneEditRequestResultZoneEditRequestResult
|
||||||
|
err = json.Unmarshal(responseBody, &errResp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, errResp)
|
||||||
|
}
|
||||||
|
if errResp.Content.Status != "SUCCESS" {
|
||||||
|
return fmt.Errorf("CSC Global API error: %s DATA: %q", errResp.Content.Status, errResp.Content.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request was successfully submitted. Now query the status link until the request is complete.
|
||||||
|
statusURL := errResp.Links.Status
|
||||||
|
return client.waitRequestURL(statusURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) waitRequest(reqID string) error {
|
||||||
|
return client.waitRequestURL(apiBase + "/zones/edits/status/" + reqID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) waitRequestURL(statusURL string) error {
|
||||||
|
t1 := time.Now()
|
||||||
|
for {
|
||||||
|
statusBody, err := client.geturl(statusURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println()
|
||||||
|
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, statusBody)
|
||||||
|
}
|
||||||
|
var statusResp zoneEditStatusResultZoneEditStatusResult
|
||||||
|
err = json.Unmarshal(statusBody, &statusResp)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println()
|
||||||
|
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, statusBody)
|
||||||
|
}
|
||||||
|
status, msg := statusResp.Content.Status, statusResp.Content.ErrorDescription
|
||||||
|
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
dur := time.Since(t1).Round(time.Second)
|
||||||
|
if msg == "" {
|
||||||
|
fmt.Printf("WAITING: % 6s STATUS=%s \r", dur, status)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("WAITING: % 6s STATUS=%s MSG=%q \r", dur, status, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if status == "FAILED" {
|
||||||
|
fmt.Println()
|
||||||
|
parts := strings.Split(statusResp.Links.Cancel, "/")
|
||||||
|
client.cancelRequest(parts[len(parts)-1])
|
||||||
|
return fmt.Errorf("update failed: %s %s", msg, statusURL)
|
||||||
|
}
|
||||||
|
if status == "COMPLETED" {
|
||||||
|
fmt.Println()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
// Response looks like:
|
||||||
|
//{
|
||||||
|
// "content": {
|
||||||
|
// "status": "SUCCESS",
|
||||||
|
// "message": "The publish request was successfully enqueued."
|
||||||
|
// },
|
||||||
|
// "links": {
|
||||||
|
// "self": "https://apis.cscglobal.com/dbs/api/v2/zones/edits/9e139e34-a2a1-462e-88ab-3645833a55d4",
|
||||||
|
// "status": "https://apis.cscglobal.com/dbs/api/v2/zones/edits/status/9e139e34-a2a1-462e-88ab-3645833a55d4"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel pending/stuck edits
|
||||||
|
|
||||||
|
type pagedZoneEditResponsePagedZoneEditResponse struct {
|
||||||
|
Meta struct {
|
||||||
|
NumResults int `json:"numResults"`
|
||||||
|
Pages int `json:"pages"`
|
||||||
|
} `json:"meta"`
|
||||||
|
ZoneEdits []struct {
|
||||||
|
ZoneName string `json:"zoneName"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"zoneEdits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) clearRequests(domain string) error {
|
||||||
|
var bodyString, err = client.get("/zones/edits?filter=zoneName==" + domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dr pagedZoneEditResponsePagedZoneEditResponse
|
||||||
|
json.Unmarshal(bodyString, &dr)
|
||||||
|
|
||||||
|
// TODO(tlim): Properly handle paganation.
|
||||||
|
if dr.Meta.Pages != 1 {
|
||||||
|
return fmt.Errorf("cancelPendingEdits failed: Pages=%d", dr.Meta.Pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ze := range dr.ZoneEdits {
|
||||||
|
if cscDebug {
|
||||||
|
if ze.Status != "COMPLETED" && ze.Status != "CANCELED" {
|
||||||
|
fmt.Printf("REQUEST %d: %s %s\n", i, ze.ID, ze.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch ze.Status {
|
||||||
|
case "PROPAGATING":
|
||||||
|
fmt.Printf("INFO: Waiting for id=%s status=%s\n", ze.ID, ze.Status)
|
||||||
|
client.waitRequest(ze.ID)
|
||||||
|
case "FAILED":
|
||||||
|
fmt.Printf("INFO: Deleting request status=%s id=%s\n", ze.Status, ze.ID)
|
||||||
|
client.cancelRequest(ze.ID)
|
||||||
|
case "COMPLETED", "CANCELED":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cscglobal ClearRequests: unimplemented status: %q", ze.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) cancelRequest(reqID string) error {
|
||||||
|
_, err := client.delete("/zones/edits/" + reqID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) put(endpoint string, requestBody []byte) ([]byte, error) {
|
||||||
|
hclient := &http.Client{}
|
||||||
req, _ := http.NewRequest("PUT", apiBase+endpoint, bytes.NewReader(requestBody))
|
req, _ := http.NewRequest("PUT", apiBase+endpoint, bytes.NewReader(requestBody))
|
||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
req.Header.Add("apikey", c.key)
|
req.Header.Add("apikey", client.key)
|
||||||
req.Header.Add("Authorization", "Bearer "+c.token)
|
req.Header.Add("Authorization", "Bearer "+client.token)
|
||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := hclient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -135,16 +536,95 @@ func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, er
|
|||||||
req.Host, req.URL.RequestURI())
|
req.Host, req.URL.RequestURI())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cscglobalProvider) get(endpoint string) ([]byte, error) {
|
func (client *providerClient) delete(endpoint string) ([]byte, error) {
|
||||||
client := &http.Client{}
|
hclient := &http.Client{}
|
||||||
req, _ := http.NewRequest("GET", apiBase+endpoint, nil)
|
fmt.Printf("DEBUG: delete endpoint: %q\n", apiBase+endpoint)
|
||||||
|
req, _ := http.NewRequest("DELETE", apiBase+endpoint, nil)
|
||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
req.Header.Add("apikey", c.key)
|
req.Header.Add("apikey", client.key)
|
||||||
req.Header.Add("Authorization", "Bearer "+c.token)
|
req.Header.Add("Authorization", "Bearer "+client.token)
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := hclient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyString, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
fmt.Printf("DEBUG: Delete successful (200)\n")
|
||||||
|
return bodyString, nil
|
||||||
|
}
|
||||||
|
fmt.Printf("DEBUG: Delete failed (%d)\n", resp.StatusCode)
|
||||||
|
|
||||||
|
// Got a error response from API, see if it's json format
|
||||||
|
var errResp errorResponse
|
||||||
|
err = json.Unmarshal(bodyString, &errResp)
|
||||||
|
if err != nil {
|
||||||
|
// Some error messages are plain text
|
||||||
|
return nil, fmt.Errorf("CSC Global API error: %s URL: %s%s",
|
||||||
|
bodyString,
|
||||||
|
req.Host, req.URL.RequestURI())
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CSC Global API error code: %s description: %s URL: %s%s",
|
||||||
|
errResp.Code, errResp.Description,
|
||||||
|
req.Host, req.URL.RequestURI())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) post(endpoint string, requestBody []byte) ([]byte, error) {
|
||||||
|
hclient := &http.Client{}
|
||||||
|
req, _ := http.NewRequest("POST", apiBase+endpoint, bytes.NewBuffer(requestBody))
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
req.Header.Add("apikey", client.key)
|
||||||
|
req.Header.Add("Authorization", "Bearer "+client.token)
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := hclient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyString, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
//fmt.Printf("DEBUG: resp.StatusCode == %d\n", resp.StatusCode)
|
||||||
|
//fmt.Printf("POST RESPONSE = %s\n", bodyString)
|
||||||
|
//fmt.Printf("------------------\n")
|
||||||
|
if resp.StatusCode == 201 {
|
||||||
|
return bodyString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got a error response from API, see if it's json format
|
||||||
|
var errResp errorResponse
|
||||||
|
err = json.Unmarshal(bodyString, &errResp)
|
||||||
|
if err != nil {
|
||||||
|
// Some error messages are plain text
|
||||||
|
return nil, fmt.Errorf("CSC Global API error: %s URL: %s%s",
|
||||||
|
bodyString,
|
||||||
|
req.Host, req.URL.RequestURI())
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CSC Global API error code: %s description: %s URL: %s%s",
|
||||||
|
errResp.Code, errResp.Description,
|
||||||
|
req.Host, req.URL.RequestURI())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) get(endpoint string) ([]byte, error) {
|
||||||
|
return client.geturl(apiBase + endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) geturl(url string) ([]byte, error) {
|
||||||
|
hclient := &http.Client{}
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
req.Header.Add("apikey", client.key)
|
||||||
|
req.Header.Add("Authorization", "Bearer "+client.token)
|
||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := hclient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,39 @@ package cscglobal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/StackExchange/dnscontrol/v3/models"
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/recordaudit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuditRecords returns an error if any records are not
|
// AuditRecords returns an error if any records are not
|
||||||
// supportable by this provider.
|
// supportable by this provider.
|
||||||
func AuditRecords(records []*models.RecordConfig) error {
|
func AuditRecords(records []*models.RecordConfig) error {
|
||||||
|
|
||||||
|
// Each test should be encapsulated in a function that can be tested
|
||||||
|
// individually. If the test is of general use, add it to the
|
||||||
|
// recordaudit module.
|
||||||
|
|
||||||
|
// Each test should document the last time we verified the test was
|
||||||
|
// still needed. Sometimes companies change their API.
|
||||||
|
|
||||||
|
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
|
||||||
|
return err
|
||||||
|
} // Needed as of 2022-06-10
|
||||||
|
|
||||||
|
if err := recordaudit.TxtNoLen255(records); err != nil {
|
||||||
|
return err
|
||||||
|
} // Needed as of 2022-06-10
|
||||||
|
|
||||||
|
if err := recordaudit.TxtNoMultipleStrings(records); err != nil {
|
||||||
|
return err
|
||||||
|
} // Needed as of 2022-06-10
|
||||||
|
|
||||||
|
if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
|
||||||
|
return err
|
||||||
|
} // Needed as of 2022-06-10
|
||||||
|
|
||||||
|
if err := recordaudit.TxtNotEmpty(records); err != nil {
|
||||||
|
return err
|
||||||
|
} // Needed as of 2022-06-10
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
129
providers/cscglobal/convert.go
Normal file
129
providers/cscglobal/convert.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package cscglobal
|
||||||
|
|
||||||
|
// Convert the provider's native record description to models.RecordConfig.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nativeToRecordA takes an A record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordA(nr nativeRecordA, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "A",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetIP(net.ParseIP(nr.Value).To4())
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordCNAME takes a CNAME record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordCNAME(nr nativeRecordCNAME, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "CNAME",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTarget(nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordA takes an AAAA record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordAAAA(nr nativeRecordAAAA, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "AAAA",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetIP(net.ParseIP(nr.Value).To16())
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordTXT takes a TXT record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordTXT(nr nativeRecordTXT, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetTXT(nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordMX takes a MX record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordMX(nr nativeRecordMX, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "MX",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetMX(nr.Priority, nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordNS takes a NS record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordNS(nr nativeRecordNS, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "NS",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTarget(nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordSRV takes a SRV record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordSRV(nr nativeRecordSRV, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "SRV",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetSRV(nr.Priority, nr.Weight, nr.Port, nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeToRecordCAA takes a CAA record from DNS and returns a native RecordConfig struct.
|
||||||
|
func nativeToRecordCAA(nr nativeRecordCAA, origin string, defaultTTL uint32) *models.RecordConfig {
|
||||||
|
ttl := nr.TTL
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = defaultTTL
|
||||||
|
}
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "CAA",
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
rc.SetLabel(nr.Key, origin)
|
||||||
|
rc.SetTargetCAA(nr.Flag, nr.Tag, nr.Value)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package cscglobal
|
package cscglobal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/models"
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,12 +18,32 @@ Info required in `creds.json`:
|
|||||||
- notification_emails (optional) Comma separated list of email addresses to send notifications to
|
- notification_emails (optional) Comma separated list of email addresses to send notifications to
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func init() {
|
type providerClient struct {
|
||||||
providers.RegisterRegistrarType("CSCGLOBAL", newCscGlobal)
|
key string
|
||||||
|
token string
|
||||||
|
notifyEmails []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCscGlobal(m map[string]string) (providers.Registrar, error) {
|
var features = providers.DocumentationNotes{
|
||||||
api := &cscglobalProvider{}
|
providers.CanGetZones: providers.Can(),
|
||||||
|
//providers.CanUseCAA: providers.Can(),
|
||||||
|
providers.CanUseSRV: providers.Can(),
|
||||||
|
providers.DocOfficiallySupported: providers.Can(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cscDebug to true if you want to see the JSON of important API requests and responses.
|
||||||
|
var cscDebug = false
|
||||||
|
|
||||||
|
func newReg(conf map[string]string) (providers.Registrar, error) {
|
||||||
|
return newProvider(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
|
return newProvider(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProvider(m map[string]string) (*providerClient, error) {
|
||||||
|
api := &providerClient{}
|
||||||
|
|
||||||
api.key, api.token = m["api-key"], m["user-token"]
|
api.key, api.token = m["api-key"], m["user-token"]
|
||||||
if api.key == "" || api.token == "" {
|
if api.key == "" || api.token == "" {
|
||||||
@@ -38,35 +57,12 @@ func newCscGlobal(m map[string]string) (providers.Registrar, error) {
|
|||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRegistrarCorrections gathers corrections that would being n to match dc.
|
func init() {
|
||||||
func (c *cscglobalProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
providers.RegisterRegistrarType("CSCGLOBAL", newReg)
|
||||||
nss, err := c.getNameservers(dc.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
foundNameservers := strings.Join(nss, ",")
|
|
||||||
|
|
||||||
expected := []string{}
|
fns := providers.DspFuncs{
|
||||||
for _, ns := range dc.Nameservers {
|
Initializer: newDsp,
|
||||||
if ns.Name[len(ns.Name)-1] == '.' {
|
RecordAuditor: AuditRecords,
|
||||||
// When this code was written ns.Name never included a single trailing dot.
|
|
||||||
// If that changes, the code should change too.
|
|
||||||
return nil, fmt.Errorf("name server includes a trailing dot, has the API changed?")
|
|
||||||
}
|
|
||||||
expected = append(expected, ns.Name)
|
|
||||||
}
|
}
|
||||||
sort.Strings(expected)
|
providers.RegisterDomainServiceProviderType("CSCGLOBAL", fns, features)
|
||||||
expectedNameservers := strings.Join(expected, ",")
|
|
||||||
|
|
||||||
if foundNameservers != expectedNameservers {
|
|
||||||
return []*models.Correction{
|
|
||||||
{
|
|
||||||
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
|
|
||||||
F: func() error {
|
|
||||||
return c.updateNameservers(expected, dc.Name)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
314
providers/cscglobal/dns.go
Normal file
314
providers/cscglobal/dns.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package cscglobal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||||
|
func (client *providerClient) GetZoneRecords(domain string) (models.Records, error) {
|
||||||
|
records, err := client.getZoneRecordsAll(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert them to DNScontrol's native format:
|
||||||
|
|
||||||
|
existingRecords := []*models.RecordConfig{}
|
||||||
|
|
||||||
|
// Option 1: One long list. If your provider returns one long list,
|
||||||
|
// convert each one to RecordType like this:
|
||||||
|
// for _, rr := range records {
|
||||||
|
// existingRecords = append(existingRecords, nativeToRecord(rr, domain))
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Option 2: Grouped records. Sometimes the provider returns one item per
|
||||||
|
// label. Each item contains a list of all the records at that label.
|
||||||
|
// You'll need to split them out into one RecordConfig for each record. An
|
||||||
|
// example of this is the ROUTE53 provider.
|
||||||
|
// for _, rg := range records {
|
||||||
|
// for _, rr := range rg {
|
||||||
|
// existingRecords = append(existingRecords, nativeToRecords(rg, rr, domain)...)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Option 3: Something else. In this case, we get a big massive structure
|
||||||
|
// which needs to be broken up. Still, we're generating a list of
|
||||||
|
// RecordConfig structures.
|
||||||
|
defaultTTL := records.Soa.TTL
|
||||||
|
for _, rr := range records.A {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordA(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Cname {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordCNAME(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Aaaa {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordAAAA(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Txt {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordTXT(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Mx {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordMX(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Ns {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordNS(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Srv {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordSRV(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
for _, rr := range records.Caa {
|
||||||
|
existingRecords = append(existingRecords, nativeToRecordCAA(rr, domain, defaultTTL))
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *providerClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||||
|
nss, err := client.getNameservers(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return models.ToNameservers(nss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainCorrections get the current and existing records,
|
||||||
|
// post-process them, and generate corrections.
|
||||||
|
// NB(tlim): This function should be exactly the same in all DNS providers. Once
|
||||||
|
// all providers do this, we can eliminate it and use a Go interface instead.
|
||||||
|
func (client *providerClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||||
|
existing, err := client.GetZoneRecords(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
models.PostProcessRecords(existing)
|
||||||
|
//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||||
|
|
||||||
|
clean := PrepFoundRecords(existing)
|
||||||
|
PrepDesiredRecords(dc)
|
||||||
|
return client.GenerateDomainCorrections(dc, clean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepFoundRecords munges any records to make them compatible with
|
||||||
|
// this provider. Usually this is a no-op.
|
||||||
|
func PrepFoundRecords(recs models.Records) models.Records {
|
||||||
|
// If there are records that need to be modified, removed, etc. we
|
||||||
|
// do it here. Usually this is a no-op.
|
||||||
|
return recs
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepDesiredRecords munges any records to best suit this provider.
|
||||||
|
func PrepDesiredRecords(dc *models.DomainConfig) {
|
||||||
|
// Sort through the dc.Records, eliminate any that can't be
|
||||||
|
// supported; modify any that need adjustments to work with the
|
||||||
|
// provider. We try to do minimal changes otherwise it gets
|
||||||
|
// confusing.
|
||||||
|
|
||||||
|
dc.Punycode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
|
||||||
|
func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
|
||||||
|
|
||||||
|
// Read foundRecords:
|
||||||
|
foundRecords, err := client.GetZoneRecords(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("c.GetDNSZoneRecords(%v) failed: %v", dc.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
models.PostProcessRecords(foundRecords)
|
||||||
|
//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||||
|
|
||||||
|
differ := diff.New(dc)
|
||||||
|
_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// How to generate corrections?
|
||||||
|
|
||||||
|
// (1) Most providers take individual deletes, creates, and
|
||||||
|
// modifications:
|
||||||
|
|
||||||
|
// // Generate changes.
|
||||||
|
// corrections := []*models.Correction{}
|
||||||
|
// for _, del := range dels {
|
||||||
|
// corrections = append(corrections, client.deleteRec(client.dnsserver, dc.Name, del))
|
||||||
|
// }
|
||||||
|
// for _, cre := range creates {
|
||||||
|
// corrections = append(corrections, client.createRec(client.dnsserver, dc.Name, cre)...)
|
||||||
|
// }
|
||||||
|
// for _, m := range modifications {
|
||||||
|
// corrections = append(corrections, client.modifyRec(client.dnsserver, dc.Name, m))
|
||||||
|
// }
|
||||||
|
// return corrections, nil
|
||||||
|
|
||||||
|
// (2) Some providers upload the entire zone every time. Look at
|
||||||
|
// GetDomainCorrections for BIND and NAMECHEAP for inspiration.
|
||||||
|
|
||||||
|
// (3) Others do something entirely different. Like CSCGlobal:
|
||||||
|
|
||||||
|
// CSCGlobal has a unique API. A list of edits is sent in one API
|
||||||
|
// call. Edits aren't permitted if an existing edit is being
|
||||||
|
// processed. Therefore, before we do an edit we block until the
|
||||||
|
// previous edit is done executing.
|
||||||
|
|
||||||
|
var edits []zoneResourceRecordEdit
|
||||||
|
var descriptions []string
|
||||||
|
for _, del := range dels {
|
||||||
|
edits = append(edits, makePurge(dc.Name, del))
|
||||||
|
descriptions = append(descriptions, del.String())
|
||||||
|
}
|
||||||
|
for _, cre := range creates {
|
||||||
|
edits = append(edits, makeAdd(dc.Name, cre))
|
||||||
|
descriptions = append(descriptions, cre.String())
|
||||||
|
}
|
||||||
|
for _, m := range modifications {
|
||||||
|
edits = append(edits, makeEdit(dc.Name, m))
|
||||||
|
descriptions = append(descriptions, m.String())
|
||||||
|
}
|
||||||
|
corrections := []*models.Correction{}
|
||||||
|
if len(edits) > 0 {
|
||||||
|
c := &models.Correction{
|
||||||
|
Msg: "\t" + strings.Join(descriptions, "\n\t"),
|
||||||
|
F: func() error {
|
||||||
|
// CSCGlobal's API only permits one pending update at a time.
|
||||||
|
// Therefore we block until any outstanding updates are done.
|
||||||
|
// We also clear out any failures, since (and I can't believe
|
||||||
|
// I'm writing this) any time something fails, the failure has
|
||||||
|
// to be cleared out with an additional API call.
|
||||||
|
|
||||||
|
err := client.clearRequests(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return client.sendZoneEditRequest(dc.Name, edits)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
corrections = append(corrections, c)
|
||||||
|
}
|
||||||
|
return corrections, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
|
||||||
|
var existingTarget string
|
||||||
|
|
||||||
|
switch cor.Existing.Type {
|
||||||
|
case "TXT":
|
||||||
|
existingTarget = strings.Join(cor.Existing.TxtStrings, "")
|
||||||
|
default:
|
||||||
|
existingTarget = cor.Existing.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
zer := zoneResourceRecordEdit{
|
||||||
|
Action: "PURGE",
|
||||||
|
RecordType: cor.Existing.Type,
|
||||||
|
CurrentKey: cor.Existing.Name,
|
||||||
|
CurrentValue: existingTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cor.Existing.Type == "CAA" {
|
||||||
|
var tagValue = cor.Existing.CaaTag
|
||||||
|
//fmt.Printf("DEBUG: CAA TAG = %q\n", tagValue)
|
||||||
|
zer.CurrentTag = &tagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return zer
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
|
||||||
|
rec := cre.Desired
|
||||||
|
|
||||||
|
var recTarget string
|
||||||
|
switch rec.Type {
|
||||||
|
case "TXT":
|
||||||
|
recTarget = strings.Join(rec.TxtStrings, "")
|
||||||
|
default:
|
||||||
|
recTarget = rec.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
zer := zoneResourceRecordEdit{
|
||||||
|
Action: "ADD",
|
||||||
|
RecordType: rec.Type,
|
||||||
|
NewKey: rec.Name,
|
||||||
|
NewValue: recTarget,
|
||||||
|
NewTTL: rec.TTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rec.Type {
|
||||||
|
case "CAA":
|
||||||
|
var tagValue = rec.CaaTag
|
||||||
|
var flagValue = rec.CaaFlag
|
||||||
|
zer.NewTag = &tagValue
|
||||||
|
zer.NewFlag = &flagValue
|
||||||
|
case "MX":
|
||||||
|
zer.NewPriority = rec.MxPreference
|
||||||
|
case "SRV":
|
||||||
|
zer.NewPriority = rec.SrvPriority
|
||||||
|
zer.NewWeight = rec.SrvWeight
|
||||||
|
zer.NewPort = rec.SrvPort
|
||||||
|
case "TXT":
|
||||||
|
zer.NewValue = strings.Join(rec.TxtStrings, "")
|
||||||
|
default: // "A", "CNAME", "NS"
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
return zer
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeEdit(domainname string, m diff.Correlation) zoneResourceRecordEdit {
|
||||||
|
old, rec := m.Existing, m.Desired
|
||||||
|
// TODO: Assert that old.Type == rec.Type
|
||||||
|
// TODO: Assert that old.Name == rec.Name
|
||||||
|
|
||||||
|
var oldTarget, recTarget string
|
||||||
|
switch old.Type {
|
||||||
|
case "TXT":
|
||||||
|
oldTarget = strings.Join(old.TxtStrings, "")
|
||||||
|
recTarget = strings.Join(rec.TxtStrings, "")
|
||||||
|
default:
|
||||||
|
oldTarget = old.GetTargetField()
|
||||||
|
recTarget = rec.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
zer := zoneResourceRecordEdit{
|
||||||
|
Action: "EDIT",
|
||||||
|
RecordType: old.Type,
|
||||||
|
CurrentKey: old.Name,
|
||||||
|
CurrentValue: oldTarget,
|
||||||
|
}
|
||||||
|
if oldTarget != recTarget {
|
||||||
|
zer.NewValue = recTarget
|
||||||
|
}
|
||||||
|
if old.TTL != rec.TTL {
|
||||||
|
zer.NewTTL = rec.TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
switch old.Type {
|
||||||
|
case "CAA":
|
||||||
|
var tagValue = old.CaaTag
|
||||||
|
zer.CurrentTag = &tagValue
|
||||||
|
if old.CaaTag != rec.CaaTag {
|
||||||
|
zer.NewTag = &(rec.CaaTag)
|
||||||
|
}
|
||||||
|
if old.CaaFlag != rec.CaaFlag {
|
||||||
|
zer.NewFlag = &(rec.CaaFlag)
|
||||||
|
}
|
||||||
|
case "MX":
|
||||||
|
if old.MxPreference != rec.MxPreference {
|
||||||
|
zer.NewPriority = rec.MxPreference
|
||||||
|
}
|
||||||
|
case "SRV":
|
||||||
|
zer.NewWeight = rec.SrvWeight
|
||||||
|
zer.NewPort = rec.SrvPort
|
||||||
|
zer.NewPriority = rec.SrvPriority
|
||||||
|
default: // "A", "CNAME", "NS", "TXT"
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
return zer
|
||||||
|
}
|
||||||
7
providers/cscglobal/listzones.go
Normal file
7
providers/cscglobal/listzones.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package cscglobal
|
||||||
|
|
||||||
|
// ListZones returns all the zones in an account
|
||||||
|
func (client *providerClient) ListZones() ([]string, error) {
|
||||||
|
|
||||||
|
return client.getDomains()
|
||||||
|
}
|
||||||
42
providers/cscglobal/registrar.go
Normal file
42
providers/cscglobal/registrar.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package cscglobal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRegistrarCorrections gathers corrections that would being n to match dc.
|
||||||
|
func (client *providerClient) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||||
|
nss, err := client.getNameservers(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundNameservers := strings.Join(nss, ",")
|
||||||
|
|
||||||
|
expected := []string{}
|
||||||
|
for _, ns := range dc.Nameservers {
|
||||||
|
if ns.Name[len(ns.Name)-1] == '.' {
|
||||||
|
// When this code was written ns.Name never included a single trailing dot.
|
||||||
|
// If that changes, the code should change too.
|
||||||
|
return nil, fmt.Errorf("name server includes a trailing dot, has the API changed?")
|
||||||
|
}
|
||||||
|
expected = append(expected, ns.Name)
|
||||||
|
}
|
||||||
|
sort.Strings(expected)
|
||||||
|
expectedNameservers := strings.Join(expected, ",")
|
||||||
|
|
||||||
|
if foundNameservers != expectedNameservers {
|
||||||
|
return []*models.Correction{
|
||||||
|
{
|
||||||
|
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
|
||||||
|
F: func() error {
|
||||||
|
return client.updateNameservers(expected, dc.Name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ func CreateRegistrar(rType string, config map[string]string) (Registrar, error)
|
|||||||
|
|
||||||
initer, ok := RegistrarTypes[rType]
|
initer, ok := RegistrarTypes[rType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("No such registrar type: %q", rType)
|
return nil, fmt.Errorf("no such registrar type: %q", rType)
|
||||||
}
|
}
|
||||||
return initer(config)
|
return initer(config)
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func CreateDNSProvider(providerTypeName string, config map[string]string, meta j
|
|||||||
|
|
||||||
p, ok := DNSProviderTypes[providerTypeName]
|
p, ok := DNSProviderTypes[providerTypeName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("No such DNS service provider: %q", providerTypeName)
|
return nil, fmt.Errorf("no such DNS service provider: %q", providerTypeName)
|
||||||
}
|
}
|
||||||
return p.Initializer(config, meta)
|
return p.Initializer(config, meta)
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ func beCompatible(n string, config map[string]string) (string, error) {
|
|||||||
func AuditRecords(dType string, rcs models.Records) error {
|
func AuditRecords(dType string, rcs models.Records) error {
|
||||||
p, ok := DNSProviderTypes[dType]
|
p, ok := DNSProviderTypes[dType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Unknown DNS service provider type: %q", dType)
|
return fmt.Errorf("unknown DNS service provider type: %q", dType)
|
||||||
}
|
}
|
||||||
if p.RecordAuditor == nil {
|
if p.RecordAuditor == nil {
|
||||||
return fmt.Errorf("DNS service provider type %q has no RecordAuditor", dType)
|
return fmt.Errorf("DNS service provider type %q has no RecordAuditor", dType)
|
||||||
|
|||||||
Reference in New Issue
Block a user