diff --git a/.env.example b/.env.example index 412023a..b2b9c5c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ DRY_RUN=true HETZNER_API_TOKEN=token-goes-here +PUSHOVER_ENABLE=false +PUSHOVER_APP_KEY=key-goes-here +PUSHOVER_USER_KEY=key-goes-here \ No newline at end of file diff --git a/go.mod b/go.mod index b90ab16..fda37e2 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/golobby/cast v1.3.3 // 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/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.63 // indirect diff --git a/go.sum b/go.sum index 78c4152..b04772f 100644 --- a/go.sum +++ b/go.sum @@ -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/dotenv v1.3.2 h1:9vA8XqXXIB3cX/5xQ1CTbOCPegioHtHXIxeFng+uOqQ= 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.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/helpers/config.go b/helpers/config.go index 6a775e6..a47b859 100644 --- a/helpers/config.go +++ b/helpers/config.go @@ -11,6 +11,11 @@ type Config struct { Hetzner struct { 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 diff --git a/helpers/pushover.go b/helpers/pushover.go new file mode 100644 index 0000000..267d881 --- /dev/null +++ b/helpers/pushover.go @@ -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 +} diff --git a/main.go b/main.go index f179731..a1717f5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "slices" "strings" @@ -96,7 +97,16 @@ func main() { 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 for _, zone := range zones { if zone.DefaultTTL <= 0 { @@ -105,7 +115,7 @@ func main() { for _, hZone := range hZones { 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 for _, record := range zone.Records { @@ -162,57 +172,58 @@ func main() { if updateNeeded { log.Info( - "Updating record", + "Marking record for update", + "id", id, "name", record.Name, "type", record.Type, - "id", id, + "value", record.Value, + "ttl", record.TTL, ) - if !config.DryRun { - newRecord, err := hetzner.UpdateRecord(&hetzner.Record{ - 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{ + recordsToUpdate = append(recordsToUpdate, hetzner.Record{ + 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 created", - "ID", newRecord.ID, - ) + pushoverMessages = append(pushoverMessages, fmt.Sprintf( + "Action: update\nZone: %s\nRecord: %s\nType: %s\nValue: %s\nTTL: %d", + 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 _, hZone := range hZones { 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 { if !slices.Contains(keepTheseIds, hRecord.ID) { log.Info( - "Deleting record", + "Marking record for deletion", + "id", hRecord.ID, "name", hRecord.Name, "type", hRecord.Type, - "id", hRecord.ID, + "value", hRecord.Value, + "ttl", hRecord.TTL, ) - if !config.DryRun { - hetzner.DeleteRecord(&hetzner.Record{ - ID: hRecord.ID, - }) - if err != nil { - log.Error(err) - } + recordsToDelete = append(recordsToDelete, hetzner.Record{ + ID: hRecord.ID, + }) - log.Info( - "Record deleted", - "ID", hRecord.ID, - ) - } + pushoverMessages = append(pushoverMessages, fmt.Sprintf( + "Action: delete\nZone: %s\nRecord: %s\nType: %s\nValue: %s\nTTL: %d", + 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!") + + 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) + } + } + } }