mirror of
https://gitlab.com/pnx-tools/dns-updater.git
synced 2026-06-16 05:54:56 +02:00
adding vultr provider
This commit is contained in:
parent
0c347312bd
commit
6b8c340dcf
7 changed files with 407 additions and 3 deletions
|
|
@ -5,6 +5,8 @@ services:
|
|||
providers:
|
||||
digitalocean:
|
||||
token: xxxx
|
||||
vultr:
|
||||
token: xxxx
|
||||
|
||||
updates:
|
||||
digitalocean:
|
||||
|
|
@ -15,4 +17,10 @@ updates:
|
|||
www: wan
|
||||
mail: wan
|
||||
static: 84.24.254.21
|
||||
vultr:
|
||||
example1.com:
|
||||
www: wan
|
||||
example2.com:
|
||||
www: wan
|
||||
ftp: 88.212.99.90
|
||||
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -1,24 +1,28 @@
|
|||
module dnsupdater
|
||||
|
||||
go 1.19
|
||||
go 1.23
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/digitalocean/godo v1.99.0
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/vultr/govultr/v3 v3.24.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/digitalocean/godo v1.99.0 h1:gUHO7n9bDaZFWvbzOum4bXE0/09ZuYA9yA8idQHX57E=
|
||||
github.com/digitalocean/godo v1.99.0/go.mod h1:SsS2oXo2rznfM/nORlZ/6JaUJZFhmKTib1YhopUc8NA=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
|
|
@ -15,6 +17,12 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
|
|
@ -35,6 +43,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
44
provider/vultr/mock_test.go
Normal file
44
provider/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
|
||||
}
|
||||
95
provider/vultr/provider.go
Normal file
95
provider/vultr/provider.go
Normal 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
|
||||
}
|
||||
241
provider/vultr/provider_test.go
Normal file
241
provider/vultr/provider_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue