refactor
This commit is contained in:
parent
49563af412
commit
555c553686
18 changed files with 481 additions and 742 deletions
28
dns/cache.go
Normal file
28
dns/cache.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package dns
|
||||
|
||||
type DomainRecordCache struct {
|
||||
domains map[string]RecordList
|
||||
fetcher Fetcher
|
||||
}
|
||||
|
||||
type Fetcher func(domain string) (RecordList, error)
|
||||
|
||||
func NewDomainRecordCache(fetcher Fetcher) *DomainRecordCache {
|
||||
return &DomainRecordCache{
|
||||
fetcher: fetcher,
|
||||
domains: make(map[string]RecordList),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DomainRecordCache) Get(domain string) (RecordList, error) {
|
||||
records, ok := c.domains[domain]
|
||||
if !ok {
|
||||
var err error
|
||||
records, err = c.fetcher(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.domains[domain] = records
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
26
dns/record.go
Normal file
26
dns/record.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
Id string // Internal id.
|
||||
Name string
|
||||
Ip net.IP
|
||||
}
|
||||
|
||||
type RecordList []Record
|
||||
|
||||
func (l *RecordList) Add(record Record) {
|
||||
*l = append(*l, record)
|
||||
}
|
||||
|
||||
func (l RecordList) FindByName(name string) (Record, bool) {
|
||||
for _, record := range l {
|
||||
if record.Name == name {
|
||||
return record, true
|
||||
}
|
||||
}
|
||||
return Record{}, false
|
||||
}
|
||||
109
dns/service/digitalocean/mock_test.go
Normal file
109
dns/service/digitalocean/mock_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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")
|
||||
}
|
||||
|
||||
if m.edit_record_error != nil {
|
||||
return nil, nil, m.edit_record_error
|
||||
}
|
||||
|
||||
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, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
67
dns/service/digitalocean/service.go
Normal file
67
dns/service/digitalocean/service.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"dnsupdater/dns"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
api godo.DomainsService
|
||||
}
|
||||
|
||||
func New(token string) Service {
|
||||
return Service{
|
||||
api: godo.NewFromToken(token).Domains,
|
||||
}
|
||||
}
|
||||
|
||||
func Factory(args map[string]any) (any, 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 Service) List(domain_name string) (dns.RecordList, error) {
|
||||
fetchedRecords, _, err := d.api.RecordsByType(context.Background(), domain_name, "A", &godo.ListOptions{
|
||||
PerPage: 50,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := dns.RecordList{}
|
||||
for _, rec := range fetchedRecords {
|
||||
records.Add(dns.Record{
|
||||
Id: strconv.Itoa(rec.ID),
|
||||
Name: rec.Name,
|
||||
Ip: net.ParseIP(rec.Data),
|
||||
})
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (d Service) Update(domain, recordID, ip string) error {
|
||||
id, err := strconv.Atoi(recordID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err = d.api.EditRecord(context.Background(), domain, id, &godo.DomainRecordEditRequest{
|
||||
Data: ip,
|
||||
})
|
||||
return err
|
||||
}
|
||||
103
dns/service/digitalocean/service_test.go
Normal file
103
dns/service/digitalocean/service_test.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"dnsupdater/dns"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDigitalOceanService_New(t *testing.T) {
|
||||
assert.Equal(t, Service{
|
||||
api: godo.NewFromToken("token").Domains,
|
||||
}, New("token"))
|
||||
}
|
||||
|
||||
func TestDigitalOceanService_List(t *testing.T) {
|
||||
expected := dns.RecordList{
|
||||
{
|
||||
Id: "236718",
|
||||
Name: "sub1",
|
||||
Ip: net.IPv4(161, 125, 137, 64),
|
||||
},
|
||||
{
|
||||
Id: "23123131",
|
||||
Name: "sub2",
|
||||
Ip: net.IPv4(154, 63, 46, 159),
|
||||
},
|
||||
}
|
||||
|
||||
service := Service{
|
||||
api: mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": {
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test fetch.
|
||||
records, err := service.List("example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, records)
|
||||
}
|
||||
|
||||
func TestDigitalOceanService_Update(t *testing.T) {
|
||||
mockApi := mock{
|
||||
t: t,
|
||||
records_by_type: map[string][]godo.DomainRecord{
|
||||
"example.com": {
|
||||
{
|
||||
ID: 1337,
|
||||
Name: "www",
|
||||
Data: "80.17.42.157",
|
||||
Priority: 10,
|
||||
TTL: 360,
|
||||
Port: 22,
|
||||
Weight: 100,
|
||||
Flags: 0xf1,
|
||||
Tag: "some_tag",
|
||||
},
|
||||
},
|
||||
},
|
||||
edit_record_request: &godo.DomainRecordEditRequest{
|
||||
Data: "221.135.170.186",
|
||||
},
|
||||
}
|
||||
|
||||
service := Service{
|
||||
api: &mockApi,
|
||||
}
|
||||
|
||||
err := service.Update("example.com", "1337", net.IPv4(221, 135, 170, 186).String())
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockApi.edit_record_error = errors.New("Error")
|
||||
err = service.Update("invalid.com", "1340", net.IPv4(72, 82, 118, 186).String())
|
||||
assert.Error(t, err)
|
||||
|
||||
err = service.Update("example.com", "1337", net.IPv4(221, 135, 170, 186).String())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
49
dns/service/manager.go
Normal file
49
dns/service/manager.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dnsupdater/dns/service/digitalocean"
|
||||
"dnsupdater/dns/service/vultr"
|
||||
)
|
||||
|
||||
var factories = map[string]Factory{
|
||||
"digitalocean": digitalocean.Factory,
|
||||
"vultr": vultr.Factory,
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
services map[string]Service
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
services: make(map[string]Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Manager) Get(name string) Service {
|
||||
if service, ok := m.services[name]; ok {
|
||||
return service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) RegisterFromConfig(providers map[string]map[string]any) 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.(Service))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) Register(name string, provider Service) {
|
||||
m.services[name] = provider
|
||||
}
|
||||
10
dns/service/service.go
Normal file
10
dns/service/service.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package service
|
||||
|
||||
import "dnsupdater/dns"
|
||||
|
||||
type Service interface {
|
||||
List(domain string) (dns.RecordList, error)
|
||||
Update(domain, recordID, ip string) error
|
||||
}
|
||||
|
||||
type Factory func(map[string]any) (any, error)
|
||||
44
dns/service/vultr/mock_test.go
Normal file
44
dns/service/vultr/mock_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/vultr/govultr/v3"
|
||||
)
|
||||
|
||||
type mock struct {
|
||||
t *testing.T
|
||||
|
||||
ListReturn map[string][]govultr.DomainRecord
|
||||
updateError error
|
||||
}
|
||||
|
||||
func (m mock) Create(ctx context.Context, domain string, domainRecordReq *govultr.DomainRecordReq) (*govultr.DomainRecord, *http.Response, error) {
|
||||
m.t.Error("Create called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) List(ctx context.Context, domain string, options *govultr.ListOptions) ([]govultr.DomainRecord, *govultr.Meta, *http.Response, error) {
|
||||
records, ok := m.ListReturn[domain]
|
||||
if !ok {
|
||||
return nil, nil, nil, errors.New("not found")
|
||||
}
|
||||
return records, nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Get(ctx context.Context, domain, recordID string) (*govultr.DomainRecord, *http.Response, error) {
|
||||
m.t.Error("Get called when it should not have been")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m mock) Update(ctx context.Context, domain, recordID string, domainRecordReq *govultr.DomainRecordReq) error {
|
||||
return m.updateError
|
||||
}
|
||||
|
||||
func (m mock) Delete(ctx context.Context, domain, recordID string) error {
|
||||
m.t.Error("Delete called when it should not have been")
|
||||
return nil
|
||||
}
|
||||
69
dns/service/vultr/service.go
Normal file
69
dns/service/vultr/service.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"dnsupdater/dns"
|
||||
|
||||
"github.com/vultr/govultr/v3"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
api govultr.DomainRecordService
|
||||
}
|
||||
|
||||
func New(token string) Service {
|
||||
ctx := context.Background()
|
||||
config := &oauth2.Config{}
|
||||
ts := config.TokenSource(ctx, &oauth2.Token{AccessToken: token})
|
||||
client := govultr.NewClient(oauth2.NewClient(ctx, ts))
|
||||
|
||||
return Service{
|
||||
api: client.DomainRecord,
|
||||
}
|
||||
}
|
||||
|
||||
func Factory(args map[string]any) (any, 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 (p Service) List(domain_name string) (dns.RecordList, error) {
|
||||
fetchedRecords, _, _, err := p.api.List(context.Background(), domain_name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := dns.RecordList{}
|
||||
for _, record := range fetchedRecords {
|
||||
|
||||
if record.Type != "A" {
|
||||
continue
|
||||
}
|
||||
|
||||
records.Add(dns.Record{
|
||||
Id: record.ID,
|
||||
Name: record.Name,
|
||||
Ip: net.ParseIP(record.Data),
|
||||
})
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (p Service) Update(domain, recordID, ip string) error {
|
||||
return p.api.Update(context.Background(), domain, recordID, &govultr.DomainRecordReq{
|
||||
Data: ip,
|
||||
})
|
||||
}
|
||||
96
dns/service/vultr/service_test.go
Normal file
96
dns/service/vultr/service_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package vultr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"dnsupdater/dns"
|
||||
|
||||
"github.com/vultr/govultr/v3"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVultrService_List(t *testing.T) {
|
||||
expected := dns.RecordList{
|
||||
{
|
||||
Id: "656939ee-f942-4ce2-af1d-3bd68c764e96",
|
||||
Name: "sub1",
|
||||
Ip: net.IPv4(201, 110, 66, 72),
|
||||
},
|
||||
{
|
||||
Id: "c80118f4-f04c-4ad2-8ec2-16eb15cc8aca",
|
||||
Name: "sub2",
|
||||
Ip: net.IPv4(242, 124, 218, 187),
|
||||
},
|
||||
}
|
||||
|
||||
service := Service{
|
||||
api: mock{
|
||||
t: t,
|
||||
ListReturn: map[string][]govultr.DomainRecord{
|
||||
"example.com": {
|
||||
{
|
||||
ID: "656939ee-f942-4ce2-af1d-3bd68c764e96",
|
||||
Type: "A",
|
||||
Name: "sub1",
|
||||
Data: "201.110.66.72",
|
||||
Priority: 2,
|
||||
TTL: 1800,
|
||||
},
|
||||
{
|
||||
ID: "c80118f4-f04c-4ad2-8ec2-16eb15cc8aca",
|
||||
Type: "A",
|
||||
Name: "sub2",
|
||||
Data: "242.124.218.187",
|
||||
Priority: 1,
|
||||
TTL: 1800,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
records, err := service.List("example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
|
||||
// Fetch invalid
|
||||
_, err = service.List("noexists.com")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVultrService_Update(t *testing.T) {
|
||||
mockApi := mock{
|
||||
t: t,
|
||||
ListReturn: map[string][]govultr.DomainRecord{
|
||||
"example.com": {
|
||||
{
|
||||
ID: "6cabe6ba-1ea1-405d-b66d-cd56ecac45ce",
|
||||
Type: "A",
|
||||
Name: "www",
|
||||
Data: "80.17.42.157",
|
||||
Priority: 10,
|
||||
TTL: 360,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service := Service{
|
||||
api: &mockApi,
|
||||
}
|
||||
|
||||
err := service.Update("example.com", "6cabe6ba-1ea1-405d-b66d-cd56ecac45ce", net.IPv4(221, 135, 170, 186).String())
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockApi.updateError = errors.New("Error")
|
||||
|
||||
err = service.Update("invalid.com", "332b40fc-0ddf-436c-a0c7-46586b928ac2", net.IPv4(72, 82, 118, 186).String())
|
||||
assert.Error(t, err)
|
||||
|
||||
err = service.Update("example.com", "6cabe6ba-1ea1-405d-b66d-cd56ecac45ce", net.IPv4(221, 135, 170, 186).String())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue