You've already forked ddns-updater
Initial Notifiers implementation
This commit is contained in:
@@ -17,6 +17,7 @@ const (
|
|||||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||||
|
envKeyNotifiers = "NOTIFIERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -25,6 +26,7 @@ type Config struct {
|
|||||||
ApiToken string
|
ApiToken string
|
||||||
CloudflareZone string
|
CloudflareZone string
|
||||||
OnChangeComment string
|
OnChangeComment string
|
||||||
|
Notifiers []string
|
||||||
CheckInterval time.Duration
|
CheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +54,12 @@ func NewConfig() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||||
|
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package internal
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
func parseDNSToCheck(data string) []string {
|
func parseCommaDelimited(data string) []string {
|
||||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||||
for _, dns := range strings.Split(data, ",") {
|
for _, item := range strings.Split(data, ",") {
|
||||||
if w := strings.TrimSpace(dns); w != "" {
|
if w := strings.TrimSpace(item); w != "" {
|
||||||
out = append(out, w)
|
out = append(out, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
|
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
32
main.go
32
main.go
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
"github.com/cloudflare/cloudflare-go"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||||
|
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,21 @@ type PublicIpResolver interface {
|
|||||||
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getNotifiers(tags []string) notifications.Notifiers {
|
||||||
|
out := notifications.Notifiers{}
|
||||||
|
for _, t := range tags {
|
||||||
|
if initFn, ok := notifications.Available[t]; ok {
|
||||||
|
notifier, err := initFn()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, notifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func getResolver(resolverName string) (PublicIpResolver, string) {
|
func getResolver(resolverName string) (PublicIpResolver, string) {
|
||||||
switch resolverName {
|
switch resolverName {
|
||||||
// HERE add another resolver if needed
|
// HERE add another resolver if needed
|
||||||
@@ -51,6 +67,8 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifiers := getNotifiers(config.Notifiers)
|
||||||
|
|
||||||
// public ip resolver
|
// public ip resolver
|
||||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||||
|
|
||||||
@@ -85,9 +103,19 @@ func main() {
|
|||||||
|
|
||||||
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
||||||
log.Printf("error updating dns record: %s", err)
|
log.Printf("error updating dns record: %s", err)
|
||||||
} else {
|
continue
|
||||||
log.Printf("Updated to `%s`", currentPublicIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
|
||||||
|
OldIp: net.ParseIP(dnsRecord.Content),
|
||||||
|
NewIp: currentPublicIP,
|
||||||
|
CheckedAt: time.Now(),
|
||||||
|
ResolverTag: resolverTag,
|
||||||
|
Domain: dnsRecord.Name,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("errors in notifications: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("Updated to `%s`", currentPublicIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
notifications/types.go
Normal file
44
notifications/types.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Notifiers []Notifier
|
||||||
|
|
||||||
|
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
|
||||||
|
var outErr error
|
||||||
|
for _, notifier := range n {
|
||||||
|
if err := notifier.Notify(ctx, notification); err != nil {
|
||||||
|
outErr = errors.Join(outErr, err)
|
||||||
|
}
|
||||||
|
log.Printf("Notification sent via %s\n", notifier.Tag())
|
||||||
|
}
|
||||||
|
return outErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
OldIp net.IP `json:"old_ip,omitempty"`
|
||||||
|
NewIp net.IP `json:"new_ip"`
|
||||||
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
|
ResolverTag string `json:"resolver_tag"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Available = map[string]func() (Notifier, error){
|
||||||
|
webhookTag: func() (Notifier, error) {
|
||||||
|
return NewWebhookNotification(NewWebhookConfigFromEnv(), &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notifier interface {
|
||||||
|
Tag() string
|
||||||
|
Notify(ctx context.Context, notification Notification) error
|
||||||
|
}
|
||||||
82
notifications/webhook.go
Normal file
82
notifications/webhook.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
webhookTag = "webhook"
|
||||||
|
webhookRequestTypeJson = "JSON"
|
||||||
|
envWebhookUrl = "WEBHOOK_RL"
|
||||||
|
envWebhookRequestType = "WEBHOOK_REQ_TYPE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookConfig struct {
|
||||||
|
Url string
|
||||||
|
Json bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookConfigFromEnv() WebhookConfig {
|
||||||
|
return WebhookConfig{
|
||||||
|
Url: os.Getenv(envWebhookUrl),
|
||||||
|
Json: strings.ToUpper(os.Getenv(envWebhookRequestType)) == webhookRequestTypeJson,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookNotification struct {
|
||||||
|
config WebhookConfig
|
||||||
|
client Doer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) Tag() string {
|
||||||
|
return webhookTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookNotification(config WebhookConfig, client Doer) *WebhookNotification {
|
||||||
|
return &WebhookNotification{config: config, client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) getRequestBody(notification Notification) (io.Reader, error) {
|
||||||
|
out := bytes.NewBuffer(notification.NewIp)
|
||||||
|
if w.config.Json {
|
||||||
|
if err := json.NewEncoder(out).Encode(notification); err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding notification body: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) Notify(ctx context.Context, notification Notification) error {
|
||||||
|
body, err := w.getRequestBody(notification)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.config.Url, body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := w.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog unexpected non 2xx code %d returned", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
notifications/webhook_test.go
Normal file
46
notifications/webhook_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebhookNotification_getRequestBody(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
config WebhookConfig
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
notification Notification
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want io.Reader
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
fields: fields{},
|
||||||
|
args: args{},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
w := WebhookNotification{
|
||||||
|
config: tt.fields.config,
|
||||||
|
}
|
||||||
|
got, err := w.getRequestBody(tt.args.notification)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("getRequestBody() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("getRequestBody() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user