github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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  	"github.com/pkg/errors"
    14  
    15  	"github.com/digitalocean/godo"
    16  	"golang.org/x/oauth2"
    17  )
    18  
    19  /*
    20  
    21  Digitalocean API DNS provider:
    22  
    23  Info required in `creds.json`:
    24     - token
    25  
    26  */
    27  
    28  // DoApi is the handle for operations.
    29  type DoApi struct {
    30  	client *godo.Client
    31  }
    32  
    33  var defaultNameServerNames = []string{
    34  	"ns1.digitalocean.com",
    35  	"ns2.digitalocean.com",
    36  	"ns3.digitalocean.com",
    37  }
    38  
    39  // NewDo creates a DO-specific DNS provider.
    40  func NewDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    41  	if m["token"] == "" {
    42  		return nil, errors.Errorf("no Digitalocean token provided")
    43  	}
    44  
    45  	ctx := context.Background()
    46  	oauthClient := oauth2.NewClient(
    47  		ctx,
    48  		oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
    49  	)
    50  	client := godo.NewClient(oauthClient)
    51  
    52  	api := &DoApi{client: client}
    53  
    54  	// Get a domain to validate the token
    55  	_, resp, err := api.client.Domains.List(ctx, &godo.ListOptions{PerPage: 1})
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	if resp.StatusCode != http.StatusOK {
    60  		return nil, errors.Errorf("token for digitalocean is not valid")
    61  	}
    62  
    63  	return api, nil
    64  }
    65  
    66  var features = providers.DocumentationNotes{
    67  	providers.DocCreateDomains:       providers.Can(),
    68  	providers.DocOfficiallySupported: providers.Cannot(),
    69  	providers.CanUseSRV:              providers.Can(),
    70  }
    71  
    72  func init() {
    73  	providers.RegisterDomainServiceProviderType("DIGITALOCEAN", NewDo, features)
    74  }
    75  
    76  // EnsureDomainExists returns an error if domain doesn't exist.
    77  func (api *DoApi) EnsureDomainExists(domain string) error {
    78  	ctx := context.Background()
    79  	_, resp, err := api.client.Domains.Get(ctx, domain)
    80  	if resp.StatusCode == http.StatusNotFound {
    81  		_, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{
    82  			Name:      domain,
    83  			IPAddress: "",
    84  		})
    85  		return err
    86  	}
    87  	return err
    88  }
    89  
    90  // GetNameservers returns the nameservers for domain.
    91  func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
    92  	return models.StringsToNameservers(defaultNameServerNames), nil
    93  }
    94  
    95  // GetDomainCorrections returns a list of corretions for the  domain.
    96  func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    97  	ctx := context.Background()
    98  	dc.Punycode()
    99  
   100  	records, err := getRecords(api, dc.Name)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	existingRecords := make([]*models.RecordConfig, len(records))
   106  	for i := range records {
   107  		existingRecords[i] = toRc(dc, &records[i])
   108  	}
   109  
   110  	// Normalize
   111  	models.PostProcessRecords(existingRecords)
   112  
   113  	differ := diff.New(dc)
   114  	_, create, delete, modify := differ.IncrementalDiff(existingRecords)
   115  
   116  	var corrections = []*models.Correction{}
   117  
   118  	// Deletes first so changing type works etc.
   119  	for _, m := range delete {
   120  		id := m.Existing.Original.(*godo.DomainRecord).ID
   121  		corr := &models.Correction{
   122  			Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
   123  			F: func() error {
   124  				_, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id)
   125  				return err
   126  			},
   127  		}
   128  		corrections = append(corrections, corr)
   129  	}
   130  	for _, m := range create {
   131  		req := toReq(dc, m.Desired)
   132  		corr := &models.Correction{
   133  			Msg: m.String(),
   134  			F: func() error {
   135  				_, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req)
   136  				return err
   137  			},
   138  		}
   139  		corrections = append(corrections, corr)
   140  	}
   141  	for _, m := range modify {
   142  		id := m.Existing.Original.(*godo.DomainRecord).ID
   143  		req := toReq(dc, m.Desired)
   144  		corr := &models.Correction{
   145  			Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
   146  			F: func() error {
   147  				_, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req)
   148  				return err
   149  			},
   150  		}
   151  		corrections = append(corrections, corr)
   152  	}
   153  
   154  	return corrections, nil
   155  }
   156  
   157  func getRecords(api *DoApi, name string) ([]godo.DomainRecord, error) {
   158  	ctx := context.Background()
   159  
   160  	records := []godo.DomainRecord{}
   161  	opt := &godo.ListOptions{}
   162  	for {
   163  		result, resp, err := api.client.Domains.Records(ctx, name, opt)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  
   168  		for _, d := range result {
   169  			records = append(records, d)
   170  		}
   171  
   172  		if resp.Links == nil || resp.Links.IsLastPage() {
   173  			break
   174  		}
   175  
   176  		page, err := resp.Links.CurrentPage()
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  
   181  		opt.Page = page + 1
   182  	}
   183  
   184  	return records, nil
   185  }
   186  
   187  func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
   188  	// This handles "@" etc.
   189  	name := dnsutil.AddOrigin(r.Name, dc.Name)
   190  
   191  	target := r.Data
   192  	// Make target FQDN (#rtype_variations)
   193  	if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
   194  		// If target is the domainname, e.g. cname foo.example.com -> example.com,
   195  		// DO returns "@" on read even if fqdn was written.
   196  		if target == "@" {
   197  			target = dc.Name
   198  		}
   199  		target = dnsutil.AddOrigin(target+".", dc.Name)
   200  		// FIXME(tlim): The AddOrigin should be a no-op.
   201  		// Test whether or not it is actually needed.
   202  	}
   203  
   204  	t := &models.RecordConfig{
   205  		Type:         r.Type,
   206  		TTL:          uint32(r.TTL),
   207  		MxPreference: uint16(r.Priority),
   208  		SrvPriority:  uint16(r.Priority),
   209  		SrvWeight:    uint16(r.Weight),
   210  		SrvPort:      uint16(r.Port),
   211  		Original:     r,
   212  	}
   213  	t.SetLabelFromFQDN(name, dc.Name)
   214  	t.SetTarget(target)
   215  	switch rtype := r.Type; rtype {
   216  	case "TXT":
   217  		t.SetTargetTXTString(target)
   218  	default:
   219  		// nothing additional required
   220  	}
   221  	return t
   222  }
   223  
   224  func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
   225  	name := rc.GetLabel()         // DO wants the short name or "@" for apex.
   226  	target := rc.GetTargetField() // DO uses the target field only for a single value
   227  	priority := 0                 // DO uses the same property for MX and SRV priority
   228  
   229  	switch rc.Type { // #rtype_variations
   230  	case "MX":
   231  		priority = int(rc.MxPreference)
   232  	case "SRV":
   233  		priority = int(rc.SrvPriority)
   234  	case "TXT":
   235  		// TXT records are the one place where DO combines many items into one field.
   236  		target = rc.GetTargetCombined()
   237  	default:
   238  		// no action required
   239  	}
   240  
   241  	return &godo.DomainRecordEditRequest{
   242  		Type:     rc.Type,
   243  		Name:     name,
   244  		Data:     target,
   245  		TTL:      int(rc.TTL),
   246  		Priority: priority,
   247  		Port:     int(rc.SrvPort),
   248  		Weight:   int(rc.SrvWeight),
   249  	}
   250  }