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.Zone{ Domain: strings.ToLower(hZone.Name), DefaultTTL: 3600, } for _, record := range hZone.Records { zone.Records = append(zone.Records, dnsconfig.Record{ Name: record.Name, Type: record.Type, Value: record.Value, TTL: record.TTL, Flat: false, }) } err = dnsconfig.CreateZone(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 { if zone.DefaultTTL <= 0 { zone.DefaultTTL = 3600 } 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 { if record.Flat { record.Value = helpers.ResolveRecord( record.Value, record.Type, ) if len(record.Value) == 0 { log.Error("Could not flatten record, skipping", "record", record) continue } } if record.TTL <= 0 { record.TTL = zone.DefaultTTL } 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) } 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) } } } }