github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/digitalocean/digitaloceanProvider.go (about)

     1  package digitalocean
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  
     9  	"github.com/StackExchange/dnscontrol/models"
    10  	"github.com/StackExchange/dnscontrol/providers"
    11  	"github.com/StackExchange/dnscontrol/providers/diff"
    12  	"github.com/miekg/dns/dnsutil"
    13  
    14  	"github.com/digitalocean/godo"
    15  	"golang.org/x/oauth2"
    16  )
    17  
    18  /*
    19  
    20  Digitalocean API DNS provider:
    21  
    22  Info required in `creds.json`:
    23     - token
    24  
    25  */
    26  
    27  type DoApi struct {
    28  	client *godo.Client
    29  }
    30  
    31  var defaultNameServerNames = []string{
    32  	"ns1.digitalocean.com",
    33  	"ns2.digitalocean.com",
    34  	"ns3.digitalocean.com",
    35  }
    36  
    37  func NewDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    38  	if m["token"] == "" {
    39  		return nil, fmt.Errorf("Digitalocean Token must be provided.")
    40  	}
    41  
    42  	ctx := context.Background()
    43  	oauthClient := oauth2.NewClient(
    44  		ctx,
    45  		oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
    46  	)
    47  	client := godo.NewClient(oauthClient)
    48  
    49  	api := &DoApi{client: client}
    50  
    51  	// Get a domain to validate the token
    52  	_, resp, err := api.client.Domains.List(ctx, &godo.ListOptions{PerPage: 1})
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	if resp.StatusCode != http.StatusOK {
    57  		return nil, fmt.Errorf("Digitalocean Token is not valid.")
    58  	}
    59  
    60  	return api, nil
    61  }
    62  
    63  var docNotes = providers.DocumentationNotes{
    64  	providers.DocCreateDomains:       providers.Can(),
    65  	providers.DocOfficiallySupported: providers.Cannot(),
    66  }
    67  
    68  func init() {
    69  	providers.RegisterDomainServiceProviderType("DIGITALOCEAN", NewDo, providers.CanUseSRV, docNotes)
    70  }
    71  
    72  func (api *DoApi) EnsureDomainExists(domain string) error {
    73  	ctx := context.Background()
    74  	_, resp, err := api.client.Domains.Get(ctx, domain)
    75  	if resp.StatusCode == http.StatusNotFound {
    76  		_, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{
    77  			Name:      domain,
    78  			IPAddress: "",
    79  		})
    80  		return err
    81  	} else {
    82  		return err
    83  	}
    84  }
    85  
    86  func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
    87  	return models.StringsToNameservers(defaultNameServerNames), nil
    88  }
    89  
    90  func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    91  	ctx := context.Background()
    92  	dc.Punycode()
    93  
    94  	records, err := getRecords(api, dc.Name)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	existingRecords := make([]*models.RecordConfig, len(records))
   100  	for i := range records {
   101  		existingRecords[i] = toRc(dc, &records[i])
   102  	}
   103  
   104  	differ := diff.New(dc)
   105  	_, create, delete, modify := differ.IncrementalDiff(existingRecords)
   106  
   107  	var corrections = []*models.Correction{}
   108  
   109  	// Deletes first so changing type works etc.
   110  	for _, m := range delete {
   111  		id := m.Existing.Original.(*godo.DomainRecord).ID
   112  		corr := &models.Correction{
   113  			Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
   114  			F: func() error {
   115  				_, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id)
   116  				return err
   117  			},
   118  		}
   119  		corrections = append(corrections, corr)
   120  	}
   121  	for _, m := range create {
   122  		req := toReq(dc, m.Desired)
   123  		corr := &models.Correction{
   124  			Msg: m.String(),
   125  			F: func() error {
   126  				_, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req)
   127  				return err
   128  			},
   129  		}
   130  		corrections = append(corrections, corr)
   131  	}
   132  	for _, m := range modify {
   133  		id := m.Existing.Original.(*godo.DomainRecord).ID
   134  		req := toReq(dc, m.Desired)
   135  		corr := &models.Correction{
   136  			Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
   137  			F: func() error {
   138  				_, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req)
   139  				return err
   140  			},
   141  		}
   142  		corrections = append(corrections, corr)
   143  	}
   144  
   145  	return corrections, nil
   146  }
   147  
   148  func getRecords(api *DoApi, name string) ([]godo.DomainRecord, error) {
   149  	ctx := context.Background()
   150  
   151  	records := []godo.DomainRecord{}
   152  	opt := &godo.ListOptions{}
   153  	for {
   154  		result, resp, err := api.client.Domains.Records(ctx, name, opt)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  
   159  		for _, d := range result {
   160  			records = append(records, d)
   161  		}
   162  
   163  		if resp.Links == nil || resp.Links.IsLastPage() {
   164  			break
   165  		}
   166  
   167  		page, err := resp.Links.CurrentPage()
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  
   172  		opt.Page = page + 1
   173  	}
   174  
   175  	return records, nil
   176  }
   177  
   178  func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
   179  	// This handles "@" etc.
   180  	name := dnsutil.AddOrigin(r.Name, dc.Name)
   181  
   182  	target := r.Data
   183  	// Make target FQDN (#rtype_variations)
   184  	if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
   185  		// If target is the domainname, e.g. cname foo.example.com -> example.com,
   186  		// DO returns "@" on read even if fqdn was written.
   187  		if target == "@" {
   188  			target = dc.Name
   189  		}
   190  		target = dnsutil.AddOrigin(target+".", dc.Name)
   191  	}
   192  
   193  	return &models.RecordConfig{
   194  		NameFQDN:     name,
   195  		Type:         r.Type,
   196  		Target:       target,
   197  		TTL:          uint32(r.TTL),
   198  		MxPreference: uint16(r.Priority),
   199  		SrvPriority:  uint16(r.Priority),
   200  		SrvWeight:    uint16(r.Weight),
   201  		SrvPort:      uint16(r.Port),
   202  		Original:     r,
   203  	}
   204  }
   205  
   206  func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
   207  	// DO wants the short name, e.g. @
   208  	name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
   209  
   210  	// DO uses the same property for MX and SRV priority
   211  	priority := 0
   212  	switch rc.Type { // #rtype_variations
   213  	case "MX":
   214  		priority = int(rc.MxPreference)
   215  	case "SRV":
   216  		priority = int(rc.SrvPriority)
   217  	}
   218  
   219  	return &godo.DomainRecordEditRequest{
   220  		Type:     rc.Type,
   221  		Name:     name,
   222  		Data:     rc.Target,
   223  		TTL:      int(rc.TTL),
   224  		Priority: priority,
   225  		Port:     int(rc.SrvPort),
   226  		Weight:   int(rc.SrvWeight),
   227  	}
   228  }