mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-06 13:14:25 +08:00
EXOSCALE: Migrate to v2 API (#1748)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
49590df8bf
commit
128e075066
5 changed files with 196 additions and 85 deletions
2
go.mod
2
go.mod
|
@ -25,7 +25,7 @@ require (
|
|||
github.com/digitalocean/godo v1.83.0
|
||||
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
|
||||
github.com/dnsimple/dnsimple-go v0.71.1
|
||||
github.com/exoscale/egoscale v0.89.0
|
||||
github.com/exoscale/egoscale v0.90.2
|
||||
github.com/go-acme/lego v2.7.2+incompatible
|
||||
github.com/go-gandi/go-gandi v0.5.0
|
||||
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe
|
||||
|
|
4
go.sum
4
go.sum
|
@ -198,8 +198,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/exoscale/egoscale v0.89.0 h1:YEA3pG97j14diC6okttXMOH9mLwlMuv/184uZyUGxhk=
|
||||
github.com/exoscale/egoscale v0.89.0/go.mod h1:wyXE5zrnFynMXA0jMhwQqSe24CfUhmBk2WI5wFZcq6Y=
|
||||
github.com/exoscale/egoscale v0.90.2 h1:oGSJy5Dxbcn5m5F0/DcnU4WXJg+2j3g+UgEu4yyKG9M=
|
||||
github.com/exoscale/egoscale v0.90.2/go.mod h1:NDhQbdGNKwnLVC2YGTB6ds9WIPw+V5ckvEEV8ho7pFE=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
|
|
|
@ -78,9 +78,10 @@
|
|||
},
|
||||
"EXOSCALE": {
|
||||
"apikey": "$EXOSCALE_API_KEY",
|
||||
"dns-endpoint": "https://api.exoscale.ch/dns",
|
||||
"dns-endpoint": "https://api.exoscale.com/v2",
|
||||
"domain": "$EXOSCALE_DOMAIN",
|
||||
"secretkey": "$EXOSCALE_SECRET_KEY"
|
||||
"secretkey": "$EXOSCALE_SECRET_KEY",
|
||||
"apizone": "ch-gva-2"
|
||||
},
|
||||
"GANDI_V5": {
|
||||
"apikey": "$GANDI_V5_APIKEY",
|
||||
|
|
|
@ -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.") ||
|
||||
|
|
Loading…
Add table
Reference in a new issue