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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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)
|
||||
* **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
|
||||
two zones and returns records that are unchanged, created, deleted,
|
||||
|
2
go.mod
2
go.mod
@@ -62,6 +62,7 @@ require (
|
||||
|
||||
require (
|
||||
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
|
||||
)
|
||||
|
||||
@@ -129,7 +130,6 @@ require (
|
||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // 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-isatty v0.0.14 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
title := fmt.Sprintf("Create TXT %s", s)
|
||||
label := fmt.Sprintf("foo%d", len(s))
|
||||
@@ -760,6 +764,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
not(
|
||||
"AUTODNS",
|
||||
"AZURE_DNS",
|
||||
"CSCGLOBAL", // Last verified 2022-06-07
|
||||
"DIGITALOCEAN",
|
||||
"DNSIMPLE",
|
||||
"GANDI_V5",
|
||||
@@ -900,7 +905,10 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
tc("Create TXT with double-quote", txt("foodq", `quo"te`)),
|
||||
clear(),
|
||||
tc("Create TXT with ws at end", txt("foows1", "with space at end ")),
|
||||
clear(),
|
||||
),
|
||||
|
||||
//
|
||||
testgroup("gentxt TXT",
|
||||
gentxt("0"),
|
||||
gentxt("1"),
|
||||
gentxt("10"),
|
||||
@@ -1030,6 +1038,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
// - DIGITALOCEAN: page size is 100 (default: 20)
|
||||
not(
|
||||
"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.
|
||||
"MSDNS", // No paging done. No need to test.
|
||||
"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",
|
||||
only(
|
||||
//"MSDNS", // No paging done. No need to test.
|
||||
//"AZURE_DNS", // Currently failing.
|
||||
"HEXONET",
|
||||
//"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
|
||||
"GCLOUD",
|
||||
"HEXONET",
|
||||
//"MSDNS", // No paging done. No need to test.
|
||||
"ROUTE53",
|
||||
),
|
||||
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.
|
||||
//"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
|
||||
//"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
|
||||
"HEXONET",
|
||||
"HOSTINGDE",
|
||||
//"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),
|
||||
not(
|
||||
"CSCGLOBAL", // Not supported.
|
||||
"EXOSCALE", // Not supported.
|
||||
"HEXONET", // Not supported.
|
||||
"INWX", // Not supported.
|
||||
|
@@ -27,6 +27,12 @@
|
||||
"BIND": {
|
||||
"domain": "$BIND_DOMAIN"
|
||||
},
|
||||
"CSCGLOBAL": {
|
||||
"api-key": "$CSCGLOBAL_APIKEY",
|
||||
"user-token": "$CSCGLOBAL_USERTOKEN",
|
||||
"notification_emails": "$CSCGLOBAL_NOTIFICATION",
|
||||
"domain": "$CSCGLOBAL_DOMAIN"
|
||||
},
|
||||
"CLOUDFLAREAPI": {
|
||||
"apikey": "$CLOUDFLAREAPI_KEY",
|
||||
"apitoken": "$CLOUDFLAREAPI_TOKEN",
|
||||
|
@@ -3,8 +3,8 @@ package models
|
||||
// DNSProvider is an interface for DNS Provider plug-ins.
|
||||
type DNSProvider interface {
|
||||
GetNameservers(domain string) ([]*Nameserver, error)
|
||||
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
|
||||
GetZoneRecords(domain string) (Records, error)
|
||||
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
// an MX record also has a .MxPreference field.
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
// their environment variable equivalents. To reference an environment variable in your json file, simply use values in this format:
|
||||
// "key"="$ENV_VAR_NAME"
|
||||
|
@@ -598,7 +598,7 @@ func checkLabelHasMultipleTTLs(records []*models.RecordConfig) (errs []error) {
|
||||
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 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
|
||||
|
@@ -6,19 +6,18 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const apiBase = "https://apis.cscglobal.com/dbs/api/v2"
|
||||
|
||||
// Api layer for CSC Global
|
||||
|
||||
type cscglobalProvider struct {
|
||||
key string
|
||||
token string
|
||||
notifyEmails []string
|
||||
}
|
||||
|
||||
type requestParams map[string]string
|
||||
|
||||
type errorResponse struct {
|
||||
@@ -55,8 +54,154 @@ type domainRecord struct {
|
||||
Nameserver []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
|
||||
var bodyString, err = c.get("/domains/" + domain)
|
||||
// Get zone
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -69,16 +214,16 @@ func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error {
|
||||
func (client *providerClient) updateNameservers(ns []string, domain string) error {
|
||||
req := nsModRequest{
|
||||
Domain: domain,
|
||||
NameServers: ns,
|
||||
DNSType: "OTHER_DNS",
|
||||
ShowPrice: false,
|
||||
}
|
||||
if c.notifyEmails != nil {
|
||||
if client.notifyEmails != nil {
|
||||
req.Notifications.Enabled = true
|
||||
req.Notifications.Emails = c.notifyEmails
|
||||
req.Notifications.Emails = client.notifyEmails
|
||||
}
|
||||
req.CustomFields = []string{}
|
||||
|
||||
@@ -87,7 +232,7 @@ func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error
|
||||
return err
|
||||
}
|
||||
|
||||
bodyString, err := c.put("/domains/nsmodification", requestBody)
|
||||
bodyString, err := client.put("/domains/nsmodification", requestBody)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
// domainsResult is the JSON returned by "/domains". Fields we don't
|
||||
// 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))
|
||||
|
||||
// Add headers
|
||||
req.Header.Add("apikey", c.key)
|
||||
req.Header.Add("Authorization", "Bearer "+c.token)
|
||||
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 := client.Do(req)
|
||||
resp, err := hclient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,16 +536,95 @@ func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, er
|
||||
req.Host, req.URL.RequestURI())
|
||||
}
|
||||
|
||||
func (c *cscglobalProvider) get(endpoint string) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", apiBase+endpoint, nil)
|
||||
func (client *providerClient) delete(endpoint string) ([]byte, error) {
|
||||
hclient := &http.Client{}
|
||||
fmt.Printf("DEBUG: delete endpoint: %q\n", apiBase+endpoint)
|
||||
req, _ := http.NewRequest("DELETE", apiBase+endpoint, nil)
|
||||
|
||||
// Add headers
|
||||
req.Header.Add("apikey", c.key)
|
||||
req.Header.Add("Authorization", "Bearer "+c.token)
|
||||
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)
|
||||
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")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := hclient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -2,10 +2,39 @@ package cscglobal
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/recordaudit"
|
||||
)
|
||||
|
||||
// AuditRecords returns an error if any records are not
|
||||
// supportable by this provider.
|
||||
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
|
||||
}
|
||||
|
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"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
|
||||
*/
|
||||
|
||||
func init() {
|
||||
providers.RegisterRegistrarType("CSCGLOBAL", newCscGlobal)
|
||||
type providerClient struct {
|
||||
key string
|
||||
token string
|
||||
notifyEmails []string
|
||||
}
|
||||
|
||||
func newCscGlobal(m map[string]string) (providers.Registrar, error) {
|
||||
api := &cscglobalProvider{}
|
||||
var features = providers.DocumentationNotes{
|
||||
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"]
|
||||
if api.key == "" || api.token == "" {
|
||||
@@ -38,35 +57,12 @@ func newCscGlobal(m map[string]string) (providers.Registrar, error) {
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// GetRegistrarCorrections gathers corrections that would being n to match dc.
|
||||
func (c *cscglobalProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
nss, err := c.getNameservers(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundNameservers := strings.Join(nss, ",")
|
||||
func init() {
|
||||
providers.RegisterRegistrarType("CSCGLOBAL", newReg)
|
||||
|
||||
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?")
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: newDsp,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
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 c.updateNameservers(expected, dc.Name)
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
providers.RegisterDomainServiceProviderType("CSCGLOBAL", fns, features)
|
||||
}
|
||||
|
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]
|
||||
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)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func CreateDNSProvider(providerTypeName string, config map[string]string, meta j
|
||||
|
||||
p, ok := DNSProviderTypes[providerTypeName]
|
||||
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)
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func beCompatible(n string, config map[string]string) (string, error) {
|
||||
func AuditRecords(dType string, rcs models.Records) error {
|
||||
p, ok := DNSProviderTypes[dType]
|
||||
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 {
|
||||
return fmt.Errorf("DNS service provider type %q has no RecordAuditor", dType)
|
||||
|
Reference in New Issue
Block a user