1
0
Fork 0

adding vultr provider

This commit is contained in:
Henrik Hautakoski 2025-10-12 23:55:53 +02:00
parent 0c347312bd
commit 6b8c340dcf
7 changed files with 407 additions and 3 deletions

View file

@ -5,10 +5,12 @@ import (
"dnsupdater/provider"
"dnsupdater/provider/digitalocean"
"dnsupdater/provider/vultr"
)
var factories = map[string]provider.ProviderFactory{
"digitalocean": digitalocean.Factory,
"vultr": vultr.Factory,
}
type Manager struct {

View 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
}

View file

@ -0,0 +1,95 @@
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
}

View file

@ -0,0 +1,241 @@
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)
}