From 555c553686401eeda44f415cb15e42bf7fd434b7 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Mon, 13 Oct 2025 16:58:42 +0200 Subject: [PATCH] refactor --- app/app.go | 24 +- app/updater.go | 45 +++ cmd/dnsupdater/main.go | 18 +- dns/cache.go | 28 ++ dns/record.go | 26 ++ .../service}/digitalocean/mock_test.go | 6 +- dns/service/digitalocean/service.go | 67 +++++ dns/service/digitalocean/service_test.go | 103 +++++++ {provider/manager => dns/service}/manager.go | 23 +- dns/service/service.go | 10 + {provider => dns/service}/vultr/mock_test.go | 0 dns/service/vultr/service.go | 69 +++++ dns/service/vultr/service_test.go | 96 +++++++ provider/digitalocean/provider.go | 98 ------- provider/digitalocean/provider_test.go | 263 ------------------ provider/interface.go | 11 - provider/vultr/provider.go | 95 ------- provider/vultr/provider_test.go | 241 ---------------- 18 files changed, 481 insertions(+), 742 deletions(-) create mode 100644 app/updater.go create mode 100644 dns/cache.go create mode 100644 dns/record.go rename {provider => dns/service}/digitalocean/mock_test.go (93%) create mode 100644 dns/service/digitalocean/service.go create mode 100644 dns/service/digitalocean/service_test.go rename {provider/manager => dns/service}/manager.go (52%) create mode 100644 dns/service/service.go rename {provider => dns/service}/vultr/mock_test.go (100%) create mode 100644 dns/service/vultr/service.go create mode 100644 dns/service/vultr/service_test.go delete mode 100644 provider/digitalocean/provider.go delete mode 100644 provider/digitalocean/provider_test.go delete mode 100644 provider/interface.go delete mode 100644 provider/vultr/provider.go delete mode 100644 provider/vultr/provider_test.go diff --git a/app/app.go b/app/app.go index 1fc5b19..41a9cb5 100644 --- a/app/app.go +++ b/app/app.go @@ -6,13 +6,12 @@ import ( "net" "time" - "dnsupdater/provider/manager" - + dnsservice "dnsupdater/dns/service" "dnsupdater/ip" "dnsupdater/ip/resolver" ) -// Constant name for the virtual WAN interface +// WAN_IFACE Name for the virtual WAN interface const WAN_IFACE = "wan" type App struct { @@ -20,8 +19,8 @@ type App struct { cacheDefaultCallback ip.CacheDefaultCallback - // Updater manager - ProviderManager *manager.Manager + // DNS service manager + DnsServiceMgr *dnsservice.Manager } func makeCacheCallback(service resolver.Service) ip.CacheDefaultCallback { @@ -36,23 +35,22 @@ func makeCacheCallback(service resolver.Service) ip.CacheDefaultCallback { } func NewApp(config *Config) (*App, error) { - providerMgr := manager.New() - // providerMgr.Register("digitalocean", digitalocean.New(config.Services.DigitalOcean.Token)) - err := providerMgr.RegisterFromConfig(config.Providers) + dnsServiceMgr := dnsservice.NewManager() + err := dnsServiceMgr.RegisterFromConfig(config.Providers) if err != nil { return nil, err } - service := resolver.Get(config.Services.IPLookup) + ipService := resolver.Get(config.Services.IPLookup) - if service == nil { - return nil, fmt.Errorf("Failed to load lookup service: %s", config.Services.IPLookup) + if ipService == nil { + return nil, fmt.Errorf("failed to load lookup service: %s", config.Services.IPLookup) } return &App{ - ProviderManager: providerMgr, + DnsServiceMgr: dnsServiceMgr, cache: ip.NewCache(), - cacheDefaultCallback: makeCacheCallback(service), + cacheDefaultCallback: makeCacheCallback(ipService), }, nil } diff --git a/app/updater.go b/app/updater.go new file mode 100644 index 0000000..28b2e2a --- /dev/null +++ b/app/updater.go @@ -0,0 +1,45 @@ +package app + +import ( + "fmt" + "net" + + "dnsupdater/dns" + "dnsupdater/dns/service" +) + +type Updater struct { + service service.Service + // cache map[string][]dns.Record + cache dns.DomainRecordCache +} + +func NewUpdater(service service.Service) *Updater { + return &Updater{ + service: service, + cache: *dns.NewDomainRecordCache(service.List), + } +} + +func (u Updater) find(domain string, record_name string) (*dns.Record, error) { + records, err := u.cache.Get(domain) + if err != nil { + return nil, err + } + if rec, found := records.FindByName(record_name); found { + return &rec, nil + } + return nil, fmt.Errorf("could not find record %s", record_name) +} + +func (u Updater) Update(domain, record string, ip net.IP) error { + var err error + var r *dns.Record + if r, err = u.find(domain, record); err == nil { + // Update if needed. + if r.Ip == nil || !r.Ip.Equal(ip) { + return u.service.Update(domain, r.Id, ip.String()) + } + } + return err +} diff --git a/cmd/dnsupdater/main.go b/cmd/dnsupdater/main.go index fcec2bd..c3bb9c1 100644 --- a/cmd/dnsupdater/main.go +++ b/cmd/dnsupdater/main.go @@ -5,7 +5,7 @@ import ( "os" "time" - "dnsupdater/app" + App "dnsupdater/app" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -21,28 +21,30 @@ func main() { TimeFormat: time.RFC3339, }) - config, err := app.LoadConfig(*configFile) + config, err := App.LoadConfig(*configFile) if err != nil { log.Fatal().Err(err).Str("file", *configFile).Msg("Failed to load config") } - app, err := app.NewApp(config) + app, err := App.NewApp(config) if err != nil { log.Fatal().Err(err).Msg("Failed to initialize application") } for service_name, domains := range config.Updates { - // Get service - service := app.ProviderManager.Get(service_name) + // Get DNS Service + dnsService := app.DnsServiceMgr.Get(service_name) - if service == nil { - log.Warn().Str("service", service_name).Msg("Invalid service") + if dnsService == nil { + log.Warn().Str("service", service_name).Msg("Invalid DNS service") continue } log.Info().Str("service", service_name).Msg("Begin update for service") + updater := App.NewUpdater(dnsService) + for domain, records := range domains { for name, data := range records { @@ -61,7 +63,7 @@ func main() { logger = logger.With().IPAddr("ip", ip).Logger() - err = service.Update(domain, name, ip) + err = updater.Update(domain, name, ip) if err != nil { logger.Error().Err(err).Msg("Failed to update record") } else { diff --git a/dns/cache.go b/dns/cache.go new file mode 100644 index 0000000..0f46583 --- /dev/null +++ b/dns/cache.go @@ -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 +} diff --git a/dns/record.go b/dns/record.go new file mode 100644 index 0000000..4a7c57d --- /dev/null +++ b/dns/record.go @@ -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 +} diff --git a/provider/digitalocean/mock_test.go b/dns/service/digitalocean/mock_test.go similarity index 93% rename from provider/digitalocean/mock_test.go rename to dns/service/digitalocean/mock_test.go index 561ff0e..2f64a24 100644 --- a/provider/digitalocean/mock_test.go +++ b/dns/service/digitalocean/mock_test.go @@ -81,6 +81,10 @@ func (m mock) EditRecord(_ context.Context, domain string, id int, req *godo.Dom 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{ @@ -96,7 +100,7 @@ func (m mock) EditRecord(_ context.Context, domain string, id int, req *godo.Dom Tag: req.Tag, } - return &record, nil, m.edit_record_error + return &record, nil, nil } func (m mock) CreateRecord(context.Context, string, *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) { diff --git a/dns/service/digitalocean/service.go b/dns/service/digitalocean/service.go new file mode 100644 index 0000000..e0414be --- /dev/null +++ b/dns/service/digitalocean/service.go @@ -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 +} diff --git a/dns/service/digitalocean/service_test.go b/dns/service/digitalocean/service_test.go new file mode 100644 index 0000000..414a27c --- /dev/null +++ b/dns/service/digitalocean/service_test.go @@ -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) +} diff --git a/provider/manager/manager.go b/dns/service/manager.go similarity index 52% rename from provider/manager/manager.go rename to dns/service/manager.go index 5cd609b..acbf8f4 100644 --- a/provider/manager/manager.go +++ b/dns/service/manager.go @@ -1,36 +1,35 @@ -package manager +package service import ( "fmt" - "dnsupdater/provider" - "dnsupdater/provider/digitalocean" - "dnsupdater/provider/vultr" + "dnsupdater/dns/service/digitalocean" + "dnsupdater/dns/service/vultr" ) -var factories = map[string]provider.ProviderFactory{ +var factories = map[string]Factory{ "digitalocean": digitalocean.Factory, "vultr": vultr.Factory, } type Manager struct { - services map[string]provider.Provider + services map[string]Service } -func New() *Manager { +func NewManager() *Manager { return &Manager{ - services: make(map[string]provider.Provider), + services: make(map[string]Service), } } -func (m Manager) Get(name string) provider.Provider { +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]interface{}) error { +func (m Manager) RegisterFromConfig(providers map[string]map[string]any) error { for name, args := range providers { if factory, ok := factories[name]; ok { @@ -39,12 +38,12 @@ func (m Manager) RegisterFromConfig(providers map[string]map[string]interface{}) return fmt.Errorf("could not create provider '%s': %v", name, err) } - m.Register(name, provider) + m.Register(name, provider.(Service)) } } return nil } -func (m Manager) Register(name string, provider provider.Provider) { +func (m Manager) Register(name string, provider Service) { m.services[name] = provider } diff --git a/dns/service/service.go b/dns/service/service.go new file mode 100644 index 0000000..e76a8e5 --- /dev/null +++ b/dns/service/service.go @@ -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) diff --git a/provider/vultr/mock_test.go b/dns/service/vultr/mock_test.go similarity index 100% rename from provider/vultr/mock_test.go rename to dns/service/vultr/mock_test.go diff --git a/dns/service/vultr/service.go b/dns/service/vultr/service.go new file mode 100644 index 0000000..58f9539 --- /dev/null +++ b/dns/service/vultr/service.go @@ -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, + }) +} diff --git a/dns/service/vultr/service_test.go b/dns/service/vultr/service_test.go new file mode 100644 index 0000000..4dff360 --- /dev/null +++ b/dns/service/vultr/service_test.go @@ -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) +} diff --git a/provider/digitalocean/provider.go b/provider/digitalocean/provider.go deleted file mode 100644 index c218c75..0000000 --- a/provider/digitalocean/provider.go +++ /dev/null @@ -1,98 +0,0 @@ -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]any) (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 -} diff --git a/provider/digitalocean/provider_test.go b/provider/digitalocean/provider_test.go deleted file mode 100644 index bf4a771..0000000 --- a/provider/digitalocean/provider_test.go +++ /dev/null @@ -1,263 +0,0 @@ -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) -} diff --git a/provider/interface.go b/provider/interface.go deleted file mode 100644 index 542f192..0000000 --- a/provider/interface.go +++ /dev/null @@ -1,11 +0,0 @@ -package provider - -import ( - "net" -) - -type Provider interface { - Update(domain string, record string, ip net.IP) error -} - -type ProviderFactory func(map[string]any) (Provider, error) diff --git a/provider/vultr/provider.go b/provider/vultr/provider.go deleted file mode 100644 index bb8753f..0000000 --- a/provider/vultr/provider.go +++ /dev/null @@ -1,95 +0,0 @@ -package vultr - -import ( - "context" - "errors" - "net" - - "dnsupdater/provider" - - "github.com/vultr/govultr/v3" - "golang.org/x/oauth2" -) - -type Provider struct { - service govultr.DomainRecordService - cache map[string][]govultr.DomainRecord -} - -func New(token string) Provider { - ctx := context.Background() - config := &oauth2.Config{} - ts := config.TokenSource(ctx, &oauth2.Token{AccessToken: token}) - client := govultr.NewClient(oauth2.NewClient(ctx, ts)) - - return Provider{ - service: client.DomainRecord, - cache: make(map[string][]govultr.DomainRecord), - } -} - -func Factory(args map[string]any) (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) ([]govultr.DomainRecord, error) { - records, ok := d.cache[domain] - if !ok { - fetchedRecords, _, _, err := d.service.List(context.Background(), domain, nil) - if err != nil { - return nil, err - } - - for _, rec := range fetchedRecords { - if rec.Type != "A" { - continue - } - - records = append(records, rec) - } - d.cache[domain] = records - } - return records, nil -} - -func (d Provider) find(domain, name string) (govultr.DomainRecord, error) { - records, err := d.fetch(domain) - if err != nil { - return govultr.DomainRecord{}, err - } - - for _, rec := range records { - if rec.Name == name { - return rec, nil - } - } - return govultr.DomainRecord{}, errors.New("not found") -} - -func (d Provider) Update(domain string, name string, ip net.IP) error { - ctx := context.Background() - - record, err := d.find(domain, name) - if err != nil { - return err - } - - recordIP := net.ParseIP(record.Data) - if recordIP == nil || !recordIP.Equal(ip) { - updateData := govultr.DomainRecordReq{ - Data: ip.String(), - } - return d.service.Update(ctx, domain, record.ID, &updateData) - } - return nil -} diff --git a/provider/vultr/provider_test.go b/provider/vultr/provider_test.go deleted file mode 100644 index 764cc25..0000000 --- a/provider/vultr/provider_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package vultr - -import ( - "errors" - "net" - "testing" - - "github.com/vultr/govultr/v3" - - "github.com/stretchr/testify/assert" -) - -func TestProvider_fetch(t *testing.T) { - expected := []govultr.DomainRecord{ - { - 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, - }, - } - - provider := Provider{ - service: mock{ - t: t, - ListReturn: map[string][]govultr.DomainRecord{ - "example.com": expected, - }, - }, - cache: make(map[string][]govultr.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 := []govultr.DomainRecord{ - { - ID: "884edc0a-295c-418a-97ac-b7b67d85efc1", - Type: "A", - Name: "sub1", - Data: "107.218.197.189", - Priority: 2, - TTL: 1800, - }, - { - ID: "43608e90-8917-45f9-8300-1a40b13886b7", - Type: "A", - Name: "sub2", - Data: "254.221.12.160", - Priority: 1, - TTL: 1800, - }, - } - - another_com_records := []govultr.DomainRecord{ - { - ID: "43608e90-8917-45f9-8300-1a40b13886b7", - Type: "A", - Name: "box", - Data: "108.151.98.62", - Priority: 2, - TTL: 1800, - }, - { - ID: "43608e90-8917-45f9-8300-1a40b13886b7", - Type: "A", - Name: "ntp", - Data: "190.255.140.208", - Priority: 10, - TTL: 300, - }, - } - - mockService := mock{ - t: t, - ListReturn: map[string][]govultr.DomainRecord{ - "example.com": example_com_records, - "another.com": another_com_records, - }, - } - - provider := Provider{ - service: mockService, - cache: make(map[string][]govultr.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.ListReturn) -} - -func TestProvider_fetch_from_cache(t *testing.T) { - expected := []govultr.DomainRecord{ - { - ID: "43608e90-8917-45f9-8300-1a40b13886b7", - Type: "A", - Name: "sub1", - Data: "42.170.152.94", - Priority: 10, - TTL: 1800, - }, - } - - provider := Provider{ - service: mock{t: t}, - cache: map[string][]govultr.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 := []govultr.DomainRecord{ - { - ID: "7d53d1b4-8264-40f7-b379-536b5caff54c", - Type: "A", - Name: "sub1", - Data: "161.125.137.64", - Priority: 10, - TTL: 1800, - }, - { - ID: "309f6dda-4aef-453d-ae05-71f8274cdd76", - Type: "A", - Name: "sub2", - Data: "154.63.46.159", - Priority: 5, - TTL: 1800, - }, - } - - expected_cache := []govultr.DomainRecord{ - { - ID: "2216c435-0405-49aa-96bc-1d177146ee4e", - Type: "A", - Name: "mail", - Data: "176.151.152.10", - Priority: 10, - TTL: 3600, - }, - } - - provider := Provider{ - service: mock{ - t: t, - ListReturn: map[string][]govultr.DomainRecord{ - "example.com": expected, - }, - }, - cache: map[string][]govultr.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 := []govultr.DomainRecord{ - { - ID: "6cabe6ba-1ea1-405d-b66d-cd56ecac45ce", - Type: "A", - Name: "www", - Data: "80.17.42.157", - Priority: 10, - TTL: 360, - }, - } - - mockService := mock{ - t: t, - ListReturn: map[string][]govultr.DomainRecord{ - "example.com": expected, - }, - } - - provider := Provider{ - service: &mockService, - cache: map[string][]govultr.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.updateError = errors.New("Error") - - err = provider.Update("example.com", "www", net.IPv4(221, 135, 170, 186)) - assert.Error(t, err) -}