mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
EXOSCALE: Migrate to v2 API (#1748)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
committed by
GitHub
parent
49590df8bf
commit
128e075066
@@ -17,5 +17,7 @@ func AuditRecords(records []*models.RecordConfig) []error {
|
||||
|
||||
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
|
||||
|
||||
a.Add("TXT", rejectif.TxtHasUnpairedDoubleQuotes) // Last verified 2022-09-14
|
||||
|
||||
return a.Audit(records)
|
||||
}
|
||||
|
@@ -3,25 +3,54 @@ package exoscale
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
egoscale "github.com/exoscale/egoscale/v2"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
"github.com/exoscale/egoscale"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAPIZone = "ch-gva-2"
|
||||
)
|
||||
|
||||
// ErrDomainNotFound error indicates domain name is not managed by Exoscale.
|
||||
var ErrDomainNotFound = errors.New("domain not found")
|
||||
|
||||
type exoscaleProvider struct {
|
||||
client *egoscale.Client
|
||||
client *egoscale.Client
|
||||
apiZone string
|
||||
}
|
||||
|
||||
// NewExoscale creates a new Exoscale DNS provider.
|
||||
func NewExoscale(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
endpoint, apiKey, secretKey := m["dns-endpoint"], m["apikey"], m["secretkey"]
|
||||
|
||||
return &exoscaleProvider{client: egoscale.NewClient(endpoint, apiKey, secretKey)}, nil
|
||||
client, err := egoscale.NewClient(
|
||||
apiKey,
|
||||
secretKey,
|
||||
egoscale.ClientOptWithAPIEndpoint(endpoint),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := exoscaleProvider{
|
||||
client: client,
|
||||
apiZone: defaultAPIZone,
|
||||
}
|
||||
|
||||
if z, ok := m["apizone"]; ok {
|
||||
provider.apiZone = z
|
||||
}
|
||||
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
@@ -45,15 +74,9 @@ func init() {
|
||||
}
|
||||
|
||||
// EnsureDomainExists returns an error if domain doesn't exist.
|
||||
func (c *exoscaleProvider) EnsureDomainExists(domain string) error {
|
||||
ctx := context.Background()
|
||||
_, err := c.client.GetDomain(ctx, domain)
|
||||
if err != nil {
|
||||
_, err := c.client.CreateDomain(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
func (c *exoscaleProvider) EnsureDomainExists(domainName string) error {
|
||||
_, err := c.findDomainByName(domainName)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,47 +97,92 @@ func (c *exoscaleProvider) GetZoneRecords(domain string) (models.Records, error)
|
||||
func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
|
||||
domain, err := c.findDomainByName(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainID := *domain.ID
|
||||
|
||||
ctx := context.Background()
|
||||
records, err := c.client.GetRecords(ctx, dc.Name)
|
||||
records, err := c.client.ListDNSDomainRecords(ctx, c.apiZone, domainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingRecords := make([]*models.RecordConfig, 0, len(records))
|
||||
for _, r := range records {
|
||||
if r.RecordType == "SOA" || r.RecordType == "NS" {
|
||||
if r.ID == nil {
|
||||
continue
|
||||
}
|
||||
if r.Name == "" {
|
||||
r.Name = "@"
|
||||
|
||||
recordID := *r.ID
|
||||
|
||||
record, err := c.client.GetDNSDomainRecord(ctx, c.apiZone, domainID, recordID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.RecordType == "CNAME" || r.RecordType == "MX" || r.RecordType == "ALIAS" || r.RecordType == "SRV" {
|
||||
r.Content += "."
|
||||
|
||||
// nil pointers are not expected, but just to be on the safe side...
|
||||
var rtype, rcontent, rname string
|
||||
if record.Type == nil {
|
||||
continue
|
||||
}
|
||||
rtype = *record.Type
|
||||
if record.Content != nil {
|
||||
rcontent = *record.Content
|
||||
}
|
||||
if record.Name != nil {
|
||||
rname = *record.Name
|
||||
}
|
||||
|
||||
if rtype == "SOA" || rtype == "NS" {
|
||||
continue
|
||||
}
|
||||
if rname == "" {
|
||||
t := "@"
|
||||
record.Name = &t
|
||||
}
|
||||
if rtype == "CNAME" || rtype == "MX" || rtype == "ALIAS" || rtype == "SRV" {
|
||||
t := rcontent + "."
|
||||
// for SRV records we need to aditionally prefix target with priority, which API handles as separate field.
|
||||
if rtype == "SRV" && record.Priority != nil {
|
||||
t = fmt.Sprintf("%d %s", *record.Priority, t)
|
||||
}
|
||||
rcontent = t
|
||||
}
|
||||
// exoscale adds these odd txt records that mirror the alias records.
|
||||
// they seem to manage them on deletes and things, so we'll just pretend they don't exist
|
||||
if r.RecordType == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") {
|
||||
if rtype == "TXT" && strings.HasPrefix(rcontent, "ALIAS for ") {
|
||||
continue
|
||||
}
|
||||
rec := &models.RecordConfig{
|
||||
TTL: uint32(r.TTL),
|
||||
Original: r,
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
Original: record,
|
||||
}
|
||||
rec.SetLabel(r.Name, dc.Name)
|
||||
switch rtype := r.RecordType; rtype {
|
||||
if record.TTL != nil {
|
||||
rc.TTL = uint32(*record.TTL)
|
||||
}
|
||||
rc.SetLabel(rname, dc.Name)
|
||||
|
||||
switch rtype {
|
||||
case "ALIAS", "URL":
|
||||
rec.Type = r.RecordType
|
||||
rec.SetTarget(r.Content)
|
||||
rc.Type = rtype
|
||||
rc.SetTarget(rcontent)
|
||||
case "MX":
|
||||
if err := rec.SetTargetMX(uint16(r.Prio), r.Content); err != nil {
|
||||
return nil, fmt.Errorf("unparsable record received from exoscale: %w", err)
|
||||
var prio uint16
|
||||
if record.Priority != nil {
|
||||
prio = uint16(*record.Priority)
|
||||
}
|
||||
err = rc.SetTargetMX(prio, rcontent)
|
||||
default:
|
||||
if err := rec.PopulateFromString(r.RecordType, r.Content, dc.Name); err != nil {
|
||||
return nil, fmt.Errorf("unparsable record received from exoscale: %w", err)
|
||||
}
|
||||
err = rc.PopulateFromString(rtype, rcontent, dc.Name)
|
||||
}
|
||||
existingRecords = append(existingRecords, rec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unparsable record received from exoscale: %w", err)
|
||||
}
|
||||
|
||||
existingRecords = append(existingRecords, rc)
|
||||
}
|
||||
removeOtherNS(dc)
|
||||
|
||||
@@ -130,27 +198,27 @@ func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
|
||||
var corrections = []*models.Correction{}
|
||||
|
||||
for _, del := range delete {
|
||||
rec := del.Existing.Original.(egoscale.DNSRecord)
|
||||
record := del.Existing.Original.(*egoscale.DNSDomainRecord)
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: del.String(),
|
||||
F: c.deleteRecordFunc(rec.ID, dc.Name),
|
||||
F: c.deleteRecordFunc(*record.ID, domainID),
|
||||
})
|
||||
}
|
||||
|
||||
for _, cre := range create {
|
||||
rec := cre.Desired
|
||||
rc := cre.Desired
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: cre.String(),
|
||||
F: c.createRecordFunc(rec, dc.Name),
|
||||
F: c.createRecordFunc(rc, domainID),
|
||||
})
|
||||
}
|
||||
|
||||
for _, mod := range modify {
|
||||
old := mod.Existing.Original.(egoscale.DNSRecord)
|
||||
old := mod.Existing.Original.(*egoscale.DNSDomainRecord)
|
||||
new := mod.Desired
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: mod.String(),
|
||||
F: c.updateRecordFunc(&old, new, dc.Name),
|
||||
F: c.updateRecordFunc(old, new, domainID),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,88 +226,128 @@ func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to create a record in a zone.
|
||||
func (c *exoscaleProvider) createRecordFunc(rc *models.RecordConfig, domainName string) func() error {
|
||||
func (c *exoscaleProvider) createRecordFunc(rc *models.RecordConfig, domainID string) func() error {
|
||||
return func() error {
|
||||
client := c.client
|
||||
|
||||
target := rc.GetTargetCombined()
|
||||
name := rc.GetLabel()
|
||||
var prio *int64
|
||||
|
||||
if rc.Type == "MX" {
|
||||
target = rc.GetTargetField()
|
||||
|
||||
if rc.MxPreference != 0 {
|
||||
p := int64(rc.MxPreference)
|
||||
prio = &p
|
||||
}
|
||||
}
|
||||
|
||||
if rc.Type == "SRV" {
|
||||
// API wants priority as a separate argument, here we will strip it from combined target.
|
||||
sp := strings.Split(target, " ")
|
||||
target = strings.Join(sp[1:], " ")
|
||||
p, err := strconv.ParseInt(sp[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prio = &p
|
||||
}
|
||||
|
||||
if rc.Type == "NS" && (name == "@" || name == "") {
|
||||
name = "*"
|
||||
}
|
||||
|
||||
record := egoscale.DNSRecord{
|
||||
Name: name,
|
||||
RecordType: rc.Type,
|
||||
Content: target,
|
||||
TTL: int(rc.TTL),
|
||||
Prio: int(rc.MxPreference),
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err := client.CreateRecord(ctx, domainName, record)
|
||||
if err != nil {
|
||||
return err
|
||||
record := egoscale.DNSDomainRecord{
|
||||
Name: &name,
|
||||
Type: &rc.Type,
|
||||
Content: &target,
|
||||
Priority: prio,
|
||||
}
|
||||
|
||||
return nil
|
||||
if rc.TTL != 0 {
|
||||
ttl := int64(rc.TTL)
|
||||
record.TTL = &ttl
|
||||
}
|
||||
|
||||
_, err := c.client.CreateDNSDomainRecord(context.Background(), c.apiZone, domainID, &record)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to delete a record in a zone.
|
||||
func (c *exoscaleProvider) deleteRecordFunc(recordID int64, domainName string) func() error {
|
||||
func (c *exoscaleProvider) deleteRecordFunc(recordID, domainID string) func() error {
|
||||
return func() error {
|
||||
client := c.client
|
||||
|
||||
ctx := context.Background()
|
||||
if err := client.DeleteRecord(ctx, domainName, recordID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
return c.client.DeleteDNSDomainRecord(
|
||||
context.Background(),
|
||||
c.apiZone,
|
||||
domainID,
|
||||
&egoscale.DNSDomainRecord{ID: &recordID},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to update a record in a zone.
|
||||
func (c *exoscaleProvider) updateRecordFunc(old *egoscale.DNSRecord, rc *models.RecordConfig, domainName string) func() error {
|
||||
func (c *exoscaleProvider) updateRecordFunc(record *egoscale.DNSDomainRecord, rc *models.RecordConfig, domainID string) func() error {
|
||||
return func() error {
|
||||
client := c.client
|
||||
|
||||
target := rc.GetTargetCombined()
|
||||
name := rc.GetLabel()
|
||||
|
||||
if rc.Type == "MX" {
|
||||
target = rc.GetTargetField()
|
||||
|
||||
if rc.MxPreference != 0 {
|
||||
p := int64(rc.MxPreference)
|
||||
record.Priority = &p
|
||||
}
|
||||
}
|
||||
|
||||
if rc.Type == "SRV" {
|
||||
// API wants priority as separate argument, here we will strip it from combined target.
|
||||
sp := strings.Split(target, " ")
|
||||
target = strings.Join(sp[1:], " ")
|
||||
p, err := strconv.ParseInt(sp[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Priority = &p
|
||||
}
|
||||
|
||||
if rc.Type == "NS" && (name == "@" || name == "") {
|
||||
name = "*"
|
||||
}
|
||||
|
||||
record := egoscale.UpdateDNSRecord{
|
||||
Name: name,
|
||||
RecordType: rc.Type,
|
||||
Content: target,
|
||||
TTL: int(rc.TTL),
|
||||
Prio: int(rc.MxPreference),
|
||||
ID: old.ID,
|
||||
record.Name = &name
|
||||
record.Type = &rc.Type
|
||||
record.Content = &target
|
||||
if rc.TTL != 0 {
|
||||
ttl := int64(rc.TTL)
|
||||
record.TTL = &ttl
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.UpdateRecord(ctx, domainName, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.client.UpdateDNSDomainRecord(
|
||||
context.Background(),
|
||||
c.apiZone,
|
||||
domainID,
|
||||
record,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *exoscaleProvider) findDomainByName(name string) (*egoscale.DNSDomain, error) {
|
||||
domains, err := c.client.ListDNSDomains(context.Background(), c.apiZone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
if domain.UnicodeName != nil && domain.ID != nil && *domain.UnicodeName == name {
|
||||
return &domain, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrDomainNotFound
|
||||
}
|
||||
|
||||
func defaultNSSUffix(defNS string) bool {
|
||||
return (strings.HasSuffix(defNS, ".exoscale.io.") ||
|
||||
strings.HasSuffix(defNS, ".exoscale.com.") ||
|
||||
|
Reference in New Issue
Block a user