Skip to content

Commit

Permalink
feat: Hetzer Support (#1)
Browse files Browse the repository at this point in the history
* implement hetzner api, service and config

* improve logging in get records

* fix map error in service

* fix error when updating records

* update readme

* fix json in readme
  • Loading branch information
leonlatsch authored Oct 7, 2024
1 parent 0e5ab92 commit 3405499
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 56 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,28 @@ docker run -d \
--name go-resolve \
ghcr.io/leonlatsch/go-resolve:latest
```

### Config

```json
{
"provider": "hetzner", // Available: updateUrl, goDaddy, hetzner
"interval": "1h",
"domain": "yourdomain.dom",
"hosts": [
"subdomain1",
"subdomain2"
],
"updateUrlConfig": {
"url": "UPDATE_URL"
},
"goDaddyConfig": {
"apiKey": "GODADDY_API_KEY",
"apiSecret": "GODADDY_API_SECRET"
},
"hetznerConfig": {
"zoneId": "HETZNER_ZONE_ID",
"apiToken": "HETZNER_API_TOKEN"
}
}
```
30 changes: 0 additions & 30 deletions internal/api/godaddy_api_fake.go

This file was deleted.

26 changes: 13 additions & 13 deletions internal/api/godaddy_api.go → internal/godaddy/godaddy_api.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package api
package godaddy

import (
"fmt"
Expand All @@ -10,10 +10,10 @@ import (
)

type GodaddyApi interface {
GetDomainDetail() (models.DomainDetail, error)
GetRecords(host string) ([]models.DnsRecord, error)
CreateRecord(record models.DnsRecord) error
UpdateRecord(record models.DnsRecord) error
GetDomainDetail() (DomainDetail, error)
GetRecords(host string) ([]DnsRecord, error)
CreateRecord(record DnsRecord) error
UpdateRecord(record DnsRecord) error
}

type GodaddyApiImpl struct {
Expand All @@ -23,8 +23,8 @@ type GodaddyApiImpl struct {

const BASE_URL = "https://api.godaddy.com/v1"

func (self *GodaddyApiImpl) GetDomainDetail() (models.DomainDetail, error) {
var detail models.DomainDetail
func (self *GodaddyApiImpl) GetDomainDetail() (DomainDetail, error) {
var detail DomainDetail
json, err := self.HttpClient.Get(self.endpointDomainDetail(), self.getAuthHeaders())
if err != nil {
return detail, err
Expand All @@ -37,8 +37,8 @@ func (self *GodaddyApiImpl) GetDomainDetail() (models.DomainDetail, error) {
return detail, nil
}

func (self *GodaddyApiImpl) GetRecords(host string) ([]models.DnsRecord, error) {
var records []models.DnsRecord
func (self *GodaddyApiImpl) GetRecords(host string) ([]DnsRecord, error) {
var records []DnsRecord
recordsJson, err := self.HttpClient.Get(self.endpointARecords(host), self.getAuthHeaders())

if err != nil {
Expand All @@ -52,9 +52,9 @@ func (self *GodaddyApiImpl) GetRecords(host string) ([]models.DnsRecord, error)
return records, nil
}

func (self *GodaddyApiImpl) CreateRecord(record models.DnsRecord) error {
func (self *GodaddyApiImpl) CreateRecord(record DnsRecord) error {
log.Println("Creating " + record.Name + "." + self.Config.Domain + " -> " + record.Data)
records := []models.DnsRecord{record}
records := []DnsRecord{record}

if _, err := self.HttpClient.Patch(self.endpointRecords(""), self.getAuthHeaders(), records); err != nil {
return err
Expand All @@ -63,9 +63,9 @@ func (self *GodaddyApiImpl) CreateRecord(record models.DnsRecord) error {
return nil
}

func (self *GodaddyApiImpl) UpdateRecord(record models.DnsRecord) error {
func (self *GodaddyApiImpl) UpdateRecord(record DnsRecord) error {
log.Println("Updating " + record.Name + "." + self.Config.Domain + " -> " + record.Data)
records := []models.DnsRecord{record}
records := []DnsRecord{record}

if _, err := self.HttpClient.Put(self.endpointARecords(record.Name), self.getAuthHeaders(), records); err != nil {
return err
Expand Down
28 changes: 28 additions & 0 deletions internal/godaddy/godaddy_api_fake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package godaddy

type GodaddyApiFake struct {
DomainDetail DomainDetail
ExistingRecords map[string][]DnsRecord
Error error

CreateRecordCalledWith DnsRecord
UpdateRecordCalledWith DnsRecord
}

func (self *GodaddyApiFake) GetDomainDetail() (DomainDetail, error) {
return self.DomainDetail, self.Error
}

func (self *GodaddyApiFake) GetRecords(host string) ([]DnsRecord, error) {
return self.ExistingRecords[host], self.Error
}

func (self *GodaddyApiFake) CreateRecord(record DnsRecord) error {
self.CreateRecordCalledWith = record
return self.Error
}

func (self *GodaddyApiFake) UpdateRecord(record DnsRecord) error {
self.UpdateRecordCalledWith = record
return self.Error
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package models
package godaddy

type DnsRecord struct {
Data string `json:"data"`
Expand Down
59 changes: 59 additions & 0 deletions internal/hetzner/hetzner_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package hetzner

import (
"fmt"

"github.com/leonlatsch/go-resolve/internal/http"
"github.com/leonlatsch/go-resolve/internal/models"
"github.com/leonlatsch/go-resolve/internal/serialization"
)

type HetznerApi interface {
GetRecords() ([]Record, error)
BulkUpdate(records []Record) error
}

type HetznerApiImpl struct {
Config *models.Config
HttpClient http.HttpClient
}

const BASE_URL = "https://dns.hetzner.com/api/v1"

func (api *HetznerApiImpl) GetRecords() ([]Record, error) {
var records []Record
var recordsWrapper RecordsWrapper

url := fmt.Sprintf("%v/records?zone_id=%v", BASE_URL, api.Config.HetznerConfig.ZoneId)
recordsJson, err := api.HttpClient.Get(url, api.getHeaders())

if err != nil {
return records, err
}

if err := serialization.FromJson(recordsJson, &recordsWrapper); err != nil {
return records, err
}

return recordsWrapper.Records, nil
}

func (api *HetznerApiImpl) BulkUpdate(records []Record) error {
recordsWrapper := RecordsWrapper{
Records: records,
}

url := fmt.Sprintf("%v/records/bulk", BASE_URL)

if _, err := api.HttpClient.Put(url, api.getHeaders(), recordsWrapper); err != nil {
return err
}

return nil
}

func (api *HetznerApiImpl) getHeaders() map[string]string {
headers := make(map[string]string)
headers["Auth-API-Token"] = api.Config.HetznerConfig.ApiToken
return headers
}
82 changes: 82 additions & 0 deletions internal/hetzner/hetzner_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package hetzner

import (
"errors"
"fmt"
"log"

"github.com/leonlatsch/go-resolve/internal/models"
"github.com/leonlatsch/go-resolve/internal/service"
)

type HetznerService struct {
Config *models.Config
HetznerApi HetznerApi
IpObserverService service.IpObserverService

RecordIds map[string]RecordId
}

func (service *HetznerService) PreloadRecordIds() error {
recordIds := make(map[string]RecordId)

records, err := service.HetznerApi.GetRecords()
if err != nil {
log.Println("Could not preload records ids. Please check your config")
return err
}

for _, record := range records {
for _, host := range service.Config.Hosts {
if host == record.Name {
log.Printf("Loaded record id %v for %v", record.Id, host)
recordIds[host] = record.Id
}
}
}

if len(recordIds) <= 0 {
return errors.New("Could not find configured records in dns entries")
}

service.RecordIds = recordIds
return nil
}

func (service *HetznerService) ObserveAndUpdateDns() {
log.Println("Running for hetzner")
service.IpObserverService.ObserveIp(func(ip string) {
service.UpdateDns(ip)
})
}

func (service *HetznerService) UpdateDns(ip string) {
log.Println("Ip changed: " + ip)

if len(service.RecordIds) <= 0 {
log.Println("No records ids loaded. Not updating.")
return
}

records := []Record{}
for _, host := range service.Config.Hosts {
record := Record{
Id: service.RecordIds[host],
Value: ip,
Type: "A",
Name: host,
ZoneId: ZoneId(service.Config.HetznerConfig.ZoneId),
}
records = append(records, record)
}

log.Println(fmt.Sprintf("Updating %v records for %v", len(records), service.Config.Domain))
if err := service.HetznerApi.BulkUpdate(records); err != nil {
log.Println("Bulk update failed. Not caching ip")
log.Println(err)
return
}

log.Println("Successfully updated all records. Caching " + ip)
service.IpObserverService.LastIp = ip
}
16 changes: 16 additions & 0 deletions internal/hetzner/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package hetzner

type ZoneId string
type RecordId string

type Record struct {
Id RecordId `json:"id"`
Value string `json:"value"`
Type string `json:"type"`
Name string `json:"name"`
ZoneId ZoneId `json:"zone_id"`
}

type RecordsWrapper struct {
Records []Record `json:"records"`
}
11 changes: 11 additions & 0 deletions internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
const (
ProviderUpdateUrl = "updateUrl"
ProviderGoDaddy = "goDaddy"
ProviderHetzner = "hetzner"
)

type Config struct {
Expand All @@ -13,6 +14,7 @@ type Config struct {

UpdateUrlConfig UpdateUrlConfig `json:"updateUrlConfig"`
GoDaddyConfig GoDaddyConfig `json:"goDaddyConfig"`
HetznerConfig HetznerConfig `json:"hetznerConfig"`
}

type UpdateUrlConfig struct {
Expand All @@ -24,6 +26,11 @@ type GoDaddyConfig struct {
ApiSecret string `json:"apiSecret"`
}

type HetznerConfig struct {
ZoneId string `json:"zoneId"`
ApiToken string `json:"apiToken"`
}

// Empty Config. Used for generating file at first launch

var EmptyConfig = Config{
Expand All @@ -39,4 +46,8 @@ var EmptyConfig = Config{
ApiKey: "GODADDY_API_KEY",
ApiSecret: "GODADDY_API_SECRET",
},
HetznerConfig: HetznerConfig{
ZoneId: "ZONE_ID",
ApiToken: "API_TOKEN",
},
}
5 changes: 3 additions & 2 deletions internal/service/godaddy_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"log"

"github.com/leonlatsch/go-resolve/internal/api"
"github.com/leonlatsch/go-resolve/internal/godaddy"
"github.com/leonlatsch/go-resolve/internal/models"
)

type GodaddyService struct {
Config *models.Config
GodaddyApi api.GodaddyApi
GodaddyApi godaddy.GodaddyApi
IpApi api.IpApi
IpObserver IpObserverService
}
Expand Down Expand Up @@ -56,7 +57,7 @@ func (self *GodaddyService) UpdateDns(ip string) {
continue
}

record := models.DnsRecord{
record := godaddy.DnsRecord{
Data: ip,
Name: host,
Type: "A",
Expand Down
Loading

0 comments on commit 3405499

Please sign in to comment.