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

     1  package softlayer
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/softlayer/softlayer-go/datatypes"
    10  	"github.com/softlayer/softlayer-go/filter"
    11  	"github.com/softlayer/softlayer-go/services"
    12  	"github.com/softlayer/softlayer-go/session"
    13  
    14  	"github.com/StackExchange/dnscontrol/v2/models"
    15  	"github.com/StackExchange/dnscontrol/v2/providers"
    16  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    17  )
    18  
    19  // SoftLayer is the protocol handle for this provider.
    20  type SoftLayer struct {
    21  	Session *session.Session
    22  }
    23  
    24  var features = providers.DocumentationNotes{
    25  	providers.CanUseSRV:   providers.Can(),
    26  	providers.CanGetZones: providers.Unimplemented(),
    27  }
    28  
    29  func init() {
    30  	providers.RegisterDomainServiceProviderType("SOFTLAYER", newReg, features)
    31  }
    32  
    33  func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
    34  	s := session.New(conf["username"], conf["api_key"], conf["endpoint_url"], conf["timeout"])
    35  
    36  	if len(s.UserName) == 0 || len(s.APIKey) == 0 {
    37  		return nil, fmt.Errorf("SoftLayer UserName and APIKey must be provided")
    38  	}
    39  
    40  	// s.Debug = true
    41  
    42  	api := &SoftLayer{
    43  		Session: s,
    44  	}
    45  
    46  	return api, nil
    47  }
    48  
    49  // GetNameservers returns the nameservers for a domain.
    50  func (s *SoftLayer) GetNameservers(domain string) ([]*models.Nameserver, error) {
    51  	// Always use the same nameservers for softlayer
    52  	nservers := []string{"ns1.softlayer.com", "ns2.softlayer.com"}
    53  	return models.StringsToNameservers(nservers), nil
    54  }
    55  
    56  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
    57  func (client *SoftLayer) GetZoneRecords(domain string) (models.Records, error) {
    58  	return nil, fmt.Errorf("not implemented")
    59  	// This enables the get-zones subcommand.
    60  	// Implement this by extracting the code from GetDomainCorrections into
    61  	// a single function.  For most providers this should be relatively easy.
    62  }
    63  
    64  // GetDomainCorrections returns corrections to update a domain.
    65  func (s *SoftLayer) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    66  	corrections := []*models.Correction{}
    67  
    68  	domain, err := s.getDomain(&dc.Name)
    69  
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	actual, err := s.getExistingRecords(domain)
    75  
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	_, create, delete, modify := diff.New(dc).IncrementalDiff(actual)
    81  
    82  	for _, del := range delete {
    83  		existing := del.Existing.Original.(datatypes.Dns_Domain_ResourceRecord)
    84  		corrections = append(corrections, &models.Correction{
    85  			Msg: del.String(),
    86  			F:   s.deleteRecordFunc(*existing.Id),
    87  		})
    88  	}
    89  
    90  	for _, cre := range create {
    91  		corrections = append(corrections, &models.Correction{
    92  			Msg: cre.String(),
    93  			F:   s.createRecordFunc(cre.Desired, domain),
    94  		})
    95  	}
    96  
    97  	for _, mod := range modify {
    98  		existing := mod.Existing.Original.(datatypes.Dns_Domain_ResourceRecord)
    99  		corrections = append(corrections, &models.Correction{
   100  			Msg: mod.String(),
   101  			F:   s.updateRecordFunc(&existing, mod.Desired),
   102  		})
   103  	}
   104  
   105  	return corrections, nil
   106  }
   107  
   108  func (s *SoftLayer) getDomain(name *string) (*datatypes.Dns_Domain, error) {
   109  	domains, err := services.GetAccountService(s.Session).
   110  		Filter(filter.Path("domains.name").Eq(name).Build()).
   111  		Mask("resourceRecords").
   112  		GetDomains()
   113  
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	if len(domains) == 0 {
   119  		return nil, fmt.Errorf("Didn't find a domain matching %s", *name)
   120  	} else if len(domains) > 1 {
   121  		return nil, fmt.Errorf("Found %d domains matching %s", len(domains), *name)
   122  	}
   123  
   124  	return &domains[0], nil
   125  }
   126  
   127  func (s *SoftLayer) getExistingRecords(domain *datatypes.Dns_Domain) ([]*models.RecordConfig, error) {
   128  	actual := []*models.RecordConfig{}
   129  
   130  	for _, record := range domain.ResourceRecords {
   131  		recType := strings.ToUpper(*record.Type)
   132  
   133  		if recType == "SOA" {
   134  			continue
   135  		}
   136  
   137  		recConfig := &models.RecordConfig{
   138  			Type:     recType,
   139  			TTL:      uint32(*record.Ttl),
   140  			Original: record,
   141  		}
   142  		recConfig.SetTarget(*record.Data)
   143  
   144  		switch recType {
   145  		case "SRV":
   146  			var service, protocol string = "", "_tcp"
   147  
   148  			if record.Weight != nil {
   149  				recConfig.SrvWeight = uint16(*record.Weight)
   150  			}
   151  			if record.Port != nil {
   152  				recConfig.SrvPort = uint16(*record.Port)
   153  			}
   154  			if record.Priority != nil {
   155  				recConfig.SrvPriority = uint16(*record.Priority)
   156  			}
   157  			if record.Protocol != nil {
   158  				protocol = *record.Protocol
   159  			}
   160  			if record.Service != nil {
   161  				service = *record.Service
   162  			}
   163  			recConfig.SetLabel(fmt.Sprintf("%s.%s", service, strings.ToLower(protocol)), *domain.Name)
   164  		case "TXT":
   165  			recConfig.TxtStrings = append(recConfig.TxtStrings, *record.Data)
   166  			fallthrough
   167  		case "MX":
   168  			if record.MxPriority != nil {
   169  				recConfig.MxPreference = uint16(*record.MxPriority)
   170  			}
   171  			fallthrough
   172  		default:
   173  			recConfig.SetLabel(*record.Host, *domain.Name)
   174  		}
   175  
   176  		actual = append(actual, recConfig)
   177  	}
   178  
   179  	// Normalize
   180  	models.PostProcessRecords(actual)
   181  
   182  	return actual, nil
   183  }
   184  
   185  func (s *SoftLayer) createRecordFunc(desired *models.RecordConfig, domain *datatypes.Dns_Domain) func() error {
   186  	var ttl, preference, domainID int = int(desired.TTL), int(desired.MxPreference), *domain.Id
   187  	var weight, priority, port int = int(desired.SrvWeight), int(desired.SrvPriority), int(desired.SrvPort)
   188  	var host, data, newType string = desired.GetLabel(), desired.GetTargetField(), desired.Type
   189  	var err error
   190  
   191  	srvRegexp := regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`)
   192  
   193  	return func() error {
   194  		newRecord := datatypes.Dns_Domain_ResourceRecord{
   195  			DomainId: &domainID,
   196  			Ttl:      &ttl,
   197  			Type:     &newType,
   198  			Data:     &data,
   199  			Host:     &host,
   200  		}
   201  
   202  		switch newType {
   203  		case "MX":
   204  			service := services.GetDnsDomainResourceRecordMxTypeService(s.Session)
   205  
   206  			newRecord.MxPriority = &preference
   207  
   208  			newMx := datatypes.Dns_Domain_ResourceRecord_MxType{
   209  				Dns_Domain_ResourceRecord: newRecord,
   210  			}
   211  
   212  			_, err = service.CreateObject(&newMx)
   213  
   214  		case "SRV":
   215  			service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session)
   216  			result := srvRegexp.FindStringSubmatch(host)
   217  
   218  			if len(result) != 3 {
   219  				return fmt.Errorf("SRV Record must match format \"_service._protocol\" not %s", host)
   220  			}
   221  
   222  			var serviceName, protocol string = result[1], strings.ToLower(result[2])
   223  
   224  			newSrv := datatypes.Dns_Domain_ResourceRecord_SrvType{
   225  				Dns_Domain_ResourceRecord: newRecord,
   226  				Service:                   &serviceName,
   227  				Port:                      &port,
   228  				Priority:                  &priority,
   229  				Protocol:                  &protocol,
   230  				Weight:                    &weight,
   231  			}
   232  
   233  			_, err = service.CreateObject(&newSrv)
   234  
   235  		default:
   236  			service := services.GetDnsDomainResourceRecordService(s.Session)
   237  			_, err = service.CreateObject(&newRecord)
   238  		}
   239  
   240  		return err
   241  	}
   242  }
   243  
   244  func (s *SoftLayer) deleteRecordFunc(resID int) func() error {
   245  	// seems to be no problem deleting MX and SRV records via common interface
   246  	return func() error {
   247  		_, err := services.GetDnsDomainResourceRecordService(s.Session).
   248  			Id(resID).
   249  			DeleteObject()
   250  
   251  		return err
   252  	}
   253  }
   254  
   255  func (s *SoftLayer) updateRecordFunc(existing *datatypes.Dns_Domain_ResourceRecord, desired *models.RecordConfig) func() error {
   256  	var ttl, preference int = int(desired.TTL), int(desired.MxPreference)
   257  	var priority, weight, port int = int(desired.SrvPriority), int(desired.SrvWeight), int(desired.SrvPort)
   258  
   259  	return func() error {
   260  		var changes = false
   261  		var err error
   262  
   263  		switch desired.Type {
   264  		case "MX":
   265  			service := services.GetDnsDomainResourceRecordMxTypeService(s.Session)
   266  			updated := datatypes.Dns_Domain_ResourceRecord_MxType{}
   267  
   268  			label := desired.GetLabel()
   269  			if label != *existing.Host {
   270  				updated.Host = &label
   271  				changes = true
   272  			}
   273  
   274  			target := desired.GetTargetField()
   275  			if target != *existing.Data {
   276  				updated.Data = &target
   277  				changes = true
   278  			}
   279  
   280  			if ttl != *existing.Ttl {
   281  				updated.Ttl = &ttl
   282  				changes = true
   283  			}
   284  
   285  			if preference != *existing.MxPriority {
   286  				updated.MxPriority = &preference
   287  				changes = true
   288  			}
   289  
   290  			if !changes {
   291  				return fmt.Errorf("didn't find changes when I expect some")
   292  			}
   293  
   294  			_, err = service.Id(*existing.Id).EditObject(&updated)
   295  
   296  		case "SRV":
   297  			service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session)
   298  			updated := datatypes.Dns_Domain_ResourceRecord_SrvType{}
   299  
   300  			label := desired.GetLabel()
   301  			if label != *existing.Host {
   302  				updated.Host = &label
   303  				changes = true
   304  			}
   305  
   306  			target := desired.GetTargetField()
   307  			if target != *existing.Data {
   308  				updated.Data = &target
   309  				changes = true
   310  			}
   311  
   312  			if ttl != *existing.Ttl {
   313  				updated.Ttl = &ttl
   314  				changes = true
   315  			}
   316  
   317  			if priority != *existing.Priority {
   318  				updated.Priority = &priority
   319  				changes = true
   320  			}
   321  
   322  			if weight != *existing.Weight {
   323  				updated.Weight = &weight
   324  				changes = true
   325  			}
   326  
   327  			if port != *existing.Port {
   328  				updated.Port = &port
   329  				changes = true
   330  			}
   331  
   332  			// TODO: handle service & protocol - or does that just result in a
   333  			// delete and recreate?
   334  
   335  			if !changes {
   336  				return fmt.Errorf("didn't find changes when I expect some")
   337  			}
   338  
   339  			_, err = service.Id(*existing.Id).EditObject(&updated)
   340  
   341  		default:
   342  			service := services.GetDnsDomainResourceRecordService(s.Session)
   343  			updated := datatypes.Dns_Domain_ResourceRecord{}
   344  
   345  			label := desired.GetLabel()
   346  			if label != *existing.Host {
   347  				updated.Host = &label
   348  				changes = true
   349  			}
   350  
   351  			target := desired.GetTargetField()
   352  			if target != *existing.Data {
   353  				updated.Data = &target
   354  				changes = true
   355  			}
   356  
   357  			if ttl != *existing.Ttl {
   358  				updated.Ttl = &ttl
   359  				changes = true
   360  			}
   361  
   362  			if !changes {
   363  				return fmt.Errorf("didn't find changes when I expect some")
   364  			}
   365  
   366  			_, err = service.Id(*existing.Id).EditObject(&updated)
   367  		}
   368  
   369  		return err
   370  	}
   371  }