first commit
This commit is contained in:
commit
4b1861fa9b
14 changed files with 811 additions and 0 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DRY_RUN=true
|
||||
HETZNER_API_TOKEN=token-goes-here
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__debug_bin*
|
||||
gestion-dns
|
||||
.env
|
||||
zones/*.toml
|
||||
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal 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
8
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.insertSpaces": true,
|
||||
"[go]": {
|
||||
"editor.insertSpaces": false
|
||||
}
|
||||
}
|
||||
75
dnsconfig/zones.go
Normal file
75
dnsconfig/zones.go
Normal 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
26
go.mod
Normal 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
62
go.sum
Normal 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
41
helpers/config.go
Normal 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
26
helpers/logger.go
Normal 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
14
helpers/resolver.go
Normal 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
217
hetzner/records.go
Normal 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
62
hetzner/zones.go
Normal 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
261
main.go
Normal 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
0
zones/.gitkeep
Normal file
Loading…
Add table
Add a link
Reference in a new issue