github.com/hexonet/dnscontrol@v0.2.8/providers/softlayer/softlayerProvider.go (about)

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