first commit

This commit is contained in:
William Bouzourène 2025-03-16 19:38:58 +01:00
commit 4b1861fa9b
14 changed files with 811 additions and 0 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
DRY_RUN=true
HETZNER_API_TOKEN=token-goes-here

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
__debug_bin*
gestion-dns
.env
zones/*.toml

13
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}",
"console": "integratedTerminal",
}
]
}

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.insertSpaces": true,
"[go]": {
"editor.insertSpaces": false
}
}

75
dnsconfig/zones.go Normal file
View file

@ -0,0 +1,75 @@
package dnsconfig
import (
"fmt"
"os"
"strings"
"github.com/BurntSushi/toml"
)
type Record struct {
Name string `toml:"name"`
Type string `toml:"type"`
Value string `toml:"value"`
Flat bool `toml:"flat"`
TTL int `toml:"ttl"`
}
type Zone struct {
Domain string `toml:"domain"`
DefaultTTL int `toml:"default_ttl"`
Records []Record `toml:"record"`
}
func GetZones() ([]Zone, error) {
var zones []Zone
files, err := os.ReadDir("./zones/")
if err != nil {
return zones, err
}
for _, file := range files {
name := file.Name()
path := fmt.Sprintf("./zones/%s", name)
if !strings.HasSuffix(strings.ToLower(name), ".toml") {
continue
}
var zone Zone
_, err = toml.DecodeFile(path, &zone)
if err != nil {
continue
}
if len(zone.Domain) == 0 {
continue
}
if zone.DefaultTTL <= 0 {
zone.DefaultTTL = 3600
}
zones = append(zones, zone)
}
return zones, nil
}
func CreateZone(zone Zone) error {
if len(zone.Domain) == 0 {
return fmt.Errorf("zone name cannot be empty")
}
name := strings.ReplaceAll(strings.ToLower(zone.Domain), ".", "_")
path := fmt.Sprintf("./zones/%s.toml", name)
file, err := os.Create(path)
if err != nil {
return err
}
return toml.NewEncoder(file).Encode(&zone)
}

26
go.mod Normal file
View file

@ -0,0 +1,26 @@
module git.readonly.ch/bouzoure/gestion-dns
go 1.24.1
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect
github.com/charmbracelet/log v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.4.2 // indirect
github.com/domainr/dnsr v0.0.0-20250314081958-26623e3d15cb // indirect
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/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
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

62
go.sum Normal file
View file

@ -0,0 +1,62 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/domainr/dnsr v0.0.0-20250314081958-26623e3d15cb h1:JrBQZ2g3k3DsBpyJwXDc37JgAAxJOc+tbF4TBWqT6uw=
github.com/domainr/dnsr v0.0.0-20250314081958-26623e3d15cb/go.mod h1:CoRD9PKaV6dU3S7hQpccTRzZjQpBgBnlnO0h1NHpVV8=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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/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=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

41
helpers/config.go Normal file
View file

@ -0,0 +1,41 @@
package helpers
import (
"os"
"github.com/golobby/dotenv"
)
type Config struct {
DryRun bool `env:"DRY_RUN"`
Hetzner struct {
ApiToken string `env:"HETZNER_API_TOKEN"`
}
}
var configParsed bool
var config Config
func GetConfig() (Config, error) {
if configParsed {
return config, nil
}
return parseConfig()
}
func parseConfig() (Config, error) {
file, err := os.Open(".env")
if err != nil {
return config, err
}
err = dotenv.NewDecoder(file).Decode(&config)
if err != nil {
return config, err
}
configParsed = true
return config, err
}

26
helpers/logger.go Normal file
View file

@ -0,0 +1,26 @@
package helpers
import (
"os"
"time"
"github.com/charmbracelet/log"
)
var loggerCreated bool
var logger *log.Logger
func GetLogger() *log.Logger {
if !loggerCreated {
logger = log.NewWithOptions(os.Stderr, log.Options{
Level: log.DebugLevel,
ReportCaller: true,
ReportTimestamp: true,
TimeFormat: time.RFC3339,
})
loggerCreated = true
}
return logger
}

14
helpers/resolver.go Normal file
View file

@ -0,0 +1,14 @@
package helpers
import (
"github.com/domainr/dnsr"
)
func ResolveRecord(recordName, recordType string) string {
r := dnsr.NewResolver(dnsr.WithCache(0))
for _, rr := range r.Resolve(recordName, recordType) {
return rr.Value
}
return ""
}

217
hetzner/records.go Normal file
View file

@ -0,0 +1,217 @@
package hetzner
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"git.readonly.ch/bouzoure/gestion-dns/helpers"
)
type Record struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl"`
ZoneID string `json:"zone_id"`
Created string `json:"created"`
Modified string `json:"modified"`
}
type Records struct {
Records []Record `json:"records"`
}
type CreateRecordResponse struct {
Record Record `json:"record"`
}
type UpdateRecordResponse struct {
Record Record `json:"record"`
}
func GetRecords(zone *Zone) error {
config, err := helpers.GetConfig()
if err != nil {
return err
}
// Create client
client := &http.Client{}
// Create request
url := fmt.Sprintf("https://dns.hetzner.com/api/v1/records?zone_id=%s", zone.ID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
// Headers
req.Header.Add("Auth-API-Token", config.Hetzner.ApiToken)
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return err
}
// Read Response Body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var records Records
err = json.Unmarshal(respBody, &records)
if err != nil {
return err
}
var recordsToKeep []Record
for _, record := range records.Records {
if strings.EqualFold(record.Type, "NS") {
continue
}
if strings.EqualFold(record.Type, "SOA") {
continue
}
record.Value = strings.TrimPrefix(record.Value, "\"")
record.Value = strings.TrimSuffix(record.Value, "\"")
recordsToKeep = append(recordsToKeep, record)
}
zone.Records = recordsToKeep
return nil
}
func CreateRecord(record *Record) (Record, error) {
var newRecord Record
config, err := helpers.GetConfig()
if err != nil {
return newRecord, err
}
payload, err := json.Marshal(record)
if err != nil {
return newRecord, err
}
body := bytes.NewBuffer(payload)
// Create client
client := &http.Client{}
// Create request
req, err := http.NewRequest("POST", "https://dns.hetzner.com/api/v1/records", body)
if err != nil {
return newRecord, err
}
// Headers
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Auth-API-Token", config.Hetzner.ApiToken)
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return newRecord, err
}
// Read Response Body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return newRecord, err
}
var createRecordResponse CreateRecordResponse
err = json.Unmarshal(respBody, &createRecordResponse)
if err != nil {
return newRecord, err
}
return createRecordResponse.Record, nil
}
func UpdateRecord(record *Record) (Record, error) {
var newRecord Record
config, err := helpers.GetConfig()
if err != nil {
return newRecord, err
}
payload, err := json.Marshal(record)
if err != nil {
return newRecord, err
}
body := bytes.NewBuffer(payload)
// Create client
client := &http.Client{}
// Create request
url := fmt.Sprintf("https://dns.hetzner.com/api/v1/records/%s", record.ID)
req, err := http.NewRequest("PUT", url, body)
if err != nil {
return newRecord, err
}
// Headers
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Auth-API-Token", config.Hetzner.ApiToken)
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return newRecord, err
}
// Read Response Body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return newRecord, err
}
var updateRecordResponse UpdateRecordResponse
err = json.Unmarshal(respBody, &updateRecordResponse)
if err != nil {
return newRecord, err
}
return updateRecordResponse.Record, nil
}
func DeleteRecord(record *Record) error {
config, err := helpers.GetConfig()
if err != nil {
return err
}
// Create client
client := &http.Client{}
// Create request
url := fmt.Sprintf("https://dns.hetzner.com/api/v1/records/%s", record.ID)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
// Headers
req.Header.Add("Auth-API-Token", config.Hetzner.ApiToken)
// Fetch Request
_, err = client.Do(req)
if err != nil {
return err
}
return nil
}

62
hetzner/zones.go Normal file
View file

@ -0,0 +1,62 @@
package hetzner
import (
"encoding/json"
"io"
"net/http"
"git.readonly.ch/bouzoure/gestion-dns/helpers"
)
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
TTL int `json:"ttl"`
NS []string `json:"ns"`
Records []Record `json:"-"`
}
type Zones struct {
Zones []Zone `json:"zones"`
}
func GetZones() ([]Zone, error) {
var zones Zones
config, err := helpers.GetConfig()
if err != nil {
return zones.Zones, err
}
// Create client
client := &http.Client{}
// Create request
req, err := http.NewRequest(
"GET",
"https://dns.hetzner.com/api/v1/zones",
nil,
)
if err != nil {
return zones.Zones, err
}
// Headers
req.Header.Add("Auth-API-Token", config.Hetzner.ApiToken)
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return zones.Zones, err
}
// Read Response Body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return zones.Zones, err
}
err = json.Unmarshal(respBody, &zones)
return zones.Zones, err
}

261
main.go Normal file
View file

@ -0,0 +1,261 @@
package main
import (
"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)
}
log.Info("Starting sync (step 1: create/update)", "dry_run", config.DryRun)
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("Syncing zone", "step", 1, "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(
"Updating record",
"name", record.Name,
"type", record.Type,
"id", id,
)
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{
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,
)
}
}
}
break
}
}
}
log.Info("Starting sync (step 2: delete)", "dry_run", config.DryRun)
for _, zone := range zones {
for _, hZone := range hZones {
if strings.EqualFold(zone.Domain, hZone.Name) {
log.Info("Syncing zone", "setp", 2, "name", zone.Domain)
for _, hRecord := range hZone.Records {
if !slices.Contains(keepTheseIds, hRecord.ID) {
log.Info(
"Deleting record",
"name", hRecord.Name,
"type", hRecord.Type,
"id", hRecord.ID,
)
if !config.DryRun {
hetzner.DeleteRecord(&hetzner.Record{
ID: hRecord.ID,
})
if err != nil {
log.Error(err)
}
log.Info(
"Record deleted",
"ID", hRecord.ID,
)
}
}
}
break
}
}
}
log.Info("Sync is finished, all done!")
}

0
zones/.gitkeep Normal file
View file