Rework engine to apply changes at the end & add pushover notification

This commit is contained in:
William Bouzourène 2025-03-17 16:57:41 +01:00
parent 4b1861fa9b
commit 50dcc9d994
6 changed files with 186 additions and 57 deletions

View file

@ -1,2 +1,5 @@
DRY_RUN=true DRY_RUN=true
HETZNER_API_TOKEN=token-goes-here HETZNER_API_TOKEN=token-goes-here
PUSHOVER_ENABLE=false
PUSHOVER_APP_KEY=key-goes-here
PUSHOVER_USER_KEY=key-goes-here

1
go.mod
View file

@ -12,6 +12,7 @@ require (
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golobby/cast v1.3.3 // indirect github.com/golobby/cast v1.3.3 // indirect
github.com/golobby/dotenv v1.3.2 // indirect github.com/golobby/dotenv v1.3.2 // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.63 // indirect github.com/miekg/dns v1.1.63 // indirect

2
go.sum
View file

@ -19,6 +19,8 @@ github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig=
github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY=
github.com/golobby/dotenv v1.3.2 h1:9vA8XqXXIB3cX/5xQ1CTbOCPegioHtHXIxeFng+uOqQ= github.com/golobby/dotenv v1.3.2 h1:9vA8XqXXIB3cX/5xQ1CTbOCPegioHtHXIxeFng+uOqQ=
github.com/golobby/dotenv v1.3.2/go.mod h1:9MMVXqzLNluhVxCv3X/DLYBNUb289f05tr+df1+7278= github.com/golobby/dotenv v1.3.2/go.mod h1:9MMVXqzLNluhVxCv3X/DLYBNUb289f05tr+df1+7278=
github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo=
github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

View file

@ -11,6 +11,11 @@ type Config struct {
Hetzner struct { Hetzner struct {
ApiToken string `env:"HETZNER_API_TOKEN"` ApiToken string `env:"HETZNER_API_TOKEN"`
} }
PushOver struct {
Enable bool `env:"PUSHOVER_ENABLE"`
AppKey string `env:"PUSHOVER_APP_KEY"`
UserKey string `env:"PUSHOVER_USER_KEY"`
}
} }
var configParsed bool var configParsed bool

44
helpers/pushover.go Normal file
View file

@ -0,0 +1,44 @@
package helpers
import (
"fmt"
"github.com/gregdel/pushover"
)
type PushoverMessage struct {
AppKey string
UserKey string
Title string
Message string
}
func PushoverSendMessage(message PushoverMessage) error {
if len(message.AppKey) == 0 {
return fmt.Errorf("pushover app key is required")
}
if len(message.UserKey) == 0 {
return fmt.Errorf("pushover user key is required")
}
if len(message.Message) == 0 {
return fmt.Errorf("pushover message is required")
}
app := pushover.New(message.AppKey)
user := pushover.NewRecipient(message.UserKey)
var msg *pushover.Message
if len(message.Title) > 0 {
msg = pushover.NewMessageWithTitle(
message.Message,
message.Title,
)
} else {
msg = pushover.NewMessage(message.Message)
}
_, err := app.SendMessage(msg, user)
return err
}

188
main.go
View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"slices" "slices"
"strings" "strings"
@ -96,7 +97,16 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
log.Info("Starting sync (step 1: create/update)", "dry_run", config.DryRun) // Keep operations in these slices
// Sync is only performed after diff calculation is done
var recordsToCreate []hetzner.Record
var recordsToUpdate []hetzner.Record
var recordsToDelete []hetzner.Record
// PushOver message, will only be sent if PushOver enabled and message not empty
var pushoverMessages []string
log.Info("Calculating sync diff (step 1: create/update)")
var keepTheseIds []string var keepTheseIds []string
for _, zone := range zones { for _, zone := range zones {
if zone.DefaultTTL <= 0 { if zone.DefaultTTL <= 0 {
@ -105,7 +115,7 @@ func main() {
for _, hZone := range hZones { for _, hZone := range hZones {
if strings.EqualFold(zone.Domain, hZone.Name) { if strings.EqualFold(zone.Domain, hZone.Name) {
log.Info("Syncing zone", "step", 1, "name", zone.Domain) log.Info("Calculating sync diff for zone", "name", zone.Domain)
var alreadyFoundIds []string var alreadyFoundIds []string
for _, record := range zone.Records { for _, record := range zone.Records {
@ -162,57 +172,58 @@ func main() {
if updateNeeded { if updateNeeded {
log.Info( log.Info(
"Updating record", "Marking record for update",
"id", id,
"name", record.Name, "name", record.Name,
"type", record.Type, "type", record.Type,
"id", id, "value", record.Value,
"ttl", record.TTL,
) )
if !config.DryRun { recordsToUpdate = append(recordsToUpdate, hetzner.Record{
newRecord, err := hetzner.UpdateRecord(&hetzner.Record{ ID: id,
ID: id,
Type: record.Type,
Name: record.Name,
Value: record.Value,
TTL: record.TTL,
ZoneID: hZone.ID,
})
if err != nil {
log.Error(err)
}
log.Info(
"Record updated",
"ID", newRecord.ID,
)
}
}
} else {
log.Info(
"Creating record",
"name", record.Name,
"type", record.Type,
"value", record.Value,
"ttl", record.TTL,
)
if !config.DryRun {
newRecord, err := hetzner.CreateRecord(&hetzner.Record{
Type: record.Type, Type: record.Type,
Name: record.Name, Name: record.Name,
Value: record.Value, Value: record.Value,
TTL: record.TTL, TTL: record.TTL,
ZoneID: hZone.ID, ZoneID: hZone.ID,
}) })
if err != nil {
log.Error(err)
}
log.Info( pushoverMessages = append(pushoverMessages, fmt.Sprintf(
"Record created", "Action: update\nZone: %s\nRecord: %s\nType: %s\nValue: %s\nTTL: %d",
"ID", newRecord.ID, zone.Domain,
) record.Name,
record.Type,
record.Value,
record.TTL,
))
} }
} else {
log.Info(
"Marking record for creation",
"zone_id", hZone.ID,
"name", record.Name,
"type", record.Type,
"value", record.Value,
"ttl", record.TTL,
)
recordsToCreate = append(recordsToCreate, hetzner.Record{
Type: record.Type,
Name: record.Name,
Value: record.Value,
TTL: record.TTL,
ZoneID: hZone.ID,
})
pushoverMessages = append(pushoverMessages, fmt.Sprintf(
"Action: create\nZone: %s\nRecord: %s\nType: %s\nValue: %s\nTTL: %d",
zone.Domain,
record.Name,
record.Type,
record.Value,
record.TTL,
))
} }
} }
@ -221,34 +232,35 @@ func main() {
} }
} }
log.Info("Starting sync (step 2: delete)", "dry_run", config.DryRun) log.Info("Calculating sync diff (step 2: delete)")
for _, zone := range zones { for _, zone := range zones {
for _, hZone := range hZones { for _, hZone := range hZones {
if strings.EqualFold(zone.Domain, hZone.Name) { if strings.EqualFold(zone.Domain, hZone.Name) {
log.Info("Syncing zone", "setp", 2, "name", zone.Domain) log.Info("Calculating sync diff for zone", "name", zone.Domain)
for _, hRecord := range hZone.Records { for _, hRecord := range hZone.Records {
if !slices.Contains(keepTheseIds, hRecord.ID) { if !slices.Contains(keepTheseIds, hRecord.ID) {
log.Info( log.Info(
"Deleting record", "Marking record for deletion",
"id", hRecord.ID,
"name", hRecord.Name, "name", hRecord.Name,
"type", hRecord.Type, "type", hRecord.Type,
"id", hRecord.ID, "value", hRecord.Value,
"ttl", hRecord.TTL,
) )
if !config.DryRun { recordsToDelete = append(recordsToDelete, hetzner.Record{
hetzner.DeleteRecord(&hetzner.Record{ ID: hRecord.ID,
ID: hRecord.ID, })
})
if err != nil {
log.Error(err)
}
log.Info( pushoverMessages = append(pushoverMessages, fmt.Sprintf(
"Record deleted", "Action: delete\nZone: %s\nRecord: %s\nType: %s\nValue: %s\nTTL: %d",
"ID", hRecord.ID, hZone.Name,
) hRecord.Name,
} hRecord.Type,
hRecord.Value,
hRecord.TTL,
))
} }
} }
@ -257,5 +269,67 @@ func main() {
} }
} }
log.Info("Starting sync (step 1: delete)", "dry_run", config.DryRun)
for _, record := range recordsToDelete {
if !config.DryRun {
hetzner.DeleteRecord(&record)
if err != nil {
log.Error(err)
}
log.Warn(
"Record deleted",
"ID", record.ID,
)
}
}
log.Info("Starting sync (step 2: create)", "dry_run", config.DryRun)
for _, record := range recordsToCreate {
if !config.DryRun {
newRecord, err := hetzner.UpdateRecord(&record)
if err != nil {
log.Error(err)
}
log.Warn(
"Record created",
"ID", newRecord.ID,
)
}
}
log.Info("Starting sync (step 3: update)", "dry_run", config.DryRun)
for _, record := range recordsToUpdate {
if !config.DryRun {
newRecord, err := hetzner.UpdateRecord(&record)
if err != nil {
log.Error(err)
}
log.Warn(
"Record updated",
"ID", newRecord.ID,
)
}
}
log.Info("Sync is finished, all done!") log.Info("Sync is finished, all done!")
if !config.DryRun && config.PushOver.Enable && len(pushoverMessages) > 0 {
log.Info("Changes made, sending PushOver notifications")
for _, message := range pushoverMessages {
err = helpers.PushoverSendMessage(helpers.PushoverMessage{
Message: message,
Title: "Changes made to DNS record",
AppKey: config.PushOver.AppKey,
UserKey: config.PushOver.UserKey,
})
if err != nil {
log.Error(err)
}
}
}
} }