package main
import (
"fmt"
"slices"
"strings"
"git.readonly.ch/bouzoure/gestion-dns/dnsconfig"
"git.readonly.ch/bouzoure/gestion-dns/helpers"
"git.readonly.ch/bouzoure/gestion-dns/hetzner"
)
func main() {
log := helpers.GetLogger()
config, err := helpers.GetConfig()
if err != nil {
log.Fatal(err)
}
if config.DryRun {
log.Warn("Dry run is enabled, changes will not be applied")
} else {
log.Warn("Dry run is disabled, changes will be applied")
}
// Fetch zones from Hetzner API
log.Info("Fetching existing zones from Hetzner API")
hZonesTmp, err := hetzner.GetZones()
if err != nil {
log.Fatal(err)
}
// Fetch records from Hetzner API
log.Info("Fetching existing records from Hetzner API")
var hZones []hetzner.Zone
for _, zone := range hZonesTmp {
err = hetzner.GetRecords(&zone)
if err != nil {
log.Error(err)
continue
}
hZones = append(hZones, zone)
}
// Fetch zones from config files
log.Info("Fetching zones from DNS config files")
zones, err := dnsconfig.GetZones()
if err != nil {
log.Fatal(err)
}
// Create missing config files from existing zones
log.Info("Checking if we need to create DNS config files from existing zones")
for _, hZone := range hZones {
found := false
for _, zone := range zones {
if strings.EqualFold(hZone.Name, zone.Domain) {
found = true
break
}
}
if found {
continue
}
log.Info("Creating DNS config file for existing zone", "zone", hZone.Name)
zone := dnsconfig.TomlZone{
Domain: strings.ToLower(hZone.Name),
DefaultTTL: 3600,
}
for _, record := range hZone.Records {
zone.TomlRecords = append(zone.TomlRecords, dnsconfig.TomlRecord{
Name: record.Name,
Type: record.Type,
Value: record.Value,
TTL: record.TTL,
})
}
err = dnsconfig.CreateTomlZone(zone)
if err != nil {
log.Fatal(err)
}
}
// Fetch zones from config files again
// This is because the previous step might have created new config files
log.Info("Fetching zones from DNS config files")
zones, err = dnsconfig.GetZones()
if err != nil {
log.Fatal(err)
}
// 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 {
for _, hZone := range hZones {
if strings.EqualFold(zone.Domain, hZone.Name) {
log.Info("Calculating sync diff for zone", "name", zone.Domain)
var alreadyFoundIds []string
for _, record := range zone.Records {
var id string
for _, hRecord := range hZone.Records {
if slices.Contains(alreadyFoundIds, hRecord.ID) {
continue
}
if !strings.EqualFold(record.Name, hRecord.Name) {
continue
}
if !strings.EqualFold(record.Type, hRecord.Type) {
continue
}
id = hRecord.ID
alreadyFoundIds = append(alreadyFoundIds, id)
break
}
if len(id) > 0 {
keepTheseIds = append(keepTheseIds, id)
updateNeeded := false
for _, hRecord := range hZone.Records {
if hRecord.ID == id {
if record.TTL != hRecord.TTL {
updateNeeded = true
}
if record.Value != hRecord.Value {
updateNeeded = true
}
break
}
}
if updateNeeded {
log.Info(
"Marking record for update",
"id", id,
"name", record.Name,
"type", record.Type,
"value", record.Value,
"ttl", record.TTL,
)
recordsToUpdate = append(recordsToUpdate, hetzner.Record{
ID: id,
Type: record.Type,
Name: record.Name,
Value: record.Value,
TTL: record.TTL,
ZoneID: hZone.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,
))
}
}
break
}
}
}
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("Calculating sync diff for zone", "name", zone.Domain)
for _, hRecord := range hZone.Records {
if !slices.Contains(keepTheseIds, hRecord.ID) {
log.Info(
"Marking record for deletion",
"id", hRecord.ID,
"name", hRecord.Name,
"type", hRecord.Type,
"value", hRecord.Value,
"ttl", hRecord.TTL,
)
recordsToDelete = append(recordsToDelete, hetzner.Record{
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,
))
}
}
break
}
}
}
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)
continue
}
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.CreateRecord(&record)
if err != nil {
log.Error(err)
continue
}
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)
continue
}
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)
}
}
}
}