dnscontrol/providers/gcore/gcoreProvider.go
Tom Limoncelli 4529bc8f4f
rebase to main (#3442)
Co-authored-by: nyanotech <33802077+nyanotech@users.noreply.github.com>
Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
Co-authored-by: Brice Figureau <brice@figureau.eu>
Co-authored-by: Jeffrey Cafferata <jeffrey@jcid.nl>
Co-authored-by: imlonghao <git@imlonghao.com>
2025-02-18 11:22:00 -05:00

244 lines
6.8 KiB
Go

package gcore
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
dnssdk "github.com/G-Core/gcore-dns-sdk-go"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
)
/*
G-Core API DNS provider:
Info required in `creds.json`:
- api-key
*/
type gcoreProvider struct {
provider *dnssdk.Client
ctx context.Context
apiKey string
}
// NewGCore creates the provider.
func NewGCore(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if m["api-key"] == "" {
return nil, errors.New("missing G-Core API key")
}
c := &gcoreProvider{
provider: dnssdk.NewClient(dnssdk.PermanentAPIKeyAuth(m["api-key"])),
ctx: context.TODO(),
apiKey: m["api-key"],
}
return c, nil
}
var features = providers.DocumentationNotes{
// The default for unlisted capabilities is 'Cannot'.
// See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUsePTR: providers.Can("G-Core supports PTR records only in rDNS zones"),
providers.CanUseSRV: providers.Can("G-Core doesn't support SRV records with empty targets"),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.CanUseHTTPS: providers.Can(),
providers.CanUseSVCB: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
}
var defaultNameServerNames = []string{
"ns1.gcorelabs.net",
"ns2.gcdn.services",
}
func init() {
const providerName = "GCORE"
const providerMaintainer = "@xddxdd"
fns := providers.DspFuncs{
Initializer: NewGCore,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType(providerName, fns, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
func (c *gcoreProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
return models.ToNameservers(defaultNameServerNames)
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (c *gcoreProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
zone, err := c.provider.Zone(c.ctx, domain)
if err != nil {
return nil, err
}
// Convert RRsets to DNSControl format on the fly
existingRecords := []*models.RecordConfig{}
// We cannot directly use Zone's ShortAnswers, they aren't complete for CAA & SRV
rrsets, err := c.dnssdkRRSets(domain)
if err != nil {
return nil, err
}
for _, rec := range rrsets.RRSets {
nativeRecords, err := nativeToRecords(rec, zone.Name)
if err != nil {
return nil, err
}
existingRecords = append(existingRecords, nativeRecords...)
}
return existingRecords, nil
}
// EnsureZoneExists creates a zone if it does not exist
func (c *gcoreProvider) EnsureZoneExists(domain string) error {
zones, err := c.provider.Zones(c.ctx)
if err != nil {
return err
}
for _, zone := range zones {
if zone.Name == domain {
return nil
}
}
_, err = c.provider.CreateZone(c.ctx, domain)
return err
}
func generateChangeMsg(updates []string) string {
return strings.Join(updates, "\n")
}
// GenerateDomainCorrections takes the desired and existing records
// and produces a Correction list. The correction list is simply
// a list of functions to call to actually make the desired
// correction, and a message to output to the user when the change is
// made.
func (c *gcoreProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
// Make delete happen earlier than creates & updates.
var corrections []*models.Correction
var deletions []*models.Correction
var reports []*models.Correction
// Gcore auto uses ALIAS for apex zone CNAME records, just like CloudFlare
for _, rec := range dc.Records {
if rec.Type == "ALIAS" {
rec.ChangeType("CNAME", dc.Name)
}
}
changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, comparableFunc)
if err != nil {
return nil, 0, err
}
for _, change := range changes {
record, err := recordsToNative(change.New, change.Key)
if err != nil {
return nil, 0, err
}
// Copy all params to avoid overwrites
zone := dc.Name
name := change.Key.NameFQDN
typ := change.Key.Type
msg := generateChangeMsg(change.Msgs)
switch change.Type {
case diff2.REPORT:
corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
case diff2.CREATE:
corrections = append(corrections, &models.Correction{
Msg: msg,
F: func() error {
return c.provider.CreateRRSet(c.ctx, zone, name, typ, *record)
},
})
case diff2.CHANGE:
corrections = append(corrections, &models.Correction{
Msg: msg,
F: func() error {
return c.provider.UpdateRRSet(c.ctx, zone, name, typ, *record)
},
})
case diff2.DELETE:
deletions = append(deletions, &models.Correction{
Msg: msg,
F: func() error {
return c.provider.DeleteRRSet(c.ctx, zone, name, typ)
},
})
default:
panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
}
}
dnssecEnabled, err := c.dnssdkGetDNSSEC(dc.Name)
if err != nil {
return nil, 0, err
}
if !dnssecEnabled && dc.AutoDNSSEC == "on" {
// Copy all params to avoid overwrites
zone := dc.Name
corrections = append(corrections, &models.Correction{
Msg: "Enable DNSSEC",
F: func() error {
return c.dnssdkSetDNSSEC(zone, true)
},
})
} else if dnssecEnabled && dc.AutoDNSSEC == "off" {
// Copy all params to avoid overwrites
zone := dc.Name
corrections = append(corrections, &models.Correction{
Msg: "Disable DNSSEC",
F: func() error {
return c.dnssdkSetDNSSEC(zone, false)
},
})
}
result := append(reports, deletions...)
result = append(result, corrections...)
return result, actualChangeCount, nil
}
func comparableFunc(rec *models.RecordConfig) string {
if len(rec.Metadata) == 0 {
return ""
}
// json.Marshal always serialize all fields in alphabetical order,
// so the metadata string will be consistent between runs
result, err := json.Marshal(rec.Metadata)
if err != nil {
printer.Warnf("Cannot serialize metadata of record %s", rec)
return ""
}
return string(result)
}