EXOSCALE: Migrate to v2 API (#1748)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Predrag Janosevic 2022-09-14 18:58:55 +00:00 committed by GitHub
parent 49590df8bf
commit 128e075066
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 85 deletions

2
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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",

View file

@ -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)
}

View file

@ -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.") ||