Initial commit
This commit is contained in:
commit
f57d355631
27 changed files with 1315 additions and 0 deletions
105
provider/digitalocean/mock_test.go
Normal file
105
provider/digitalocean/mock_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mock struct {
|
||||
t *testing.T
|
||||
|
||||
records_by_type map[string][]godo.DomainRecord
|
||||
|
||||
edit_record_request *godo.DomainRecordEditRequest
|
||||
edit_record_error error
|
||||
}
|
||||
|
||||
func (m mock) List(context.Context, *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
|
||||
m.t.Error("List called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Get(context.Context, string) (*godo.Domain, *godo.Response, error) {
|
||||
m.t.Error("Get called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Create(context.Context, *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) {
|
||||
m.t.Error("Create called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Delete(context.Context, string) (*godo.Response, error) {
|
||||
m.t.Error("Delete called when it should not have been")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Records(context.Context, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
|
||||
m.t.Error("Records called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) RecordsByType(_ context.Context, name string, t string, opt *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
|
||||
var err error
|
||||
|
||||
// Only care about "A" records
|
||||
assert.Equal(m.t, "A", t)
|
||||
|
||||
r, ok := m.records_by_type[name]
|
||||
if !ok {
|
||||
err = errors.New("Record not found")
|
||||
}
|
||||
return r, nil, err
|
||||
}
|
||||
|
||||
func (m mock) RecordsByName(context.Context, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
|
||||
m.t.Error("RecordsByName called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) RecordsByTypeAndName(context.Context, string, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
|
||||
m.t.Error("RecordsByTypeAndName called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Record(context.Context, string, int) (*godo.DomainRecord, *godo.Response, error) {
|
||||
m.t.Error("Record called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) DeleteRecord(context.Context, string, int) (*godo.Response, error) {
|
||||
m.t.Error("DeleteRecord called when it should not have been")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mock) EditRecord(_ context.Context, domain string, id int, req *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
|
||||
if m.edit_record_request == nil {
|
||||
m.t.Error("EditRecord called with empty request")
|
||||
}
|
||||
|
||||
assert.Equal(m.t, m.edit_record_request, req)
|
||||
|
||||
record := godo.DomainRecord{
|
||||
ID: id,
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
Data: req.Data,
|
||||
Priority: req.Priority,
|
||||
Port: req.Port,
|
||||
TTL: req.TTL,
|
||||
Weight: req.Weight,
|
||||
Flags: req.Flags,
|
||||
Tag: req.Tag,
|
||||
}
|
||||
|
||||
return &record, nil, m.edit_record_error
|
||||
}
|
||||
|
||||
func (m mock) CreateRecord(context.Context, string, *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
|
||||
m.t.Error("CreateRecord called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
98
provider/digitalocean/provider.go
Normal file
98
provider/digitalocean/provider.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"dnsupdater/provider"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
service godo.DomainsService
|
||||
cache map[string][]godo.DomainRecord
|
||||
}
|
||||
|
||||
func New(token string) Provider {
|
||||
return Provider{
|
||||
service: godo.NewFromToken(token).Domains,
|
||||
cache: make(map[string][]godo.DomainRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func Factory(args map[string]interface{}) (provider.Provider, error) {
|
||||
t, ok := args["token"]
|
||||
if !ok {
|
||||
return nil, errors.New("did not find token")
|
||||
}
|
||||
|
||||
token, ok := t.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("token must be a string")
|
||||
}
|
||||
|
||||
return New(token), nil
|
||||
}
|
||||
|
||||
func (d *Provider) fetch(domain string) ([]godo.DomainRecord, error) {
|
||||
domains, ok := d.cache[domain]
|
||||
if !ok {
|
||||
var err error
|
||||
options := &godo.ListOptions{
|
||||
PerPage: 50,
|
||||
}
|
||||
|
||||
domains, _, err = d.service.RecordsByType(context.Background(), domain, "A", options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.cache[domain] = domains
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (d *Provider) find(domain string, record string) (*godo.DomainRecord, error) {
|
||||
records, err := d.fetch(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range records {
|
||||
if r.Name == record {
|
||||
return &r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find record %s", record)
|
||||
}
|
||||
|
||||
func (d Provider) Update(domain string, record string, ip net.IP) error {
|
||||
r, err := d.find(domain, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Data != ip.String() {
|
||||
// Update
|
||||
req := godo.DomainRecordEditRequest{
|
||||
// Type: r.Type,
|
||||
// Name: r.Name,
|
||||
Data: ip.String(),
|
||||
// Priority: r.Priority,
|
||||
// Port: r.Port,
|
||||
// TTL: r.TTL,
|
||||
// Weight: r.Weight,
|
||||
// Flags: r.Flags,
|
||||
// Tag: r.Tag,
|
||||
}
|
||||
|
||||
_, _, err := d.service.EditRecord(context.Background(), domain, r.ID, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
263
provider/digitalocean/provider_test.go
Normal file
263
provider/digitalocean/provider_test.go
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvider_New(t *testing.T) {
|
||||
assert.Equal(t, Provider{
|
||||
service: godo.NewFromToken("token").Domains,
|
||||
cache: make(map[string][]godo.DomainRecord),
|
||||
}, New("token"))
|
||||
}
|
||||
|
||||
func TestProvider_fetch(t *testing.T) {
|
||||
expected := []godo.DomainRecord{
|
||||
{
|
||||
ID: 28448429,
|
||||
Type: "A",
|
||||
Name: "sub1",
|
||||
Data: "201.110.66.72",
|
||||
Priority: 2,
|
||||
TTL: 1800,
|
||||
},
|
||||
{
|
||||
ID: 28448430,
|
||||
Type: "A",
|
||||
Name: "sub2",
|
||||
Data: "242.124.218.187",
|
||||
Priority: 1,
|
||||
TTL: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
provider := Provider{
|
||||
service: mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": expected,
|
||||
},
|
||||
},
|
||||
cache: make(map[string][]godo.DomainRecord),
|
||||
}
|
||||
|
||||
records, err := provider.fetch("example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
|
||||
// Fetch invalid
|
||||
_, err = provider.fetch("noexists.com")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProvider_fetch_caches_records(t *testing.T) {
|
||||
example_com_records := []godo.DomainRecord{
|
||||
{
|
||||
ID: 28448429,
|
||||
Type: "A",
|
||||
Name: "sub1",
|
||||
Data: "107.218.197.189",
|
||||
Priority: 2,
|
||||
TTL: 1800,
|
||||
},
|
||||
{
|
||||
ID: 28448430,
|
||||
Type: "A",
|
||||
Name: "sub2",
|
||||
Data: "254.221.12.160",
|
||||
Priority: 1,
|
||||
TTL: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
another_com_records := []godo.DomainRecord{
|
||||
{
|
||||
ID: 237823,
|
||||
Type: "A",
|
||||
Name: "box",
|
||||
Data: "108.151.98.62",
|
||||
Priority: 2,
|
||||
TTL: 1800,
|
||||
},
|
||||
{
|
||||
ID: 237824,
|
||||
Type: "A",
|
||||
Name: "ntp",
|
||||
Data: "190.255.140.208",
|
||||
Priority: 10,
|
||||
TTL: 300,
|
||||
},
|
||||
}
|
||||
|
||||
mockService := mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": example_com_records,
|
||||
"another.com": another_com_records,
|
||||
},
|
||||
}
|
||||
|
||||
provider := Provider{
|
||||
service: mockService,
|
||||
cache: make(map[string][]godo.DomainRecord),
|
||||
}
|
||||
|
||||
records, err := provider.fetch("example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, example_com_records, records)
|
||||
|
||||
records, err = provider.fetch("another.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, another_com_records, records)
|
||||
|
||||
// Check cache, should be equal to the map in mock service.
|
||||
assert.Equal(t, provider.cache, mockService.records_by_type)
|
||||
}
|
||||
|
||||
func TestProvider_fetch_from_cache(t *testing.T) {
|
||||
expected := []godo.DomainRecord{
|
||||
{
|
||||
ID: 273671823,
|
||||
Type: "A",
|
||||
Name: "sub1",
|
||||
Data: "42.170.152.94",
|
||||
Priority: 10,
|
||||
TTL: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
provider := Provider{
|
||||
service: mock{t: t},
|
||||
cache: map[string][]godo.DomainRecord{
|
||||
"example.com": expected,
|
||||
},
|
||||
}
|
||||
|
||||
records, err := provider.fetch("example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
|
||||
_, err = provider.fetch("noexists.com")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProvider_find(t *testing.T) {
|
||||
expected := []godo.DomainRecord{
|
||||
{
|
||||
ID: 236718,
|
||||
Type: "A",
|
||||
Name: "sub1",
|
||||
Data: "161.125.137.64",
|
||||
Priority: 10,
|
||||
TTL: 1800,
|
||||
},
|
||||
{
|
||||
ID: 23123131,
|
||||
Type: "A",
|
||||
Name: "sub2",
|
||||
Data: "154.63.46.159",
|
||||
Priority: 5,
|
||||
TTL: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
expected_cache := []godo.DomainRecord{
|
||||
{
|
||||
ID: 23713762,
|
||||
Type: "A",
|
||||
Name: "mail",
|
||||
Data: "176.151.152.10",
|
||||
Priority: 10,
|
||||
TTL: 3600,
|
||||
},
|
||||
}
|
||||
|
||||
provider := Provider{
|
||||
service: mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": expected,
|
||||
},
|
||||
},
|
||||
cache: map[string][]godo.DomainRecord{
|
||||
"cached.com": expected_cache,
|
||||
},
|
||||
}
|
||||
|
||||
// Test fetch.
|
||||
record, err := provider.find("example.com", "sub2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected[1], *record)
|
||||
|
||||
// Test cached record
|
||||
record, err = provider.find("cached.com", "mail")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected_cache[0], *record)
|
||||
|
||||
// Test not found (domain)
|
||||
_, err = provider.find("noexists.com", "www")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test not found (subdomain)
|
||||
_, err = provider.find("cached.com", "nosub")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProvider_Update(t *testing.T) {
|
||||
expected := []godo.DomainRecord{
|
||||
{
|
||||
ID: 1337,
|
||||
Type: "A",
|
||||
Name: "www",
|
||||
Data: "80.17.42.157",
|
||||
Priority: 10,
|
||||
TTL: 360,
|
||||
Port: 22,
|
||||
Weight: 100,
|
||||
Flags: 0xf1,
|
||||
Tag: "some_tag",
|
||||
},
|
||||
}
|
||||
|
||||
mockService := mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": expected,
|
||||
},
|
||||
edit_record_request: &godo.DomainRecordEditRequest{
|
||||
// Type: "A",
|
||||
// Name: "www",
|
||||
Data: "221.135.170.186",
|
||||
// Priority: 10,
|
||||
// Port: 22,
|
||||
// TTL: 360,
|
||||
// Weight: 100,
|
||||
// Flags: 0xf1,
|
||||
// Tag: "some_tag",
|
||||
},
|
||||
}
|
||||
|
||||
provider := Provider{
|
||||
service: &mockService,
|
||||
cache: map[string][]godo.DomainRecord{},
|
||||
}
|
||||
|
||||
err := provider.Update("example.com", "www", net.IPv4(221, 135, 170, 186))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.Update("invalid.com", "www", net.IPv4(72, 82, 118, 186))
|
||||
assert.Error(t, err)
|
||||
|
||||
mockService.edit_record_error = errors.New("Error")
|
||||
|
||||
err = provider.Update("example.com", "www", net.IPv4(221, 135, 170, 186))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
11
provider/interface.go
Normal file
11
provider/interface.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Update(domain string, record string, ip net.IP) error
|
||||
}
|
||||
|
||||
type ProviderFactory func(map[string]interface{}) (Provider, error)
|
||||
48
provider/manager/manager.go
Normal file
48
provider/manager/manager.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dnsupdater/provider"
|
||||
"dnsupdater/provider/digitalocean"
|
||||
)
|
||||
|
||||
var factories = map[string]provider.ProviderFactory{
|
||||
"digitalocean": digitalocean.Factory,
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
services map[string]provider.Provider
|
||||
}
|
||||
|
||||
func New() *Manager {
|
||||
return &Manager{
|
||||
services: make(map[string]provider.Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Manager) Get(name string) provider.Provider {
|
||||
if service, ok := m.services[name]; ok {
|
||||
return service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) RegisterFromConfig(providers map[string]map[string]interface{}) error {
|
||||
for name, args := range providers {
|
||||
if factory, ok := factories[name]; ok {
|
||||
|
||||
provider, err := factory(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create provider '%s': %v", name, err)
|
||||
}
|
||||
|
||||
m.Register(name, provider)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) Register(name string, provider provider.Provider) {
|
||||
m.services[name] = provider
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue