package softlayer import ( "encoding/json" "fmt" "regexp" "strings" "github.com/StackExchange/dnscontrol/v2/models" "github.com/StackExchange/dnscontrol/v2/providers" "github.com/StackExchange/dnscontrol/v2/providers/diff" "github.com/pkg/errors" "github.com/softlayer/softlayer-go/datatypes" "github.com/softlayer/softlayer-go/filter" "github.com/softlayer/softlayer-go/services" "github.com/softlayer/softlayer-go/session" ) // SoftLayer is the protocol handle for this provider. type SoftLayer struct { Session *session.Session } var features = providers.DocumentationNotes{ providers.CanUseSRV: providers.Can(), } func init() { providers.RegisterDomainServiceProviderType("SOFTLAYER", newReg, features) } func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { s := session.New(conf["username"], conf["api_key"], conf["endpoint_url"], conf["timeout"]) if len(s.UserName) == 0 || len(s.APIKey) == 0 { return nil, errors.Errorf("SoftLayer UserName and APIKey must be provided") } // s.Debug = true api := &SoftLayer{ Session: s, } return api, nil } // GetNameservers returns the nameservers for a domain. func (s *SoftLayer) GetNameservers(domain string) ([]*models.Nameserver, error) { // Always use the same nameservers for softlayer nservers := []string{"ns1.softlayer.com", "ns2.softlayer.com"} return models.StringsToNameservers(nservers), nil } // GetDomainCorrections returns corrections to update a domain. func (s *SoftLayer) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { corrections := []*models.Correction{} domain, err := s.getDomain(&dc.Name) if err != nil { return nil, err } actual, err := s.getExistingRecords(domain) if err != nil { return nil, err } _, create, delete, modify := diff.New(dc).IncrementalDiff(actual) for _, del := range delete { existing := del.Existing.Original.(datatypes.Dns_Domain_ResourceRecord) corrections = append(corrections, &models.Correction{ Msg: del.String(), F: s.deleteRecordFunc(*existing.Id), }) } for _, cre := range create { corrections = append(corrections, &models.Correction{ Msg: cre.String(), F: s.createRecordFunc(cre.Desired, domain), }) } for _, mod := range modify { existing := mod.Existing.Original.(datatypes.Dns_Domain_ResourceRecord) corrections = append(corrections, &models.Correction{ Msg: mod.String(), F: s.updateRecordFunc(&existing, mod.Desired), }) } return corrections, nil } func (s *SoftLayer) getDomain(name *string) (*datatypes.Dns_Domain, error) { domains, err := services.GetAccountService(s.Session). Filter(filter.Path("domains.name").Eq(name).Build()). Mask("resourceRecords"). GetDomains() if err != nil { return nil, err } if len(domains) == 0 { return nil, errors.Errorf("Didn't find a domain matching %s", *name) } else if len(domains) > 1 { return nil, errors.Errorf("Found %d domains matching %s", len(domains), *name) } return &domains[0], nil } func (s *SoftLayer) getExistingRecords(domain *datatypes.Dns_Domain) ([]*models.RecordConfig, error) { actual := []*models.RecordConfig{} for _, record := range domain.ResourceRecords { recType := strings.ToUpper(*record.Type) if recType == "SOA" { continue } recConfig := &models.RecordConfig{ Type: recType, TTL: uint32(*record.Ttl), Original: record, } recConfig.SetTarget(*record.Data) switch recType { case "SRV": var service, protocol string = "", "_tcp" if record.Weight != nil { recConfig.SrvWeight = uint16(*record.Weight) } if record.Port != nil { recConfig.SrvPort = uint16(*record.Port) } if record.Priority != nil { recConfig.SrvPriority = uint16(*record.Priority) } if record.Protocol != nil { protocol = *record.Protocol } if record.Service != nil { service = *record.Service } recConfig.SetLabel(fmt.Sprintf("%s.%s", service, strings.ToLower(protocol)), *domain.Name) case "MX": if record.MxPriority != nil { recConfig.MxPreference = uint16(*record.MxPriority) } fallthrough default: recConfig.SetLabel(*record.Host, *domain.Name) } actual = append(actual, recConfig) } // Normalize models.PostProcessRecords(actual) return actual, nil } func (s *SoftLayer) createRecordFunc(desired *models.RecordConfig, domain *datatypes.Dns_Domain) func() error { var ttl, preference, domainID int = int(desired.TTL), int(desired.MxPreference), *domain.Id var weight, priority, port int = int(desired.SrvWeight), int(desired.SrvPriority), int(desired.SrvPort) var host, data, newType string = desired.GetLabel(), desired.GetTargetField(), desired.Type var err error srvRegexp := regexp.MustCompile(`^_(?P\w+)\.\_(?P\w+)$`) return func() error { newRecord := datatypes.Dns_Domain_ResourceRecord{ DomainId: &domainID, Ttl: &ttl, Type: &newType, Data: &data, Host: &host, } switch newType { case "MX": service := services.GetDnsDomainResourceRecordMxTypeService(s.Session) newRecord.MxPriority = &preference newMx := datatypes.Dns_Domain_ResourceRecord_MxType{ Dns_Domain_ResourceRecord: newRecord, } _, err = service.CreateObject(&newMx) case "SRV": service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session) result := srvRegexp.FindStringSubmatch(host) if len(result) != 3 { return errors.Errorf("SRV Record must match format \"_service._protocol\" not %s", host) } var serviceName, protocol string = result[1], strings.ToLower(result[2]) newSrv := datatypes.Dns_Domain_ResourceRecord_SrvType{ Dns_Domain_ResourceRecord: newRecord, Service: &serviceName, Port: &port, Priority: &priority, Protocol: &protocol, Weight: &weight, } _, err = service.CreateObject(&newSrv) default: service := services.GetDnsDomainResourceRecordService(s.Session) _, err = service.CreateObject(&newRecord) } return err } } func (s *SoftLayer) deleteRecordFunc(resID int) func() error { // seems to be no problem deleting MX and SRV records via common interface return func() error { _, err := services.GetDnsDomainResourceRecordService(s.Session). Id(resID). DeleteObject() return err } } func (s *SoftLayer) updateRecordFunc(existing *datatypes.Dns_Domain_ResourceRecord, desired *models.RecordConfig) func() error { var ttl, preference int = int(desired.TTL), int(desired.MxPreference) var priority, weight, port int = int(desired.SrvPriority), int(desired.SrvWeight), int(desired.SrvPort) return func() error { var changes = false var err error switch desired.Type { case "MX": service := services.GetDnsDomainResourceRecordMxTypeService(s.Session) updated := datatypes.Dns_Domain_ResourceRecord_MxType{} label := desired.GetLabel() if label != *existing.Host { updated.Host = &label changes = true } target := desired.GetTargetField() if target != *existing.Data { updated.Data = &target changes = true } if ttl != *existing.Ttl { updated.Ttl = &ttl changes = true } if preference != *existing.MxPriority { updated.MxPriority = &preference changes = true } if !changes { return errors.Errorf("didn't find changes when I expect some") } _, err = service.Id(*existing.Id).EditObject(&updated) case "SRV": service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session) updated := datatypes.Dns_Domain_ResourceRecord_SrvType{} label := desired.GetLabel() if label != *existing.Host { updated.Host = &label changes = true } target := desired.GetTargetField() if target != *existing.Data { updated.Data = &target changes = true } if ttl != *existing.Ttl { updated.Ttl = &ttl changes = true } if priority != *existing.Priority { updated.Priority = &priority changes = true } if weight != *existing.Weight { updated.Weight = &weight changes = true } if port != *existing.Port { updated.Port = &port changes = true } // TODO: handle service & protocol - or does that just result in a // delete and recreate? if !changes { return errors.Errorf("didn't find changes when I expect some") } _, err = service.Id(*existing.Id).EditObject(&updated) default: service := services.GetDnsDomainResourceRecordService(s.Session) updated := datatypes.Dns_Domain_ResourceRecord{} label := desired.GetLabel() if label != *existing.Host { updated.Host = &label changes = true } target := desired.GetTargetField() if target != *existing.Data { updated.Data = &target changes = true } if ttl != *existing.Ttl { updated.Ttl = &ttl changes = true } if !changes { return errors.Errorf("didn't find changes when I expect some") } _, err = service.Id(*existing.Id).EditObject(&updated) } return err } }