You've already forked ddns-updater
Merge pull request #9 from mkelcik/notifications
Initial Notifiers implementation
This commit is contained in:
@@ -18,6 +18,9 @@ Before run, you need configure this environment variables.
|
||||
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
|
||||
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
|
||||
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`, `1.1.1.1`
|
||||
- `NOTIFIERS` - (optional) setting the notifier in case of an update of the dns record. Multiple entries are separated by commas. (default none). Example: `webhook@http://localhost/cloudflare-notification`
|
||||
- Available(
|
||||
- `webhook` - Call defined webhook. Example: `webhook@http://localhost/cloudflare-notification`
|
||||
|
||||
### Building from source
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||
envKeyNotifiers = "NOTIFIERS"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -25,6 +26,7 @@ type Config struct {
|
||||
ApiToken string
|
||||
CloudflareZone string
|
||||
OnChangeComment string
|
||||
Notifiers []string
|
||||
CheckInterval time.Duration
|
||||
}
|
||||
|
||||
@@ -52,11 +54,12 @@ func NewConfig() Config {
|
||||
}
|
||||
|
||||
return Config{
|
||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package internal
|
||||
|
||||
import "strings"
|
||||
|
||||
func parseDNSToCheck(data string) []string {
|
||||
func parseCommaDelimited(data string) []string {
|
||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||
for _, dns := range strings.Split(data, ",") {
|
||||
if w := strings.TrimSpace(dns); w != "" {
|
||||
for _, item := range strings.Split(data, ",") {
|
||||
if w := strings.TrimSpace(item); w != "" {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
|
||||
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
17
main.go
17
main.go
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||
|
||||
// public ip resolver
|
||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||
|
||||
@@ -85,9 +88,19 @@ func main() {
|
||||
|
||||
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
||||
log.Printf("error updating dns record: %s", err)
|
||||
} else {
|
||||
log.Printf("Updated to `%s`", currentPublicIP)
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
notifications/types.go
Normal file
76
notifications/types.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configDelimiter = "@"
|
||||
)
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
func (n Notification) ToSlice() []string {
|
||||
return []string{n.OldIp.String(), n.NewIp.String(), n.CheckedAt.Format(time.RFC3339), n.ResolverTag, n.Domain}
|
||||
}
|
||||
|
||||
var Available = map[string]func(string) (Notifier, error){
|
||||
webhookTag: func(config string) (Notifier, error) {
|
||||
parts := strings.Split(config, configDelimiter)
|
||||
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("wrong webhook config, missing url part")
|
||||
}
|
||||
|
||||
return NewWebhookNotification(WebhookConfig{Url: parts[1]}, &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}), nil
|
||||
},
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
Tag() string
|
||||
Notify(ctx context.Context, notification Notification) error
|
||||
}
|
||||
|
||||
func GetNotifiers(tags []string) Notifiers {
|
||||
out := Notifiers{}
|
||||
for _, t := range tags {
|
||||
if initFn, ok := Available[strings.Split(t, configDelimiter)[0]]; ok {
|
||||
notifier, err := initFn(t)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
out = append(out, notifier)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
66
notifications/webhook.go
Normal file
66
notifications/webhook.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
webhookTag = "webhook"
|
||||
)
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type WebhookConfig struct {
|
||||
Url string
|
||||
}
|
||||
|
||||
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(nil)
|
||||
if err := json.NewEncoder(out).Encode(notification); err != nil {
|
||||
return nil, fmt.Errorf("error encoding json notification body: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user