2022-11-16 01:40:08 +08:00
|
|
|
package gcore
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
2022-12-12 04:02:58 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
|
2022-11-16 01:40:08 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
|
|
|
|
|
|
|
dnssdk "github.com/G-Core/gcore-dns-sdk-go"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
G-Core API DNS provider:
|
|
|
|
Info required in `creds.json`:
|
|
|
|
- api-key
|
|
|
|
*/
|
|
|
|
|
|
|
|
type gcoreProvider struct {
|
|
|
|
provider *dnssdk.Client
|
|
|
|
ctx context.Context
|
2022-12-31 10:38:01 +08:00
|
|
|
apiKey string
|
2022-11-16 01:40:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewGCore creates the provider.
|
|
|
|
func NewGCore(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
|
|
if m["api-key"] == "" {
|
|
|
|
return nil, fmt.Errorf("missing G-Core API key")
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &gcoreProvider{
|
|
|
|
provider: dnssdk.NewClient(dnssdk.PermanentAPIKeyAuth(m["api-key"])),
|
|
|
|
ctx: context.TODO(),
|
2022-12-31 10:38:01 +08:00
|
|
|
apiKey: m["api-key"],
|
2022-11-16 01:40:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var features = providers.DocumentationNotes{
|
|
|
|
providers.CanAutoDNSSEC: providers.Cannot(),
|
|
|
|
providers.CanGetZones: providers.Can(),
|
|
|
|
providers.CanUseAlias: providers.Cannot(),
|
|
|
|
providers.CanUseCAA: providers.Can(),
|
|
|
|
providers.CanUseDS: providers.Cannot(),
|
|
|
|
providers.CanUseNAPTR: providers.Cannot(),
|
|
|
|
providers.CanUsePTR: providers.Cannot(),
|
|
|
|
providers.CanUseSRV: providers.Can("G-Core doesn't support SRV records with empty targets"),
|
|
|
|
providers.CanUseSSHFP: providers.Cannot(),
|
|
|
|
providers.CanUseTLSA: providers.Cannot(),
|
|
|
|
providers.DocCreateDomains: providers.Can(),
|
|
|
|
providers.DocDualHost: providers.Can(),
|
|
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultNameServerNames = []string{
|
|
|
|
"ns1.gcorelabs.net",
|
|
|
|
"ns2.gcdn.services",
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
fns := providers.DspFuncs{
|
|
|
|
Initializer: NewGCore,
|
|
|
|
RecordAuditor: AuditRecords,
|
|
|
|
}
|
|
|
|
providers.RegisterDomainServiceProviderType("GCORE", fns, features)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetNameservers returns the nameservers for a domain.
|
|
|
|
func (c *gcoreProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
|
|
return models.ToNameservers(defaultNameServerNames)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *gcoreProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
|
|
existing, err := c.GetZoneRecords(dc.Name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
models.PostProcessRecords(existing)
|
|
|
|
clean := PrepFoundRecords(existing)
|
|
|
|
PrepDesiredRecords(dc)
|
|
|
|
return c.GenerateDomainCorrections(dc, clean)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
|
|
func (c *gcoreProvider) GetZoneRecords(domain 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{}
|
|
|
|
|
2022-12-31 10:38:01 +08:00
|
|
|
// 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)
|
2022-11-16 01:40:08 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
existingRecords = append(existingRecords, nativeRecords...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return existingRecords, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// EnsureDomainExists returns an error if domain doesn't exist.
|
|
|
|
func (c *gcoreProvider) EnsureDomainExists(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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
dc.Punycode()
|
|
|
|
}
|
|
|
|
|
|
|
|
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) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
|
|
|
|
|
2022-12-31 10:38:01 +08:00
|
|
|
// Make delete happen earlier than creates & updates.
|
2022-12-12 04:02:58 +08:00
|
|
|
var corrections []*models.Correction
|
2022-12-31 10:38:01 +08:00
|
|
|
var deletions []*models.Correction
|
|
|
|
|
|
|
|
if !diff2.EnableDiff2 {
|
2022-11-16 01:40:08 +08:00
|
|
|
|
2022-12-12 04:02:58 +08:00
|
|
|
// diff existing vs. current.
|
|
|
|
differ := diff.New(dc)
|
|
|
|
keysToUpdate, err := differ.ChangedGroups(existing)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(keysToUpdate) == 0 {
|
|
|
|
return nil, nil
|
2022-11-16 01:40:08 +08:00
|
|
|
}
|
|
|
|
|
2022-12-12 04:02:58 +08:00
|
|
|
desiredRecords := dc.Records.GroupedByKey()
|
|
|
|
existingRecords := existing.GroupedByKey()
|
|
|
|
|
|
|
|
for label := range keysToUpdate {
|
|
|
|
if _, ok := desiredRecords[label]; !ok {
|
|
|
|
// record deleted in update
|
|
|
|
// Copy all params to avoid overwrites
|
|
|
|
zone := dc.Name
|
|
|
|
name := label.NameFQDN
|
|
|
|
typ := label.Type
|
|
|
|
msg := generateChangeMsg(keysToUpdate[label])
|
2022-12-31 10:38:01 +08:00
|
|
|
deletions = append(deletions, &models.Correction{
|
2022-12-12 04:02:58 +08:00
|
|
|
Msg: msg,
|
|
|
|
F: func() error {
|
|
|
|
return c.provider.DeleteRRSet(c.ctx, zone, name, typ)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
} else if _, ok := existingRecords[label]; !ok {
|
|
|
|
// record created in update
|
|
|
|
record := recordsToNative(desiredRecords[label], label)
|
|
|
|
if record == nil {
|
|
|
|
panic("No records matching label")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy all params to avoid overwrites
|
|
|
|
zone := dc.Name
|
|
|
|
name := label.NameFQDN
|
|
|
|
typ := label.Type
|
|
|
|
msg := generateChangeMsg(keysToUpdate[label])
|
|
|
|
corrections = append(corrections, &models.Correction{
|
|
|
|
Msg: msg,
|
|
|
|
F: func() error {
|
2022-12-31 10:38:01 +08:00
|
|
|
return c.provider.CreateRRSet(c.ctx, zone, name, typ, *record)
|
2022-12-12 04:02:58 +08:00
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// record modified in update
|
|
|
|
record := recordsToNative(desiredRecords[label], label)
|
|
|
|
if record == nil {
|
|
|
|
panic("No records matching label")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy all params to avoid overwrites
|
|
|
|
zone := dc.Name
|
|
|
|
name := label.NameFQDN
|
|
|
|
typ := label.Type
|
|
|
|
msg := generateChangeMsg(keysToUpdate[label])
|
|
|
|
corrections = append(corrections, &models.Correction{
|
|
|
|
Msg: msg,
|
|
|
|
F: func() error {
|
2022-12-31 10:38:01 +08:00
|
|
|
return c.provider.UpdateRRSet(c.ctx, zone, name, typ, *record)
|
2022-12-12 04:02:58 +08:00
|
|
|
},
|
|
|
|
})
|
2022-11-16 01:40:08 +08:00
|
|
|
}
|
|
|
|
}
|
2022-12-12 04:02:58 +08:00
|
|
|
|
2022-12-31 10:38:01 +08:00
|
|
|
} else {
|
|
|
|
// Diff2 version
|
|
|
|
changes, err := diff2.ByRecordSet(existing, dc, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-16 01:40:08 +08:00
|
|
|
|
2022-12-31 10:38:01 +08:00
|
|
|
for _, change := range changes {
|
|
|
|
record := recordsToNative(change.New, change.Key)
|
|
|
|
|
|
|
|
// 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.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)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-12 04:02:58 +08:00
|
|
|
|
2022-12-31 10:38:01 +08:00
|
|
|
return append(deletions, corrections...), nil
|
2022-11-16 01:40:08 +08:00
|
|
|
}
|