Merge branch 'main' into chore-cleanup
All checks were successful
CI / test (pull_request) Successful in 1m48s

This commit is contained in:
2024-08-11 12:04:18 +02:00
12 changed files with 315 additions and 41 deletions

View File

@@ -1,9 +1,11 @@
name: CD name: CD
on: on:
push: workflow_run:
branches: workflows: ["CI"]
- main branches: [main]
types:
- completed
env: env:
DOCKER_REGISTRY: gitea.t000-n.de DOCKER_REGISTRY: gitea.t000-n.de
@@ -42,7 +44,6 @@ jobs:
- name: test - name: test
run: make test run: make test
build_and_push: build_and_push:
name: Build and push name: Build and push
requires: requires:

View File

@@ -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.

View File

@@ -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
View File

@@ -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"
) )
@@ -53,7 +57,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()

View File

@@ -1,25 +1,30 @@
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
notificationProvider notificationProvider.NotificationProvider
domains []config.DomainConfig 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,
notificationProvider: notificationProvider,
domains: domains, 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 {

View File

@@ -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,7 +63,8 @@ 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", TLD: "example.com",
Subdomains: []string{ Subdomains: []string{
@@ -83,7 +92,8 @@ 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", TLD: "example.com",
Subdomains: []string{ Subdomains: []string{

View File

@@ -14,6 +14,7 @@ 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"`
NotificationProvider NotificationProviderConfig `yaml:"notification_provider,omitempty"`
Domains []DomainConfig `yaml:"domains"` Domains []DomainConfig `yaml:"domains"`
CheckInterval string `yaml:"check_interval"` CheckInterval string `yaml:"check_interval"`
} }
@@ -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 {

View File

@@ -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)

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

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

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

View File

@@ -0,0 +1,5 @@
package notificationProvider
type NotificationProvider interface {
SendNotification(title string, message string) error
}