dnscontrol/providers/gandi/livedns.go

284 lines
8.2 KiB
Go
Raw Normal View History

package gandi
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/pkg/printer"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/google/uuid"
"github.com/pkg/errors"
gandiclient "github.com/prasmussen/gandi-api/client"
gandilivedomain "github.com/prasmussen/gandi-api/live_dns/domain"
gandiliverecord "github.com/prasmussen/gandi-api/live_dns/record"
gandilivezone "github.com/prasmussen/gandi-api/live_dns/zone"
)
var liveFeatures = providers.DocumentationNotes{
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
providers.RegisterDomainServiceProviderType("GANDI-LIVEDNS", newLiveDsp, liveFeatures)
}
func newLiveDsp(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
APIKey := m["apikey"]
if APIKey == "" {
return nil, errors.Errorf("missing Gandi apikey")
}
return newLiveClient(APIKey), nil
}
type domainManager interface {
Info(string) (*gandilivedomain.Info, error)
Records(string) gandiliverecord.Manager
}
type zoneManager interface {
InfoByUUID(uuid.UUID) (*gandilivezone.Info, error)
Create(gandilivezone.Info) (*gandilivezone.CreateStatus, error)
Set(string, gandilivezone.Info) (*gandilivezone.Status, error)
Records(gandilivezone.Info) gandiliverecord.Manager
}
type liveClient struct {
client *gandiclient.Client
zoneManager zoneManager
domainManager domainManager
}
func newLiveClient(APIKey string) *liveClient {
cl := gandiclient.New(APIKey, gandiclient.LiveDNS)
return &liveClient{
client: cl,
zoneManager: gandilivezone.New(cl),
domainManager: gandilivedomain.New(cl),
}
}
// GetNameservers returns the list of gandi name servers for a given domain
func (c *liveClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
domains := []string{}
response, err := c.client.Get("/nameservers/"+domain, &domains)
if err != nil {
return nil, errors.Errorf("failed to get nameservers for domain %s", domain)
}
defer response.Body.Close()
ns := []*models.Nameserver{}
for _, domain := range domains {
ns = append(ns, &models.Nameserver{Name: domain})
}
return ns, nil
}
// GetDomainCorrections returns a list of corrections recommended for this domain.
func (c *liveClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
records, err := c.domainManager.Records(dc.Name).List()
if err != nil {
return nil, err
}
foundRecords := c.recordConfigFromInfo(records, dc.Name)
recordsToKeep, records, err := c.recordsToInfo(dc.Records)
if err != nil {
return nil, err
}
dc.Records = recordsToKeep
// Normalize
models.PostProcessRecords(foundRecords)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(foundRecords)
buf := &bytes.Buffer{}
// Print a list of changes. Generate an actual change that is the zone
changes := false
for _, i := range create {
changes = true
fmt.Fprintln(buf, i)
}
for _, i := range del {
changes = true
fmt.Fprintln(buf, i)
}
for _, i := range mod {
changes = true
fmt.Fprintln(buf, i)
}
if changes {
message := fmt.Sprintf("Setting dns records for %s:", dc.Name)
message += "\n" + buf.String()
return []*models.Correction{
{
Msg: message,
F: func() error {
return c.createZone(dc.Name, records)
},
},
}, nil
}
return []*models.Correction{}, nil
}
// createZone creates a new empty zone for the domain, populates it with the record infos and associates the domain to it
func (c *liveClient) createZone(domainname string, records []*gandiliverecord.Info) error {
domainInfo, err := c.domainManager.Info(domainname)
infos, err := c.zoneManager.InfoByUUID(*domainInfo.ZoneUUID)
if err != nil {
return err
}
infos.Name = fmt.Sprintf("zone created by dnscontrol for %s on %s", domainname, time.Now().Format(time.RFC3339))
printer.Debugf("DEBUG: createZone SharingID=%v\n", infos.SharingID)
// duplicate zone Infos
status, err := c.zoneManager.Create(*infos)
if err != nil {
return err
}
zoneInfos, err := c.zoneManager.InfoByUUID(*status.UUID)
if err != nil {
// gandi might take some time to make the new zone available
for i := 0; i < 10; i++ {
printer.Printf("zone info not yet available. Delay and retry: %s\n", err.Error())
time.Sleep(100 * time.Millisecond)
zoneInfos, err = c.zoneManager.InfoByUUID(*status.UUID)
if err == nil {
break
}
}
}
if err != nil {
return err
}
recordManager := c.zoneManager.Records(*zoneInfos)
for _, record := range records {
_, err := recordManager.Create(*record)
if err != nil {
return err
}
}
_, err = c.zoneManager.Set(domainname, *zoneInfos)
if err != nil {
return err
}
return nil
}
// recordConfigFromInfo takes a DNS record from Gandi liveDNS and returns our native RecordConfig format.
func (c *liveClient) recordConfigFromInfo(infos []*gandiliverecord.Info, origin string) []*models.RecordConfig {
rcs := []*models.RecordConfig{}
for _, info := range infos {
// TXT records might have multiple values. In that case,
// they are all for the TXT record at that label.
if info.Type == "TXT" {
rc := &models.RecordConfig{
Type: info.Type,
Original: info,
TTL: uint32(info.TTL),
}
rc.SetLabel(info.Name, origin)
var parsed []string
for _, txt := range info.Values {
parsed = append(parsed, models.StripQuotes(txt))
}
err := rc.SetTargetTXTs(parsed)
if err != nil {
panic(errors.Wrapf(err, "recordConfigFromInfo=TXT failed"))
}
rcs = append(rcs, rc)
} else {
// All other record types might have multiple values, but that means
// we should create one Recordconfig for each one.
for _, value := range info.Values {
rc := &models.RecordConfig{
Type: info.Type,
Original: info,
TTL: uint32(info.TTL),
}
rc.SetLabel(info.Name, origin)
switch rtype := info.Type; rtype {
default:
err := rc.PopulateFromString(rtype, value, origin)
if err != nil {
panic(errors.Wrapf(err, "recordConfigFromInfo failed"))
}
}
rcs = append(rcs, rc)
}
}
}
return rcs
}
// recordsToInfo generates gandi record sets and filters incompatible entries from native records format
func (c *liveClient) recordsToInfo(records models.Records) (models.Records, []*gandiliverecord.Info, error) {
recordSets := map[string]map[string]*gandiliverecord.Info{}
recordInfos := []*gandiliverecord.Info{}
recordToKeep := models.Records{}
for _, rec := range records {
if rec.TTL < 300 {
printer.Warnf("Gandi does not support ttls < 300. %s will not be set to %d.\n", rec.GetLabelFQDN(), rec.TTL)
rec.TTL = 300
}
if rec.TTL > 2592000 {
return nil, nil, errors.Errorf("ERROR: Gandi does not support TTLs > 30 days (TTL=%d)", rec.TTL)
}
if rec.Type == "NS" && rec.GetLabel() == "@" {
if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") {
printer.Warnf("Gandi does not support changing apex NS records. %s will not be added.\n", rec.GetTargetField())
}
continue
}
r, ok := recordSets[rec.GetLabel()][rec.Type]
if !ok {
_, ok := recordSets[rec.GetLabel()]
if !ok {
recordSets[rec.GetLabel()] = map[string]*gandiliverecord.Info{}
}
r = &gandiliverecord.Info{
Type: rec.Type,
Name: rec.GetLabel(),
TTL: int64(rec.TTL),
}
recordInfos = append(recordInfos, r)
recordSets[rec.GetLabel()][rec.Type] = r
} else {
if r.TTL != int64(rec.TTL) {
printer.Warnf(
"Gandi liveDNS API does not support different TTL for the couple fqdn/type. Will use TTL of %d for %s %s\n",
r.TTL,
r.Type,
r.Name,
)
}
}
recordToKeep = append(recordToKeep, rec)
if rec.Type == "TXT" {
for _, t := range rec.TxtStrings {
r.Values = append(r.Values, "\""+t+"\"") // FIXME(tlim): Should do proper quoting.
}
} else {
r.Values = append(r.Values, rec.GetTargetCombined())
}
}
return recordToKeep, recordInfos, nil
}