mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 18:08:57 +08:00
255 lines
8.1 KiB
Go
255 lines
8.1 KiB
Go
package akamaiedgedns
|
|
|
|
/*
|
|
Akamai Edge DNS provider
|
|
|
|
For information about Akamai Edge DNS, see:
|
|
https://www.akamai.com/us/en/products/security/edge-dns.jsp
|
|
https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html
|
|
https://www.akamai.com/us/en/multimedia/documents/product-brief/edge-dns-product-brief.pdf
|
|
*/
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
|
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
|
)
|
|
|
|
var features = providers.DocumentationNotes{
|
|
// The default for unlisted capabilities is 'Cannot'.
|
|
// See providers/capabilities.go for the entire list of capabilties.
|
|
providers.CanAutoDNSSEC: providers.Can(),
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanUseAKAMAICDN: providers.Can(),
|
|
providers.CanUseAlias: providers.Cannot(),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUseDS: providers.Cannot(),
|
|
providers.CanUseDSForChildren: providers.Can(),
|
|
providers.CanUseNAPTR: providers.Can(),
|
|
providers.CanUsePTR: providers.Can(),
|
|
providers.CanUseSOA: providers.Cannot(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CanUseSSHFP: providers.Can(),
|
|
providers.CanUseTLSA: providers.Can(),
|
|
providers.CantUseNOPURGE: providers.Cannot(),
|
|
providers.DocCreateDomains: providers.Can(),
|
|
providers.DocDualHost: providers.Can(),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
type edgeDNSProvider struct {
|
|
contractID string
|
|
groupID string
|
|
}
|
|
|
|
func init() {
|
|
fns := providers.DspFuncs{
|
|
Initializer: newEdgeDNSDSP,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType("AKAMAIEDGEDNS", fns, features)
|
|
providers.RegisterCustomRecordType("AKAMAICDN", "AKAMAIEDGEDNS", "")
|
|
}
|
|
|
|
// DnsServiceProvider
|
|
func newEdgeDNSDSP(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
clientSecret := config["client_secret"]
|
|
host := config["host"]
|
|
accessToken := config["access_token"]
|
|
clientToken := config["client_token"]
|
|
contractID := config["contract_id"]
|
|
groupID := config["group_id"]
|
|
|
|
if clientSecret == "" {
|
|
return nil, fmt.Errorf("creds.json: client_secret must not be empty")
|
|
}
|
|
if host == "" {
|
|
return nil, fmt.Errorf("creds.json: host must not be empty")
|
|
}
|
|
if accessToken == "" {
|
|
return nil, fmt.Errorf("creds.json: accessToken must not be empty")
|
|
}
|
|
if clientToken == "" {
|
|
return nil, fmt.Errorf("creds.json: clientToken must not be empty")
|
|
}
|
|
if contractID == "" {
|
|
return nil, fmt.Errorf("creds.json: contractID must not be empty")
|
|
}
|
|
if groupID == "" {
|
|
return nil, fmt.Errorf("creds.json: groupID must not be empty")
|
|
}
|
|
|
|
initialize(clientSecret, host, accessToken, clientToken)
|
|
|
|
api := &edgeDNSProvider{
|
|
contractID: contractID,
|
|
groupID: groupID,
|
|
}
|
|
return api, nil
|
|
}
|
|
|
|
// EnsureDomainExists configures a new zone if the zone does not already exist.
|
|
func (a *edgeDNSProvider) EnsureDomainExists(domain string) error {
|
|
if zoneDoesExist(domain) {
|
|
printer.Debugf("Zone %s already exists\n", domain)
|
|
return nil
|
|
}
|
|
return createZone(domain, a.contractID, a.groupID)
|
|
}
|
|
|
|
// GetDomainCorrections return a list of corrections. Each correction is a text string describing the change
|
|
// and a function that, if called, will make the change.
|
|
// “dnscontrol preview” simply prints the text strings.
|
|
// "dnscontrol push" prints the strings and calls the functions.
|
|
func (a *edgeDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
err := dc.Punycode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingRecords, err := getRecords(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
models.PostProcessRecords(existingRecords)
|
|
txtutil.SplitSingleLongTxt(dc.Records)
|
|
|
|
var corrections []*models.Correction
|
|
var keysToUpdate map[models.RecordKey][]string
|
|
if !diff2.EnableDiff2 {
|
|
keysToUpdate, err = (diff.New(dc)).ChangedGroups(existingRecords)
|
|
} else {
|
|
keysToUpdate, err = (diff.NewCompat(dc)).ChangedGroups(existingRecords)
|
|
// TODO(tlim): In the future please adopt `pkg/diff2/By*()`
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingRecordsMap := make(map[models.RecordKey][]*models.RecordConfig)
|
|
for _, r := range existingRecords {
|
|
key := models.RecordKey{NameFQDN: r.NameFQDN, Type: r.Type}
|
|
existingRecordsMap[key] = append(existingRecordsMap[key], r)
|
|
}
|
|
|
|
desiredRecordsMap := dc.Records.GroupedByKey()
|
|
|
|
// Deletes must occur first. For example, if replacing a existing CNAME with an A of the same name:
|
|
// DELETE CNAME foo.example.net
|
|
// must occur before
|
|
// CREATE A foo.example.net
|
|
// because both an A and a CNAME for the same name is not allowed.
|
|
|
|
lastCorrections := []*models.Correction{} // creates and replaces last
|
|
|
|
for key, msg := range keysToUpdate {
|
|
existing, okExisting := existingRecordsMap[key]
|
|
desired, okDesired := desiredRecordsMap[key]
|
|
|
|
if okExisting && !okDesired {
|
|
// In the existing map but not in the desired map: Delete
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: strings.Join(msg, "\n "),
|
|
F: func() error {
|
|
return deleteRecordset(existing, dc.Name)
|
|
},
|
|
})
|
|
printer.Debugf("deleteRecordset: %s %s\n", key.NameFQDN, key.Type)
|
|
for _, rdata := range existing {
|
|
printer.Debugf(" Rdata: %s\n", rdata.GetTargetCombined())
|
|
}
|
|
} else if !okExisting && okDesired {
|
|
// Not in the existing map but in the desired map: Create
|
|
lastCorrections = append(lastCorrections, &models.Correction{
|
|
Msg: strings.Join(msg, "\n "),
|
|
F: func() error {
|
|
return createRecordset(desired, dc.Name)
|
|
},
|
|
})
|
|
printer.Debugf("createRecordset: %s %s\n", key.NameFQDN, key.Type)
|
|
for _, rdata := range desired {
|
|
printer.Debugf(" Rdata: %s\n", rdata.GetTargetCombined())
|
|
}
|
|
} else if okExisting && okDesired {
|
|
// In the existing map and in the desired map: Replace
|
|
lastCorrections = append(lastCorrections, &models.Correction{
|
|
Msg: strings.Join(msg, "\n "),
|
|
F: func() error {
|
|
return replaceRecordset(desired, dc.Name)
|
|
},
|
|
})
|
|
printer.Debugf("replaceRecordset: %s %s\n", key.NameFQDN, key.Type)
|
|
for _, rdata := range desired {
|
|
printer.Debugf(" Rdata: %s\n", rdata.GetTargetCombined())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deletes first, then creates and replaces
|
|
corrections = append(corrections, lastCorrections...)
|
|
|
|
// AutoDnsSec correction
|
|
existingAutoDNSSecEnabled, err := isAutoDNSSecEnabled(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
desiredAutoDNSSecEnabled := dc.AutoDNSSEC == "on"
|
|
|
|
if !existingAutoDNSSecEnabled && desiredAutoDNSSecEnabled {
|
|
// Existing false (disabled), Desired true (enabled)
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: "Enable AutoDnsSec\n",
|
|
F: func() error {
|
|
return autoDNSSecEnable(true, dc.Name)
|
|
},
|
|
})
|
|
printer.Debugf("autoDNSSecEnable: Enable AutoDnsSec for zone %s\n", dc.Name)
|
|
} else if existingAutoDNSSecEnabled && !desiredAutoDNSSecEnabled {
|
|
// Existing true (enabled), Desired false (disabled)
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: "Disable AutoDnsSec\n",
|
|
F: func() error {
|
|
return autoDNSSecEnable(false, dc.Name)
|
|
},
|
|
})
|
|
printer.Debugf("autoDNSSecEnable: Disable AutoDnsSec for zone %s\n", dc.Name)
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
// GetNameservers returns the nameservers for a domain.
|
|
func (a *edgeDNSProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
authorities, err := getAuthorities(a.contractID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return models.ToNameserversStripTD(authorities)
|
|
}
|
|
|
|
// GetZoneRecords returns an array of RecordConfig structs for a zone.
|
|
func (a *edgeDNSProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|
records, err := getRecords(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
// ListZones returns all DNS zones managed by this provider.
|
|
func (a *edgeDNSProvider) ListZones() ([]string, error) {
|
|
zones, err := listZones(a.contractID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return zones, nil
|
|
}
|