Merge branch 'main' into refactor-cicd-release-epends-on-test
All checks were successful
CI / test (pull_request) Successful in 1m39s
All checks were successful
CI / test (pull_request) Successful in 1m39s
This commit is contained in:
23
README.md
23
README.md
@@ -23,6 +23,12 @@ dns_provider:
|
|||||||
config:
|
config:
|
||||||
api_key: <your-api-key>
|
api_key: <your-api-key>
|
||||||
base_url: https://api.hosting.ionos.com/dns
|
base_url: https://api.hosting.ionos.com/dns
|
||||||
|
notification_provider:
|
||||||
|
type: gotify
|
||||||
|
config:
|
||||||
|
url: <your-gotify-host>
|
||||||
|
token: <your-token>
|
||||||
|
priority: 0
|
||||||
domains:
|
domains:
|
||||||
- tld: example.com
|
- tld: example.com
|
||||||
subdomains:
|
subdomains:
|
||||||
@@ -53,3 +59,20 @@ url: <your-providers-URL>
|
|||||||
Examples for providers are:
|
Examples for providers are:
|
||||||
- https://ifconfig.me
|
- https://ifconfig.me
|
||||||
- https://api.ipify.org
|
- https://api.ipify.org
|
||||||
|
|
||||||
|
## Notification Providers
|
||||||
|
The notification provider is used to send notifications whena IP address changes and a DNS record is updated.
|
||||||
|
|
||||||
|
### Console
|
||||||
|
The console notification provider is used to print the notification to the console. This is the default notification provider.
|
||||||
|
|
||||||
|
### Gotify
|
||||||
|
The Gotify notification provider is used to send notifications to a Gotify server.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: <your-gotify-host>
|
||||||
|
token: <your-token>
|
||||||
|
priority: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The priority must be between 0 and 4.
|
||||||
@@ -8,6 +8,12 @@ dns_provider:
|
|||||||
config:
|
config:
|
||||||
api_key: <your-api-key>
|
api_key: <your-api-key>
|
||||||
base_url: https://api.hosting.ionos.com/dns
|
base_url: https://api.hosting.ionos.com/dns
|
||||||
|
notification_provider:
|
||||||
|
type: gotify
|
||||||
|
config:
|
||||||
|
url: <your-gotify-host>
|
||||||
|
token: <your-token>
|
||||||
|
priority: 0
|
||||||
domains:
|
domains:
|
||||||
- tld: example.com
|
- tld: example.com
|
||||||
subdomains:
|
subdomains:
|
||||||
|
|||||||
26
main.go
26
main.go
@@ -2,13 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"realdnydns/pkg/changeDetector"
|
"realdnydns/pkg/changeDetector"
|
||||||
"realdnydns/pkg/config"
|
"realdnydns/pkg/config"
|
||||||
"realdnydns/pkg/dnsProvider"
|
"realdnydns/pkg/dnsProvider"
|
||||||
ionos "realdnydns/pkg/dnsProvider/ionos"
|
ionos "realdnydns/pkg/dnsProvider/ionos"
|
||||||
"realdnydns/pkg/externalIpProvider"
|
"realdnydns/pkg/externalIpProvider"
|
||||||
plainExternalIpProvider "realdnydns/pkg/externalIpProvider/plain"
|
plainExternalIpProvider "realdnydns/pkg/externalIpProvider/plain"
|
||||||
"time"
|
"realdnydns/pkg/notificationProvider"
|
||||||
|
notificationProviderConsole "realdnydns/pkg/notificationProvider/console"
|
||||||
|
gotify "realdnydns/pkg/notificationProvider/gotify"
|
||||||
|
|
||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron"
|
||||||
)
|
)
|
||||||
@@ -50,7 +54,25 @@ func main() {
|
|||||||
panic(fmt.Errorf("unknown DNS provider: %s", configClient.DNSProvider.Type))
|
panic(fmt.Errorf("unknown DNS provider: %s", configClient.DNSProvider.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDetector := changeDetector.New(externalIpProvider, dnsProvider, configClient.Domains)
|
var notificationProvider notificationProvider.NotificationProvider
|
||||||
|
switch configClient.NotificationProvider.Type {
|
||||||
|
case "gotify":
|
||||||
|
var gotifyConfig gotify.NotificationProviderImplGotifyConfig
|
||||||
|
err := configClient.NotificationProvider.ProviderConfig.Decode(&gotifyConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationProvider, err = gotify.New(gotifyConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Use default console notification provider
|
||||||
|
notificationProvider = notificationProviderConsole.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDetector := changeDetector.New(externalIpProvider, dnsProvider, notificationProvider, configClient.Domains)
|
||||||
|
|
||||||
s := gocron.NewScheduler(time.UTC)
|
s := gocron.NewScheduler(time.UTC)
|
||||||
s.SingletonMode()
|
s.SingletonMode()
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
package changeDetector
|
package changeDetector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"realdnydns/pkg/config"
|
"realdnydns/pkg/config"
|
||||||
"realdnydns/pkg/dnsProvider"
|
"realdnydns/pkg/dnsProvider"
|
||||||
"realdnydns/pkg/externalIpProvider"
|
"realdnydns/pkg/externalIpProvider"
|
||||||
|
"realdnydns/pkg/notificationProvider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeDetector struct {
|
type ChangeDetector struct {
|
||||||
externalIpProvider externalIpProvider.ExternalIpProvider
|
externalIpProvider externalIpProvider.ExternalIpProvider
|
||||||
dnsProvider dnsProvider.DNSProvider
|
dnsProvider dnsProvider.DNSProvider
|
||||||
domains []config.DomainConfig
|
notificationProvider notificationProvider.NotificationProvider
|
||||||
|
domains []config.DomainConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
externalIpProvider externalIpProvider.ExternalIpProvider,
|
externalIpProvider externalIpProvider.ExternalIpProvider,
|
||||||
dnsProvider dnsProvider.DNSProvider,
|
dnsProvider dnsProvider.DNSProvider,
|
||||||
|
notificationProvider notificationProvider.NotificationProvider,
|
||||||
domains []config.DomainConfig,
|
domains []config.DomainConfig,
|
||||||
) ChangeDetector {
|
) ChangeDetector {
|
||||||
return ChangeDetector{
|
return ChangeDetector{
|
||||||
externalIpProvider: externalIpProvider,
|
externalIpProvider: externalIpProvider,
|
||||||
dnsProvider: dnsProvider,
|
dnsProvider: dnsProvider,
|
||||||
domains: domains,
|
notificationProvider: notificationProvider,
|
||||||
|
domains: domains,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +45,14 @@ func (c *ChangeDetector) DetectAndApplyChanges() (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if currentRecord.IP != externalIp.String() {
|
if currentRecord.IP != externalIp.String() {
|
||||||
|
err = c.notificationProvider.SendNotification(
|
||||||
|
fmt.Sprintf("Update %s.%s", subdomain, domain.TLD),
|
||||||
|
fmt.Sprintf("The IP of %s has changed from %s to %s", domain.TLD, currentRecord.IP, externalIp.String()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return numberUpdated, err
|
||||||
|
}
|
||||||
|
|
||||||
_, err = c.dnsProvider.UpdateRecord(domain.TLD, subdomain, externalIp, currentRecord.TTL, currentRecord.Prio, currentRecord.Disabled)
|
_, err = c.dnsProvider.UpdateRecord(domain.TLD, subdomain, externalIp, currentRecord.TTL, currentRecord.Prio, currentRecord.Disabled)
|
||||||
numberUpdated++
|
numberUpdated++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ func (m *MockDNSProviderImpl) UpdateRecord(tld string, subdomain string, ip net.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MockedNotificationProvider struct{}
|
||||||
|
|
||||||
|
type MockedNotificationProviderImpl struct{}
|
||||||
|
|
||||||
|
func (m *MockedNotificationProviderImpl) SendNotification(title string, message string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestDetectAndApplyChanges(t *testing.T) {
|
func TestDetectAndApplyChanges(t *testing.T) {
|
||||||
t.Run("with changes", testDetectAndApplyChangesWithChanges())
|
t.Run("with changes", testDetectAndApplyChangesWithChanges())
|
||||||
t.Run("without changes", testDetectAndApplyChangesWithoutChanges())
|
t.Run("without changes", testDetectAndApplyChangesWithoutChanges())
|
||||||
@@ -55,15 +63,16 @@ func testDetectAndApplyChangesWithChanges() func(t *testing.T) {
|
|||||||
}, &MockDNSProviderImpl{
|
}, &MockDNSProviderImpl{
|
||||||
GetRecordIpResponse: "127.0.0.2",
|
GetRecordIpResponse: "127.0.0.2",
|
||||||
UpdateRecordIpResponse: "127.0.0.1",
|
UpdateRecordIpResponse: "127.0.0.1",
|
||||||
}, []config.DomainConfig{
|
}, &MockedNotificationProviderImpl{},
|
||||||
{
|
[]config.DomainConfig{
|
||||||
TLD: "example.com",
|
{
|
||||||
Subdomains: []string{
|
TLD: "example.com",
|
||||||
"test",
|
Subdomains: []string{
|
||||||
"@",
|
"test",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,15 +92,16 @@ func testDetectAndApplyChangesWithoutChanges() func(t *testing.T) {
|
|||||||
}, &MockDNSProviderImpl{
|
}, &MockDNSProviderImpl{
|
||||||
GetRecordIpResponse: "127.0.0.1",
|
GetRecordIpResponse: "127.0.0.1",
|
||||||
UpdateRecordIpResponse: "127.0.0.1",
|
UpdateRecordIpResponse: "127.0.0.1",
|
||||||
}, []config.DomainConfig{
|
}, &MockedNotificationProviderImpl{},
|
||||||
{
|
[]config.DomainConfig{
|
||||||
TLD: "example.com",
|
{
|
||||||
Subdomains: []string{
|
TLD: "example.com",
|
||||||
"test",
|
Subdomains: []string{
|
||||||
"@",
|
"test",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ type DomainConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ExternalIPProvider ExternalIpProviderConfig `yaml:"ip_provider"`
|
ExternalIPProvider ExternalIpProviderConfig `yaml:"ip_provider"`
|
||||||
DNSProvider DNSProviderConfig `yaml:"dns_provider"`
|
DNSProvider DNSProviderConfig `yaml:"dns_provider"`
|
||||||
Domains []DomainConfig `yaml:"domains"`
|
NotificationProvider NotificationProviderConfig `yaml:"notification_provider,omitempty"`
|
||||||
CheckInterval string `yaml:"check_interval"`
|
Domains []DomainConfig `yaml:"domains"`
|
||||||
|
CheckInterval string `yaml:"check_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalIpProviderConfig struct {
|
type ExternalIpProviderConfig struct {
|
||||||
@@ -28,6 +29,11 @@ type DNSProviderConfig struct {
|
|||||||
ProviderConfig yaml.Node `yaml:"config"`
|
ProviderConfig yaml.Node `yaml:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationProviderConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
ProviderConfig yaml.Node `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) Load(filePath string) error {
|
func (c *Config) Load(filePath string) error {
|
||||||
err := yaml.Unmarshal([]byte(filePath), c)
|
err := yaml.Unmarshal([]byte(filePath), c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ dns_provider:
|
|||||||
config:
|
config:
|
||||||
api_key: exampleAPIKey
|
api_key: exampleAPIKey
|
||||||
base_url: https://example.com
|
base_url: https://example.com
|
||||||
|
notification_provider:
|
||||||
|
type: gotify
|
||||||
|
config:
|
||||||
|
url: https://example.com
|
||||||
domains:
|
domains:
|
||||||
- tld: example.com
|
- tld: example.com
|
||||||
subdomains:
|
subdomains:
|
||||||
@@ -56,7 +60,7 @@ domains:
|
|||||||
- www
|
- www
|
||||||
check_interval: 0 0 0/6 * * * *`)
|
check_interval: 0 0 0/6 * * * *`)
|
||||||
|
|
||||||
want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain"
|
want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain" && c.NotificationProvider.Type == "gotify"
|
||||||
|
|
||||||
if !want || err != nil {
|
if !want || err != nil {
|
||||||
t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want)
|
t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want)
|
||||||
|
|||||||
16
pkg/notificationProvider/console/console.go
Normal file
16
pkg/notificationProvider/console/console.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package notificationProviderConsole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationProviderImplConsole struct{}
|
||||||
|
|
||||||
|
func New() *NotificationProviderImplConsole {
|
||||||
|
return &NotificationProviderImplConsole{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProviderImplConsole) SendNotification(title string, message string) error {
|
||||||
|
fmt.Printf("%s: %s\n", title, message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
87
pkg/notificationProvider/gotify/gotify.go
Normal file
87
pkg/notificationProvider/gotify/gotify.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package notificationProviderGotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationProviderImplGotifyConfig struct {
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
Priority int `yaml:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationProviderImplGotify struct {
|
||||||
|
Url url.URL
|
||||||
|
Token string
|
||||||
|
Priority int
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config NotificationProviderImplGotifyConfig) (*NotificationProviderImplGotify, error) {
|
||||||
|
if config.Url == "" {
|
||||||
|
return nil, fmt.Errorf("url is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := url.Parse(config.Url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Token == "" {
|
||||||
|
return nil, fmt.Errorf("token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Priority < 0 || config.Priority > 4 {
|
||||||
|
return nil, fmt.Errorf("priority must be between 0 and 4")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NotificationProviderImplGotify{
|
||||||
|
*url,
|
||||||
|
config.Token,
|
||||||
|
config.Priority,
|
||||||
|
&http.Client{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NotificationProviderImplGotify) SendNotification(title string, message string) error {
|
||||||
|
type GotifyMessage struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
messageJson, err := json.Marshal(GotifyMessage{
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
p.Priority,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
messageUrl := p.Url
|
||||||
|
messageUrl.JoinPath("message")
|
||||||
|
messageUrl.Query().Add("token", p.Token)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", messageUrl.String(), bytes.NewBuffer(messageJson))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := p.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
81
pkg/notificationProvider/gotify/gotify_test.go
Normal file
81
pkg/notificationProvider/gotify/gotify_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package notificationProviderGotify_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gotify "realdnydns/pkg/notificationProvider/gotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
t.Run("URL is required", testNewEmptyUrl())
|
||||||
|
t.Run("Token is required", testNewEmptyToken())
|
||||||
|
t.Run("Priority must be between 0 and 4", testNewInvalidPriority())
|
||||||
|
t.Run("Sends POST request to url", testNewSendsPostRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewEmptyUrl() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
_, err := gotify.New(gotify.NotificationProviderImplGotifyConfig{
|
||||||
|
Token: "1234",
|
||||||
|
Priority: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err.Error() != "url is required" {
|
||||||
|
t.Errorf("Expected error 'url is required', got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewEmptyToken() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
_, err := gotify.New(gotify.NotificationProviderImplGotifyConfig{
|
||||||
|
Url: "http://localhost:1234",
|
||||||
|
Priority: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err.Error() != "token is required" {
|
||||||
|
t.Errorf("Expected error 'token is required', got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewInvalidPriority() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
_, err := gotify.New(gotify.NotificationProviderImplGotifyConfig{
|
||||||
|
Url: "http://localhost:1234",
|
||||||
|
Token: "token",
|
||||||
|
Priority: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err.Error() != "priority must be between 0 and 4" {
|
||||||
|
t.Errorf("Expected error 'priority must be between 0 and 4', got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewSendsPostRequest() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
provider, err := gotify.New(gotify.NotificationProviderImplGotifyConfig{
|
||||||
|
Url: mockServer.URL,
|
||||||
|
Token: "1234",
|
||||||
|
Priority: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = provider.SendNotification("title", "message")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendNotification() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
pkg/notificationProvider/notificationProvider.go
Normal file
5
pkg/notificationProvider/notificationProvider.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package notificationProvider
|
||||||
|
|
||||||
|
type NotificationProvider interface {
|
||||||
|
SendNotification(title string, message string) error
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user