dnscontrol/providers/exoscale/exoscaleProvider.go
Tom Limoncelli 87ad01d194
Add "get-zone" command (#613)
* Add GetZoneRecords to DNSProvider interface
* dnscontrol now uses ufave/cli/v2
* NEW: get-zones.md
* HasRecordTypeName should be a method on models.Records not models.DomainConfig
* Implement BIND's GetZoneRecords
* new WriteZoneFile implemented
* go mod vendor
* Update docs to use get-zone instead of convertzone
* Add CanGetZone capability and update all providers.
* Get all zones for a provider at once (#626)
* implement GetZoneRecords for cloudflare
* munge cloudflare ttls
* Implement GetZoneRecords for cloudflare (#625)

Co-authored-by: Craig Peterson <192540+captncraig@users.noreply.github.com>
2020-02-18 08:59:18 -05:00

253 lines
7 KiB
Go

package exoscale
import (
"encoding/json"
"fmt"
"strings"
"github.com/exoscale/egoscale"
"github.com/StackExchange/dnscontrol/v2/models"
"github.com/StackExchange/dnscontrol/v2/providers"
"github.com/StackExchange/dnscontrol/v2/providers/diff"
)
type exoscaleProvider struct {
client *egoscale.Client
}
// 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
}
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot("Exoscale does not allow sufficient control over the apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Unimplemented(),
}
func init() {
providers.RegisterDomainServiceProviderType("EXOSCALE", NewExoscale, features)
}
// EnsureDomainExists returns an error if domain doesn't exist.
func (c *exoscaleProvider) EnsureDomainExists(domain string) error {
_, err := c.client.GetDomain(domain)
if err != nil {
_, err := c.client.CreateDomain(domain)
if err != nil {
return err
}
}
return err
}
// GetNameservers returns the nameservers for domain.
func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
return nil, nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (client *exoscaleProvider) GetZoneRecords(domain string) (models.Records, error) {
return nil, fmt.Errorf("not implemented")
// This enables the get-zones subcommand.
// Implement this by extracting the code from GetDomainCorrections into
// a single function. For most providers this should be relatively easy.
}
// GetDomainCorrections returns a list of corretions for the domain.
func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
records, err := c.client.GetRecords(dc.Name)
if err != nil {
return nil, err
}
existingRecords := make([]*models.RecordConfig, 0, len(records))
for _, r := range records {
if r.RecordType == "SOA" || r.RecordType == "NS" {
continue
}
if r.Name == "" {
r.Name = "@"
}
if r.RecordType == "CNAME" || r.RecordType == "MX" || r.RecordType == "ALIAS" || r.RecordType == "SRV" {
r.Content += "."
}
// 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 ") {
continue
}
rec := &models.RecordConfig{
TTL: uint32(r.TTL),
Original: r,
}
rec.SetLabel(r.Name, dc.Name)
switch rtype := r.RecordType; rtype {
case "ALIAS", "URL":
rec.Type = r.RecordType
rec.SetTarget(r.Content)
case "MX":
if err := rec.SetTargetMX(uint16(r.Prio), r.Content); err != nil {
panic(fmt.Errorf("unparsable record received from dnsimple: %w", err))
}
default:
if err := rec.PopulateFromString(r.RecordType, r.Content, dc.Name); err != nil {
panic(fmt.Errorf("unparsable record received from dnsimple: %w", err))
}
}
existingRecords = append(existingRecords, rec)
}
removeOtherNS(dc)
// Normalize
models.PostProcessRecords(existingRecords)
differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
var corrections = []*models.Correction{}
for _, del := range delete {
rec := del.Existing.Original.(egoscale.DNSRecord)
corrections = append(corrections, &models.Correction{
Msg: del.String(),
F: c.deleteRecordFunc(rec.ID, dc.Name),
})
}
for _, cre := range create {
rec := cre.Desired
corrections = append(corrections, &models.Correction{
Msg: cre.String(),
F: c.createRecordFunc(rec, dc.Name),
})
}
for _, mod := range modify {
old := mod.Existing.Original.(egoscale.DNSRecord)
new := mod.Desired
corrections = append(corrections, &models.Correction{
Msg: mod.String(),
F: c.updateRecordFunc(&old, new, dc.Name),
})
}
return corrections, nil
}
// 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 {
return func() error {
client := c.client
target := rc.GetTargetCombined()
name := rc.GetLabel()
if rc.Type == "MX" {
target = rc.Target
}
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),
}
_, err := client.CreateRecord(domainName, record)
if err != nil {
return err
}
return nil
}
}
// Returns a function that can be invoked to delete a record in a zone.
func (c *exoscaleProvider) deleteRecordFunc(recordID int64, domainName string) func() error {
return func() error {
client := c.client
if err := client.DeleteRecord(domainName, recordID); err != nil {
return err
}
return nil
}
}
// 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 {
return func() error {
client := c.client
target := rc.GetTargetCombined()
name := rc.GetLabel()
if rc.Type == "MX" {
target = rc.Target
}
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,
}
_, err := client.UpdateRecord(domainName, record)
if err != nil {
return err
}
return nil
}
}
func defaultNSSUffix(defNS string) bool {
return (strings.HasSuffix(defNS, ".exoscale.io.") ||
strings.HasSuffix(defNS, ".exoscale.com.") ||
strings.HasSuffix(defNS, ".exoscale.ch.") ||
strings.HasSuffix(defNS, ".exoscale.net."))
}
// remove all non-exoscale NS records from our desired state.
// if any are found, print a warning
func removeOtherNS(dc *models.DomainConfig) {
newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.Type == "NS" {
// apex NS inside exoscale are expected.
if rec.GetLabelFQDN() == dc.Name && defaultNSSUffix(rec.GetTargetField()) {
continue
}
fmt.Printf("Warning: exoscale.com(.io, .ch, .net) does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField())
continue
}
newList = append(newList, rec)
}
dc.Records = newList
}