github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/exoscale/exoscaleProvider.go (about)

     1  package exoscale
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/exoscale/egoscale"
    10  
    11  	"github.com/StackExchange/dnscontrol/v2/models"
    12  	"github.com/StackExchange/dnscontrol/v2/providers"
    13  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    14  )
    15  
    16  type exoscaleProvider struct {
    17  	client *egoscale.Client
    18  }
    19  
    20  // NewExoscale creates a new Exoscale DNS provider.
    21  func NewExoscale(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    22  	endpoint, apiKey, secretKey := m["dns-endpoint"], m["apikey"], m["secretkey"]
    23  
    24  	return &exoscaleProvider{client: egoscale.NewClient(endpoint, apiKey, secretKey)}, nil
    25  }
    26  
    27  var features = providers.DocumentationNotes{
    28  	providers.CanUseAlias:            providers.Can(),
    29  	providers.CanUseCAA:              providers.Can(),
    30  	providers.CanUsePTR:              providers.Can(),
    31  	providers.CanUseSRV:              providers.Can("SRV records with empty targets are not supported"),
    32  	providers.CanUseTLSA:             providers.Cannot(),
    33  	providers.DocCreateDomains:       providers.Cannot(),
    34  	providers.DocDualHost:            providers.Cannot("Exoscale does not allow sufficient control over the apex NS records"),
    35  	providers.DocOfficiallySupported: providers.Cannot(),
    36  	providers.CanGetZones:            providers.Unimplemented(),
    37  }
    38  
    39  func init() {
    40  	providers.RegisterDomainServiceProviderType("EXOSCALE", NewExoscale, features)
    41  }
    42  
    43  // EnsureDomainExists returns an error if domain doesn't exist.
    44  func (c *exoscaleProvider) EnsureDomainExists(domain string) error {
    45  	ctx := context.Background()
    46  	_, err := c.client.GetDomain(ctx, domain)
    47  	if err != nil {
    48  		_, err := c.client.CreateDomain(ctx, domain)
    49  		if err != nil {
    50  			return err
    51  		}
    52  	}
    53  	return err
    54  }
    55  
    56  // GetNameservers returns the nameservers for domain.
    57  func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
    58  	return nil, nil
    59  }
    60  
    61  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
    62  func (c *exoscaleProvider) GetZoneRecords(domain string) (models.Records, error) {
    63  	return nil, fmt.Errorf("not implemented")
    64  	// This enables the get-zones subcommand.
    65  	// Implement this by extracting the code from GetDomainCorrections into
    66  	// a single function.  For most providers this should be relatively easy.
    67  }
    68  
    69  // GetDomainCorrections returns a list of corretions for the  domain.
    70  func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    71  	dc.Punycode()
    72  
    73  	ctx := context.Background()
    74  	records, err := c.client.GetRecords(ctx, dc.Name)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	existingRecords := make([]*models.RecordConfig, 0, len(records))
    80  	for _, r := range records {
    81  		if r.RecordType == "SOA" || r.RecordType == "NS" {
    82  			continue
    83  		}
    84  		if r.Name == "" {
    85  			r.Name = "@"
    86  		}
    87  		if r.RecordType == "CNAME" || r.RecordType == "MX" || r.RecordType == "ALIAS" {
    88  			r.Content += "."
    89  		}
    90  		// exoscale adds these odd txt records that mirror the alias records.
    91  		// they seem to manage them on deletes and things, so we'll just pretend they don't exist
    92  		if r.RecordType == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") {
    93  			continue
    94  		}
    95  		rec := &models.RecordConfig{
    96  			TTL:      uint32(r.TTL),
    97  			Original: r,
    98  		}
    99  		rec.SetLabel(r.Name, dc.Name)
   100  		switch rtype := r.RecordType; rtype {
   101  		case "ALIAS", "URL":
   102  			rec.Type = r.RecordType
   103  			rec.SetTarget(r.Content)
   104  		case "MX":
   105  			if err := rec.SetTargetMX(uint16(r.Prio), r.Content); err != nil {
   106  				panic(fmt.Errorf("unparsable record received from exoscale: %w", err))
   107  			}
   108  		case "SRV":
   109  			var err error
   110  			parts := strings.Fields(r.Content)
   111  			if len(parts) == 3 {
   112  				err = rec.SetTargetSRVPriorityString(uint16(r.Prio), r.Content)
   113  			} else {
   114  				r.Content += "."
   115  				err = rec.PopulateFromString(r.RecordType, r.Content, dc.Name)
   116  			}
   117  			if err != nil {
   118  				panic(fmt.Errorf("unparsable record received from exoscale: %w", err))
   119  			}
   120  		default:
   121  			if err := rec.PopulateFromString(r.RecordType, r.Content, dc.Name); err != nil {
   122  				panic(fmt.Errorf("unparsable record received from exoscale: %w", err))
   123  			}
   124  		}
   125  		existingRecords = append(existingRecords, rec)
   126  	}
   127  	removeOtherNS(dc)
   128  
   129  	// Normalize
   130  	models.PostProcessRecords(existingRecords)
   131  
   132  	differ := diff.New(dc)
   133  	_, create, delete, modify := differ.IncrementalDiff(existingRecords)
   134  
   135  	var corrections = []*models.Correction{}
   136  
   137  	for _, del := range delete {
   138  		rec := del.Existing.Original.(egoscale.DNSRecord)
   139  		corrections = append(corrections, &models.Correction{
   140  			Msg: del.String(),
   141  			F:   c.deleteRecordFunc(rec.ID, dc.Name),
   142  		})
   143  	}
   144  
   145  	for _, cre := range create {
   146  		rec := cre.Desired
   147  		corrections = append(corrections, &models.Correction{
   148  			Msg: cre.String(),
   149  			F:   c.createRecordFunc(rec, dc.Name),
   150  		})
   151  	}
   152  
   153  	for _, mod := range modify {
   154  		old := mod.Existing.Original.(egoscale.DNSRecord)
   155  		new := mod.Desired
   156  		corrections = append(corrections, &models.Correction{
   157  			Msg: mod.String(),
   158  			F:   c.updateRecordFunc(&old, new, dc.Name),
   159  		})
   160  	}
   161  
   162  	return corrections, nil
   163  }
   164  
   165  // Returns a function that can be invoked to create a record in a zone.
   166  func (c *exoscaleProvider) createRecordFunc(rc *models.RecordConfig, domainName string) func() error {
   167  	return func() error {
   168  		client := c.client
   169  
   170  		target := rc.GetTargetCombined()
   171  		name := rc.GetLabel()
   172  
   173  		if rc.Type == "MX" {
   174  			target = rc.Target
   175  		}
   176  
   177  		if rc.Type == "NS" && (name == "@" || name == "") {
   178  			name = "*"
   179  		}
   180  
   181  		record := egoscale.DNSRecord{
   182  			Name:       name,
   183  			RecordType: rc.Type,
   184  			Content:    target,
   185  			TTL:        int(rc.TTL),
   186  			Prio:       int(rc.MxPreference),
   187  		}
   188  		ctx := context.Background()
   189  		_, err := client.CreateRecord(ctx, domainName, record)
   190  		if err != nil {
   191  			return err
   192  		}
   193  
   194  		return nil
   195  	}
   196  }
   197  
   198  // Returns a function that can be invoked to delete a record in a zone.
   199  func (c *exoscaleProvider) deleteRecordFunc(recordID int64, domainName string) func() error {
   200  	return func() error {
   201  		client := c.client
   202  
   203  		ctx := context.Background()
   204  		if err := client.DeleteRecord(ctx, domainName, recordID); err != nil {
   205  			return err
   206  		}
   207  
   208  		return nil
   209  
   210  	}
   211  }
   212  
   213  // Returns a function that can be invoked to update a record in a zone.
   214  func (c *exoscaleProvider) updateRecordFunc(old *egoscale.DNSRecord, rc *models.RecordConfig, domainName string) func() error {
   215  	return func() error {
   216  		client := c.client
   217  
   218  		target := rc.GetTargetCombined()
   219  		name := rc.GetLabel()
   220  
   221  		if rc.Type == "MX" {
   222  			target = rc.Target
   223  		}
   224  
   225  		if rc.Type == "NS" && (name == "@" || name == "") {
   226  			name = "*"
   227  		}
   228  
   229  		record := egoscale.UpdateDNSRecord{
   230  			Name:       name,
   231  			RecordType: rc.Type,
   232  			Content:    target,
   233  			TTL:        int(rc.TTL),
   234  			Prio:       int(rc.MxPreference),
   235  			ID:         old.ID,
   236  		}
   237  
   238  		ctx := context.Background()
   239  		_, err := client.UpdateRecord(ctx, domainName, record)
   240  		if err != nil {
   241  			return err
   242  		}
   243  
   244  		return nil
   245  	}
   246  }
   247  
   248  func defaultNSSUffix(defNS string) bool {
   249  	return (strings.HasSuffix(defNS, ".exoscale.io.") ||
   250  		strings.HasSuffix(defNS, ".exoscale.com.") ||
   251  		strings.HasSuffix(defNS, ".exoscale.ch.") ||
   252  		strings.HasSuffix(defNS, ".exoscale.net."))
   253  }
   254  
   255  // remove all non-exoscale NS records from our desired state.
   256  // if any are found, print a warning
   257  func removeOtherNS(dc *models.DomainConfig) {
   258  	newList := make([]*models.RecordConfig, 0, len(dc.Records))
   259  	for _, rec := range dc.Records {
   260  		if rec.Type == "NS" {
   261  			// apex NS inside exoscale are expected.
   262  			if rec.GetLabelFQDN() == dc.Name && defaultNSSUffix(rec.GetTargetField()) {
   263  				continue
   264  			}
   265  			fmt.Printf("Warning: exoscale.com(.io, .ch, .net) does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField())
   266  			continue
   267  		}
   268  		newList = append(newList, rec)
   269  	}
   270  	dc.Records = newList
   271  }