Refactor: Moved all under internal.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

FEAT: Directadmin provider is now working
This commit is contained in:
2024-03-18 09:55:01 +13:00
parent a52034216b
commit e1bb5adf36
19 changed files with 163 additions and 73 deletions

View File

@@ -12,7 +12,7 @@ This simple updater do the job, and send notifications, if change happen.
Before run, you need configure this environment variables.
- `CLOUDFLARE_DNS_TO_CHECK` - (required) dns records that will be automatically checked and modified based on the current public IP address. Multiple entries are separated by commas. For example: `domain.com,sub1.domain.com,sub2.domain.com`
- `DNS_NAMES` - (required) dns records that will be automatically checked and modified based on the current public IP address. Multiple entries are separated by commas. For example: `domain.com,sub1.domain.com,sub2.domain.com`
- `CLOUDFLARE_API_KEY` - (required) your cloudflare api key, with access rights to edit selected domains. See: [https://developers.cloudflare.com/fundamentals/api/get-started/create-token/](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
- `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones)
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
@@ -31,7 +31,7 @@ Request body example:
"new_ip": "xxx.xxx.xxx.xxx",
"checked_at": "2023-05-04T17:39:42.942850354+02:00",
"resolver_tag": "ifconfig.me",
"domain": "my.domain.com"
"domain": "my.domain.com",
"token": "a-webhook-token"
}
```
@@ -58,24 +58,42 @@ go install github.com/mkelcik/cloudflare-ddns-update
CLOUDFLARE_DNS_TO_CHECK="domain.com" CLOUDFLARE_API_KEY="my_key" CLOUDFLARE_ZONE="domain.com" cloudflare-ddns-update
```
### Via `docker-compose`
### Via `docker-compose for Cloudflare`
```yaml
version: "3"
services:
cf-dns-updater:
image: mkelcik/cloudflare-ddns-update:latest
image: hub.cybercinch.nz/cybercinch/ddns-update:latest
restart: unless-stopped
environment:
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
- DNS_NAMES=my.testdomain.com,your.testdomain.com
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
- CLOUDFLARE_ZONE=testdomain.com
- ON_CHANGE_COMMENT="automatically updated"
- CHECK_INTERVAL_SECONDS=300
# Optional if your webhook receiver requires a token for verification
# - WEBHOOK_TOKEN="SomeSup3rs3cureT0k3n"
```
### Via `docker-compose for DirectAdmin`
```yaml
version: "3"
services:
cf-dns-updater:
image: hub.cybercinch.nz/cybercinch/ddns-update:latest
restart: unless-stopped
environment:
- DNS_PROVIDER=directadmin
- DA_USER=some-directadmin-username
- DA_KEY=your-directadmin-password-or-login-key
- DA_URL=https://your.daserver.com:2222
- DNS_NAMES=my.testdomain.com,your.testdomain.com
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
- CHECK_INTERVAL_SECONDS=300
# Optional if your webhook receiver requires a token for verification
# - WEBHOOK_TOKEN="SomeSup3rs3cureT0k3n"
```
### Via `docker run`
```shell
docker run -e CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com -e CLOUDFLARE_API_KEY=your_cloudflare_api_key -e CLOUDFLARE_ZONE=testdomain.com -e ON_CHANGE_COMMENT="automatically updated" -e CHECK_INTERVAL_SECONDS=300 mkelcik/cloudflare-ddns-update:latest

10
go.mod
View File

@@ -1,18 +1,20 @@
module github.com/mkelcik/cloudflare-ddns-update
module hub.cybercinch.nz/cybercinch/ddns-update
go 1.22
toolchain go1.22.1
require github.com/cloudflare/cloudflare-go v0.83.0
require (
github.com/cloudflare/cloudflare-go v0.83.0
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3
)
require (
github.com/goccy/go-json v0.10.2 // 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.5 // indirect
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3 // indirect
github.com/mehrdadep/dex v1.0.2 // indirect
github.com/spf13/cast v1.6.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect

14
go.sum
View File

@@ -4,11 +4,13 @@ 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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
@@ -18,16 +20,20 @@ github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXc
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3 h1:6kGC8EpZlYTvNI2ABYX87bBtgzfPrtZV8CKqLtSvyek=
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3/go.mod h1:FJ/EtEMwe3k2ABVYLXmE/K+H8cHVpVaSgihVT2XbSkc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mehrdadep/dex v1.0.2 h1:4rUs5xbcODC107BM6cICsfMF3qBAEqKWoZaqXClMvoo=
github.com/mehrdadep/dex v1.0.2/go.mod h1:Vg+AUvtXkTFfNTYtQiY+NTvV3Bvyz7oiT0asmU4z440=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

View File

@@ -12,11 +12,13 @@ const (
defaultCheckInterval = 5 * 60
envKeyDnsToCheck = "DNS_NAMES"
envKeyPublicIpResolverTag = "PUBLIC_IP_RESOLVER"
envKeyDNSProviderTag = "DNS_PROVIDER"
envKeyPublicDNSServer = "PUBLIC_DNS_SERVER"
envKeyCloudflareApiKey = "CLOUDFLARE_API_KEY"
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
envKeyDirectadminUser = "DA_USER"
envKeyDirectadminKey = "DA_LOGIN_KEY"
envKeyDirectadminUrl = "DA_LOGIN_URL"
envKeyDirectadminKey = "DA_KEY"
envKeyDirectadminUrl = "DA_URL"
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
envKeyNotifiers = "NOTIFIERS"
@@ -25,6 +27,7 @@ const (
type Config struct {
DnsRecordsToCheck []string
PublicIpResolverTag string
PublicDNSServer string
DNSProviderTag string
DirectadminUsername string
DirectadminKey string
@@ -32,7 +35,7 @@ type Config struct {
ApiToken string
WebhookToken string
CloudflareZone string
OnChangeComment string
CloudflareOnChangeComment string
Notifiers []string
CheckInterval time.Duration
}
@@ -44,21 +47,17 @@ func (c Config) Validate() error {
return fmt.Errorf("empty api token env key %s", envKeyCloudflareApiKey)
}
// if c.CloudflareZone == "" {
// return fmt.Errorf("empty zone in env key %s", envKeyCloudflareZone)
// }
case "directadmin":
if c.DirectadminUrl == "" {
return fmt.Errorf("empty DirectAdmin URL env key %s", envKeyDirectadminUrl)
}
if c.DirectadminUsername == "" {
return fmt.Errorf("empty Username in env key %s", envKeyDirectadminUser)
return fmt.Errorf("empty DirectAdmin Username in env key %s", envKeyDirectadminUser)
}
if c.DirectadminKey == "" {
return fmt.Errorf("empty Login Key in env key %s", envKeyDirectadminKey)
return fmt.Errorf("empty DirectAdmin Login Key in env key %s", envKeyDirectadminKey)
}
}
@@ -78,15 +77,25 @@ func NewConfig() Config {
return Config{
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
DNSProviderTag: getEnvDefault(envKeyDNSProviderTag, "cloudflare"),
PublicDNSServer: getEnvDefault(envKeyPublicDNSServer, "1.1.1.1"),
PublicIpResolverTag: getEnvDefault(envKeyPublicIpResolverTag, "icanhazip"),
ApiToken: os.Getenv(envKeyCloudflareApiKey),
CloudflareZone: os.Getenv(envKeyCloudflareZone),
CloudflareOnChangeComment: os.Getenv(envKeyOnChangeComment),
DirectadminUsername: os.Getenv(envKeyDirectadminUser),
DirectadminKey: os.Getenv(envKeyDirectadminKey),
DirectadminUrl: os.Getenv(envKeyDirectadminUrl),
CloudflareZone: os.Getenv(envKeyCloudflareZone),
OnChangeComment: os.Getenv(envKeyOnChangeComment),
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
CheckInterval: time.Duration(checkInterval) * time.Second,
WebhookToken: os.Getenv("WEBHOOK_TOKEN"),
}
}
func getEnvDefault(key, fallback string) string {
value, exists := os.LookupEnv(key)
if !exists {
value = fallback
}
return value
}

View File

@@ -63,7 +63,7 @@ func TestConfig_Validate(t *testing.T) {
PublicIpResolverTag: tt.fields.PublicIpResolverTag,
ApiToken: tt.fields.ApiToken,
CloudflareZone: tt.fields.CloudflareZone,
OnChangeComment: tt.fields.OnChangeComment,
CloudflareOnChangeComment: tt.fields.OnChangeComment,
CheckInterval: tt.fields.CheckInterval,
}
if err := c.Validate(); (err != nil) != tt.wantErr {

View File

@@ -5,7 +5,8 @@ import (
)
type DNSProvider interface {
UpdateRecord(hostname string, ip string, old_ip string) error
UpdateRecord(hostname string, ip string) error
FetchRecord(hostname string) (string, error)
}
type DomainParts struct {
@@ -16,9 +17,16 @@ type DomainParts struct {
func GetDomainParts(hostname string) *DomainParts {
data := arrayToSlice(strings.Split(hostname, "."))
if len(data) <= 2 {
// This might be the actual root domain
out := DomainParts{Name: strings.Join(data[0:], "."), Domain: strings.Join(data[0:], ".")}
return &out
} else {
// This is a subdomain
out := DomainParts{Name: data[0], Domain: strings.Join(data[1:], ".")}
return &out
}
}
func arrayToSlice(array []string) []string {
return array[:]

View File

@@ -5,7 +5,7 @@ import (
"log"
"github.com/cloudflare/cloudflare-go"
"github.com/mkelcik/cloudflare-ddns-update/internal"
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
)
const (
@@ -18,7 +18,7 @@ type CloudflareProvider struct {
Config internal.Config
}
func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip string) error {
func (d *CloudflareProvider) UpdateRecord(hostname string, ip string) error {
// old_ip is not required for Cloudflare updates
domain_parts := GetDomainParts(hostname)
zoneId := d.FindZoneIdByName(domain_parts.Domain)
@@ -33,8 +33,8 @@ func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip str
Content: ip,
}
if d.Config.OnChangeComment != "" {
update.Comment = &d.Config.OnChangeComment
if d.Config.CloudflareOnChangeComment != "" {
update.Comment = &d.Config.CloudflareOnChangeComment
}
_, err = d.Client.UpdateDNSRecord(d.Context, cloudflare.ZoneIdentifier(zoneId), update)
@@ -44,6 +44,13 @@ func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip str
return nil
}
func (d *CloudflareProvider) FetchRecord(hostname string) (string, error) {
domain_parts := GetDomainParts(hostname)
zoneId := d.FindZoneIdByName(domain_parts.Domain)
dnsRecord, err := d.GetDnsRecord(hostname, zoneId)
return dnsRecord[0].Content, err
}
func (d *CloudflareProvider) NewClient(api_token string) {
api, err := cloudflare.NewWithAPIToken(api_token)
if err != nil {

View File

@@ -2,10 +2,12 @@ package dns_providers
import (
"context"
"fmt"
"log"
"time"
"github.com/levelzerotechnology/directadmin-go"
"github.com/mkelcik/cloudflare-ddns-update/internal"
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
)
const (
@@ -18,20 +20,54 @@ type Directadmin struct {
Config internal.Config
}
func (d *Directadmin) UpdateRecord(hostname string, ip string, old_ip string) error {
type ListDNSRecordsParams struct {
Name string
}
func (d *Directadmin) FetchRecord(hostname string) (string, error) {
dnsRecord, err := d.GetDnsRecord(hostname)
if err != nil {
log.Fatal("unable to retrieve DNS record")
return "", err
}
return dnsRecord[0].Value, nil
}
func (d *Directadmin) UpdateRecord(hostname string, ip string) error {
result := GetDomainParts(hostname)
current_record, _ := d.GetDnsRecord(hostname)
a := directadmin.DNSRecord{Name: result.Name, Ttl: 300, Type: "A", Value: old_ip}
b := directadmin.DNSRecord{Name: result.Name, Ttl: 300, Type: "A", Value: ip}
// Create an updated record for the new ip
new_record := directadmin.DNSRecord{Name: current_record[0].Name, Ttl: current_record[0].Ttl, Type: current_record[0].Type, Value: ip}
err := d.Client.UpdateDNSRecord(result.Domain, a, b)
err := d.Client.UpdateDNSRecord(result.Domain, current_record[0], new_record)
if err != nil {
return err
}
return nil
}
func (d *Directadmin) GetDnsRecord(hostname string) ([]directadmin.DNSRecord, error) {
domainParts := GetDomainParts(hostname)
dnsRecords, err := d.Client.GetDNSRecords(domainParts.Domain)
var slice []directadmin.DNSRecord
for _, dnsRecord := range dnsRecords {
if domainParts.Name == dnsRecord.Name {
slice = append(slice, dnsRecord)
}
}
if len(slice) == 0 {
return nil, fmt.Errorf("unable to find DNS record for %s", hostname)
}
return slice, err
}
func (d *Directadmin) NewClient(server_url string, username string, key string) {
api, err := directadmin.New(server_url, 5*time.Second, false, false)
if err != nil {

View File

@@ -1,6 +1,8 @@
package internal
import "strings"
import (
"strings"
)
func parseCommaDelimited(data string) []string {
out := make([]string, 0, strings.Count(data, ",")+1)

20
main.go
View File

@@ -8,11 +8,10 @@ import (
"syscall"
"time"
"github.com/mkelcik/cloudflare-ddns-update/internal"
"github.com/mkelcik/cloudflare-ddns-update/internal/dns_providers"
"github.com/mkelcik/cloudflare-ddns-update/internal/dns_resolver"
"github.com/mkelcik/cloudflare-ddns-update/notifications"
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
"hub.cybercinch.nz/cybercinch/ddns-update/internal/dns_providers"
"hub.cybercinch.nz/cybercinch/ddns-update/internal/notifications"
"hub.cybercinch.nz/cybercinch/ddns-update/internal/public_resolvers"
)
type PublicIpResolver interface {
@@ -75,15 +74,18 @@ func main() {
}
for _, dnsRecord := range config.DnsRecordsToCheck {
current_dns_record := dns_resolver.ResolveHostname(dnsRecord, "1.1.1.1")
current_dns_record, err := dnsProvider.FetchRecord(dnsRecord)
if err != nil {
log.Fatalf("Failed to fetch DNS record: %s", err)
}
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord, current_dns_record.String())
if currentPublicIP.String() == current_dns_record.String() {
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord, current_dns_record)
if currentPublicIP.String() == current_dns_record {
log.Println("OK")
continue // no update needed
}
if err := dnsProvider.UpdateRecord(dnsRecord, currentPublicIP.String(), current_dns_record.String()); err != nil {
if err := dnsProvider.UpdateRecord(dnsRecord, currentPublicIP.String()); err != nil {
log.Printf("error updating dns record: %s", err)
continue
}