2 Commits

Author SHA1 Message Date
0723c5cd09 style: clean up Dockerfile
All checks were successful
CI / test (pull_request) Successful in 58s
2024-12-27 12:25:07 +01:00
c066c8407f fix: align go version of Dockerfile with go modfile 2024-12-27 12:24:48 +01:00
16 changed files with 79 additions and 298 deletions

View File

@@ -4,14 +4,6 @@ on:
push: push:
branches: branches:
- main - main
paths:
- "go.mod"
- "go.sum"
- "**/*.go"
- "config.example.yaml"
- "Dockerfile"
- "Makefile"
workflow_dispatch:
env: env:
DOCKER_REGISTRY: gitea.t000-n.de DOCKER_REGISTRY: gitea.t000-n.de
@@ -22,9 +14,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup go - name: Setup go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -52,90 +44,41 @@ jobs:
build_and_push: build_and_push:
name: Build and push name: Build and push
strategy:
matrix:
arch: [amd64, arm64]
needs: needs:
- test - test
runs-on: runs-on: ubuntu-latest
- ubuntu-latest
- linux_${{ matrix.arch }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- name: Login to Registry - name: Login to Registry
uses: docker/login-action@v3 uses: docker/login-action@v2
with: with:
registry: ${{ env.DOCKER_REGISTRY }} registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.REGISTRY_USER }} username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Get Metadata - name: Get Metadata
id: meta id: meta
run: | run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_OUTPUT echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/${{ matrix.arch }} platforms: |
linux/amd64
linux/arm64
push: true push: true
provenance: false
build-args: GOARCH=${{ matrix.arch }}
tags: | tags: |
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-${{ matrix.arch }} ${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:latest
create_tag:
name: Create tag
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.new-tag }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
- uses: https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment@0.1.20
id: tag
with:
token: ${{ secrets.GITEA_TOKEN }}
prerelease: ${{ github.event_name == 'workflow_dispatch' }}
- run: |
git tag ${{ steps.tag.outputs.new-tag }}
git push origin ${{ steps.tag.outputs.new-tag }}
- name: Set output
run: |
echo "tag=${{ steps.tag.outputs.new-tag }}" >> $GITHUB_OUTPUT
create_manifest:
name: Create manifest
needs:
- build_and_push
- create_tag
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Get Metadata
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Create manifest
run: |
docker manifest create ${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ needs.create_tag.outputs.tag }} \
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-amd64 \
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-arm64
docker manifest push ${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ needs.create_tag.outputs.tag }}

View File

@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup go - name: Setup go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true

View File

@@ -1,14 +1,13 @@
FROM golang:1.25-alpine as build FROM golang:1.20-alpine
ARG GOARCH=amd64
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} \
go build -trimpath -ldflags="-s -w" -o main .
FROM gcr.io/distroless/static-debian12 COPY go.mod go.sum ./
COPY --from=build /app/main /
CMD ["/main"] RUN go mod download
COPY . .
RUN go build -o main .
CMD ["./main"]

View File

@@ -38,7 +38,6 @@ domains:
- www - www
check_interval: 0 0 0/6 * * * * check_interval: 0 0 0/6 * * * *
mode: Scheduled mode: Scheduled
log_level: info
``` ```
The config file is expected to be in the same directory as the binary and called `config.yaml`. For the OCR image, the root directory is `/app`. The config file is expected to be in the same directory as the binary and called `config.yaml`. For the OCR image, the root directory is `/app`.

View File

@@ -21,4 +21,3 @@ domains:
- www - www
check_interval: 0 0 0/6 * * * * check_interval: 0 0 0/6 * * * *
mode: Scheduled mode: Scheduled
log_level: info

5
go.mod
View File

@@ -1,16 +1,15 @@
module realdnydns module realdnydns
go 1.25.0 go 1.20
require ( require (
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-co-op/gocron/v2 v2.18.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/stretchr/testify v1.8.4 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
) )

6
go.sum
View File

@@ -4,7 +4,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -31,9 +30,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

59
main.go
View File

@@ -2,9 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"log/slog"
"os"
"strings"
"realdnydns/pkg/config" "realdnydns/pkg/config"
"realdnydns/pkg/dnsProvider" "realdnydns/pkg/dnsProvider"
@@ -18,84 +15,48 @@ import (
) )
func main() { func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
configClient := config.Config{} configClient := config.Config{}
err := configClient.Load("config.yaml") err := configClient.Load("config.yaml")
if err != nil { if err != nil {
logger.Error("Failed to load config file", slog.String("error", err.Error()))
panic(err) panic(err)
} }
if configClient.LogLevel != "" {
logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.Level(config.LogLevelMap[strings.ToLower(configClient.LogLevel)]),
}))
}
var externalIpProvider externalIpProvider.ExternalIpProvider var externalIpProvider externalIpProvider.ExternalIpProvider
switch configClient.ExternalIPProvider.Type { switch configClient.ExternalIPProvider.Type {
case "plain": case "plain":
logger.Info("Using plain external IP provider", slog.String("external_ip_provider", "plain"))
var plainConfig plainExternalIpProvider.PlainExternalIpProviderConfig var plainConfig plainExternalIpProvider.PlainExternalIpProviderConfig
err := configClient.ExternalIPProvider.ProviderConfig.Decode(&plainConfig) err := configClient.ExternalIPProvider.ProviderConfig.Decode(&plainConfig)
if err != nil { if err != nil {
logger.Error("Failed to create config",
slog.String("external_ip_provider", "plain"),
slog.String("error", err.Error()),
)
panic(err) panic(err)
} }
externalIpProvider, err = plainExternalIpProvider.New(plainConfig) externalIpProvider, err = plainExternalIpProvider.New(plainConfig)
if err != nil { if err != nil {
logger.Error("Failed to create plain external IP provider",
slog.String("external_ip_provider", "plain"),
slog.String("error", err.Error()),
)
panic(err) panic(err)
} }
default: default:
logger.Error("Unknown external IP provider", slog.String("external_ip_provider", configClient.ExternalIPProvider.Type))
panic(fmt.Errorf("unknown external IP provider: %s", configClient.ExternalIPProvider.Type)) panic(fmt.Errorf("unknown external IP provider: %s", configClient.ExternalIPProvider.Type))
} }
var dnsProvider dnsProvider.DNSProvider var dnsProvider dnsProvider.DNSProvider
switch configClient.DNSProvider.Type { switch configClient.DNSProvider.Type {
case "ionos": case "ionos":
logger.Info("Using IONOS DNS provider", slog.String("dns_provider", "ionos"))
var ionosConfig ionos.IONOSConfig var ionosConfig ionos.IONOSConfig
err := configClient.DNSProvider.ProviderConfig.Decode(&ionosConfig) err := configClient.DNSProvider.ProviderConfig.Decode(&ionosConfig)
if err != nil { if err != nil {
logger.Error("Failed to create IONOS DNS provider",
slog.String("dns_provider", "ionos"),
slog.String("error", err.Error()),
)
panic(err) panic(err)
} }
dnsProvider, err = ionos.NewIonos(&ionosConfig) dnsProvider, err = ionos.NewIonos(&ionosConfig)
if err != nil { if err != nil {
logger.Error("Failed to create IONOS DNS provider",
slog.String("dns_provider", "ionos"),
slog.String("error", err.Error()),
)
panic(err) panic(err)
} }
default: default:
logger.Error("Unknown DNS provider", slog.String("dns_provider", configClient.DNSProvider.Type))
panic(fmt.Errorf("unknown DNS provider: %s", configClient.DNSProvider.Type)) panic(fmt.Errorf("unknown DNS provider: %s", configClient.DNSProvider.Type))
} }
var notificationProvider notificationProvider.NotificationProvider var notificationProvider notificationProvider.NotificationProvider
switch configClient.NotificationProvider.Type { switch configClient.NotificationProvider.Type {
case "gotify": case "gotify":
logger.Info("Using Gotify notification provider", slog.String("notification_provider", "gotify"))
var gotifyConfig gotify.NotificationProviderImplGotifyConfig var gotifyConfig gotify.NotificationProviderImplGotifyConfig
err := configClient.NotificationProvider.ProviderConfig.Decode(&gotifyConfig) err := configClient.NotificationProvider.ProviderConfig.Decode(&gotifyConfig)
if err != nil { if err != nil {
@@ -104,39 +65,29 @@ func main() {
notificationProvider, err = gotify.New(gotifyConfig) notificationProvider, err = gotify.New(gotifyConfig)
if err != nil { if err != nil {
logger.Error("Failed to create Gotify notification provider",
slog.String("notification_provider", "gotify"),
slog.String("error", err.Error()),
)
panic(err) panic(err)
} }
default: default:
logger.Info("Using console notification provider", slog.String("notification_provider", "console"))
notificationProvider = notificationProviderConsole.New() notificationProvider = notificationProviderConsole.New()
} }
rdd := realDynDns.New(externalIpProvider, dnsProvider, notificationProvider, configClient.Domains, logger.With(slog.String("service", "realDynDns"))) rdd := realDynDns.New(externalIpProvider, dnsProvider, notificationProvider, configClient.Domains)
switch configClient.Mode { switch configClient.Mode {
case config.ScheduledMode: case config.ScheduledMode:
logger.Info("Running in scheduled mode", slog.String("interval", configClient.CheckInterval))
schedule, job, err := rdd.RunWithSchedule(configClient.CheckInterval) schedule, job, err := rdd.RunWithSchedule(configClient.CheckInterval)
if err != nil { if err != nil {
logger.Error("Failed to create scheduler", slog.String("error", err.Error()))
panic(err) panic(err)
} }
logger.Info("Next run:", slog.String("time", job.NextRun().String())) fmt.Println("Starting scheduler")
fmt.Println("Next run:", job.NextRun())
schedule.StartBlocking() schedule.StartBlocking()
case config.RunOnceMode: case config.RunOnceMode:
logger.Info("Running in run once mode") numberOfChanges, err := rdd.RunOnce()
_, err := rdd.RunOnce()
if err != nil { if err != nil {
logger.Error("Failed to run once", slog.String("error", err.Error()))
panic(err) panic(err)
} }
fmt.Println("Number of changes:", numberOfChanges)
} }
} }

View File

@@ -3,9 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"os" "os"
"strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -17,7 +15,6 @@ type Config struct {
NotificationProvider NotificationProviderConfig `yaml:"notification_provider,omitempty"` NotificationProvider NotificationProviderConfig `yaml:"notification_provider,omitempty"`
Domains []DomainConfig `yaml:"domains"` Domains []DomainConfig `yaml:"domains"`
CheckInterval string `yaml:"check_interval"` CheckInterval string `yaml:"check_interval"`
LogLevel string `yaml:"log_level"`
} }
const ( const (
@@ -25,18 +22,6 @@ const (
ScheduledMode = "Scheduled" ScheduledMode = "Scheduled"
) )
var LogLevelMap = map[string]slog.Level{
"debug": slog.LevelDebug,
"info": slog.LevelInfo,
"warn": slog.LevelWarn,
"error": slog.LevelError,
}
func isValidLogLevel(level string) bool {
_, ok := LogLevelMap[strings.ToLower(level)]
return ok
}
type DomainConfig struct { type DomainConfig struct {
TLD string `yaml:"tld"` TLD string `yaml:"tld"`
Subdomains []string `yaml:"subdomains"` Subdomains []string `yaml:"subdomains"`
@@ -83,9 +68,5 @@ func (c *Config) validate() error {
return errors.New("check interval must be set when mode is 'Scheduled'") return errors.New("check interval must be set when mode is 'Scheduled'")
} }
if c.LogLevel != "" && !isValidLogLevel(c.LogLevel) {
return fmt.Errorf("log level must be one of 'debug', 'info', 'warn', 'error', but got %s", c.LogLevel)
}
return nil return nil
} }

View File

@@ -7,7 +7,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"realdnydns/model/common" "realdnydns/model/common"
) )
@@ -19,7 +18,7 @@ type IonosAPI interface {
SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
GetZoneId(tld string) (string, error) GetZoneId(tld string) (string, error)
GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error)
HttpCall(method string, url string, body io.Reader, queryParams map[string]string) (*http.Response, error) HttpCall(method string, url string, body io.Reader) (*http.Response, error)
} }
type IonosAPIImpl struct { type IonosAPIImpl struct {
@@ -77,16 +76,8 @@ func New(APIKey string, BaseURL string) IonosAPI {
} }
} }
func (i *IonosAPIImpl) HttpCall(method string, path string, body io.Reader, queryParams map[string]string) (*http.Response, error) { func (i *IonosAPIImpl) HttpCall(method string, url string, body io.Reader) (*http.Response, error) {
requestUrl, _ := url.Parse(i.BaseURL + path) req, err := http.NewRequest(method, i.BaseURL+url, body)
query := requestUrl.Query()
for key, value := range queryParams {
query.Add(key, value)
}
requestUrl.RawQuery = query.Encode()
req, err := http.NewRequest(method, requestUrl.String(), body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -99,7 +90,7 @@ func (i *IonosAPIImpl) HttpCall(method string, path string, body io.Reader, quer
} }
func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) { func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
res, err := i.HttpCall("GET", "/v1/zones", nil, nil) res, err := i.HttpCall("GET", "/v1/zones", nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -120,14 +111,7 @@ func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
} }
func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) { func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) {
var domain string res, err := i.HttpCall("GET", "/v1/zones/"+zoneId, nil)
if subdomain == "@" || subdomain == "" {
domain = tld
} else {
domain = subdomain + "." + tld
}
res, err := i.HttpCall("GET", "/v1/zones/"+zoneId, nil, map[string]string{"recordName": domain, "recordType": recordType})
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -138,6 +122,13 @@ func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string,
zone := ZoneResponse{} zone := ZoneResponse{}
json.Unmarshal(responseBody, &zone) json.Unmarshal(responseBody, &zone)
var domain string
if subdomain == "@" || subdomain == "" {
domain = tld
} else {
domain = subdomain + "." + tld
}
for _, record := range zone.Records { for _, record := range zone.Records {
if record.Type == recordType && record.Name == domain { if record.Type == recordType && record.Name == domain {
return record.Id, nil return record.Id, nil
@@ -168,7 +159,7 @@ func (i *IonosAPIImpl) SetARecord(tld string, subdomain string, ip net.IP, ttl i
return nil, err return nil, err
} }
res, err := i.HttpCall("PUT", "/v1/zones/"+zoneId+"/records/"+recordId, bytes.NewReader(changeRecordRequest), nil) res, err := i.HttpCall("PUT", "/v1/zones/"+zoneId+"/records/"+recordId, bytes.NewReader(changeRecordRequest))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -203,7 +194,7 @@ func (ionos *IonosAPIImpl) GetARecord(tld string, subdomain string) (*common.ARe
return nil, err return nil, err
} }
res, err := ionos.HttpCall("GET", "/v1/zones/"+zoneId+"/records/"+recordId, nil, nil) res, err := ionos.HttpCall("GET", "/v1/zones/"+zoneId+"/records/"+recordId, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -76,8 +76,7 @@ func utilMockServerImpl() func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { if r.Method == "GET" {
if r.RequestURI == "/v1/zones" { if r.RequestURI == "/v1/zones" {
response = zonesResponseJson response = zonesResponseJson
} else if r.RequestURI == "/v1/zones/1234567890?recordName=example.com&recordType=A" || } else if r.RequestURI == "/v1/zones/1234567890" {
r.RequestURI == "/v1/zones/1234567890?recordName=sub.example.com&recordType=A" {
response = zoneResponseJson response = zoneResponseJson
} else if r.RequestURI == "/v1/zones/1234567890/records/abcdefghij" { } else if r.RequestURI == "/v1/zones/1234567890/records/abcdefghij" {
response = recordResponseSubJson response = recordResponseSubJson
@@ -107,7 +106,7 @@ func TestHttpCall(t *testing.T) {
func testHttpCallGet(api IonosAPI) func(t *testing.T) { func testHttpCallGet(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
res, err := api.HttpCall("GET", "/v1/zones", nil, nil) res, err := api.HttpCall("GET", "/v1/zones", nil)
if err != nil { if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err) t.Fatalf("HttpCall() returned unexpected error: %v", err)
@@ -121,7 +120,7 @@ func testHttpCallGet(api IonosAPI) func(t *testing.T) {
func testHttpCallPut(api IonosAPI) func(t *testing.T) { func testHttpCallPut(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
res, err := api.HttpCall("PUT", "/v1/zones/1234567890/records/abcdefghij", nil, nil) res, err := api.HttpCall("PUT", "/v1/zones/1234567890/records/abcdefghij", nil)
if err != nil { if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err) t.Fatalf("HttpCall() returned unexpected error: %v", err)
@@ -142,7 +141,7 @@ func testHttpCallNonExistingEndpoint() func(t *testing.T) {
api := New("dummyKey", mockServer.URL) api := New("dummyKey", mockServer.URL)
res, err := api.HttpCall("GET", "/v1/nonExistingEndpoint", nil, nil) res, err := api.HttpCall("GET", "/v1/nonExistingEndpoint", nil)
if err != nil { if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err) t.Fatalf("HttpCall() returned unexpected error: %v", err)

View File

@@ -82,7 +82,7 @@ func (m *MockIonosAPI) GetZoneId(tld string) (string, error) {
return m.GetZoneIdFunc(tld) return m.GetZoneIdFunc(tld)
} }
func (m *MockIonosAPI) HttpCall(method string, url string, body io.Reader, queryParams map[string]string) (*http.Response, error) { func (m *MockIonosAPI) HttpCall(method string, url string, body io.Reader) (*http.Response, error) {
return m.HttpCallFunc(method, url, body) return m.HttpCallFunc(method, url, body)
} }

View File

@@ -2,7 +2,6 @@ package externalIpProvider
import ( import (
"errors" "errors"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -41,15 +40,12 @@ func (p *ExternalIpProviderImplPlain) GetExternalIp() (net.IP, error) {
} }
if res.StatusCode != 200 { if res.StatusCode != 200 {
res.Body.Close()
return nil, errors.New("unexpected status code") return nil, errors.New("unexpected status code")
} }
responseBody, err := io.ReadAll(res.Body) responseBody := make([]byte, res.ContentLength)
res.Body.Close() res.Body.Read(responseBody)
if err != nil { defer res.Body.Close()
return nil, err
}
parsedIp := net.ParseIP(string(responseBody)) parsedIp := net.ParseIP(string(responseBody))
if parsedIp == nil { if parsedIp == nil {

View File

@@ -2,8 +2,6 @@ package realDynDns
import ( import (
"fmt" "fmt"
"log/slog"
"sync"
"time" "time"
"realdnydns/pkg/config" "realdnydns/pkg/config"
@@ -19,7 +17,6 @@ type ChangeDetector struct {
dnsProvider dnsProvider.DNSProvider dnsProvider dnsProvider.DNSProvider
notificationProvider notificationProvider.NotificationProvider notificationProvider notificationProvider.NotificationProvider
domains []config.DomainConfig domains []config.DomainConfig
logger *slog.Logger
} }
func New( func New(
@@ -27,14 +24,12 @@ func New(
dnsProvider dnsProvider.DNSProvider, dnsProvider dnsProvider.DNSProvider,
notificationProvider notificationProvider.NotificationProvider, notificationProvider notificationProvider.NotificationProvider,
domains []config.DomainConfig, domains []config.DomainConfig,
logger *slog.Logger,
) ChangeDetector { ) ChangeDetector {
return ChangeDetector{ return ChangeDetector{
externalIpProvider: externalIpProvider, externalIpProvider: externalIpProvider,
dnsProvider: dnsProvider, dnsProvider: dnsProvider,
notificationProvider: notificationProvider, notificationProvider: notificationProvider,
domains: domains, domains: domains,
logger: logger,
} }
} }
@@ -63,90 +58,37 @@ func (c *ChangeDetector) RunOnce() (int, error) {
} }
func (c *ChangeDetector) detectAndApplyChanges() (int, error) { func (c *ChangeDetector) detectAndApplyChanges() (int, error) {
c.logger.Info("Detecting and applying changes")
externalIp, err := c.externalIpProvider.GetExternalIp() externalIp, err := c.externalIpProvider.GetExternalIp()
if err != nil { if err != nil {
c.logger.Error("Failed to retrieve external IP", slog.String("error", err.Error()))
return 0, err return 0, err
} }
var wg sync.WaitGroup var numberUpdated int
numberUpdatedChannel := make(chan int)
for _, domain := range c.domains { for _, domain := range c.domains {
for _, subdomain := range domain.Subdomains { for _, subdomain := range domain.Subdomains {
wg.Add(1) currentRecord, err := c.dnsProvider.GetRecord(domain.TLD, subdomain)
if err != nil {
return numberUpdated, err
}
go func(domain config.DomainConfig, subdomain string) { if currentRecord.IP != externalIp.String() {
defer wg.Done() err = c.notificationProvider.SendNotification(
fmt.Sprintf("Update %s.%s", subdomain, domain.TLD),
c.logger.Info("Checking record", fmt.Sprintf("The IP of %s has changed from %s to %s", domain.TLD, currentRecord.IP, externalIp.String()),
slog.String("tld", domain.TLD),
slog.String("subdomain", subdomain),
) )
currentRecord, err := c.dnsProvider.GetRecord(domain.TLD, subdomain)
if err != nil { if err != nil {
c.logger.Error("Failed to retrieve record", return numberUpdated, err
slog.String("error", err.Error()),
slog.String("tld", domain.TLD),
slog.String("subdomain", subdomain),
)
return
} }
if currentRecord.IP != externalIp.String() { _, err = c.dnsProvider.UpdateRecord(domain.TLD, subdomain, externalIp, currentRecord.TTL, currentRecord.Prio, currentRecord.Disabled)
c.logger.Info("Record has changed", numberUpdated++
slog.String("tld", domain.TLD), if err != nil {
slog.String("subdomain", subdomain), return numberUpdated, err
slog.String("current_ip", currentRecord.IP),
slog.String("external_ip", externalIp.String()),
)
err = c.notificationProvider.SendNotification(
fmt.Sprintf("Update %s.%s", subdomain, domain.TLD),
fmt.Sprintf("The IP of %s has changed from %s to %s", domain.TLD, currentRecord.IP, externalIp.String()),
)
if err != nil {
c.logger.Warn("Failed to send notification",
slog.String("error", err.Error()),
)
return
}
c.logger.Info("Updating record",
slog.String("tld", domain.TLD),
slog.String("subdomain", subdomain),
slog.String("current_ip", currentRecord.IP),
slog.String("external_ip", externalIp.String()),
)
_, err = c.dnsProvider.UpdateRecord(domain.TLD, subdomain, externalIp, currentRecord.TTL, currentRecord.Prio, currentRecord.Disabled)
if err != nil {
c.logger.Error("Failed to update record",
slog.String("error", err.Error()),
slog.String("tld", domain.TLD),
slog.String("subdomain", subdomain),
)
return
}
numberUpdatedChannel <- 1
} }
}(domain, subdomain) }
} }
} }
go func() {
wg.Wait()
close(numberUpdatedChannel)
}()
numberUpdated := 0
for v := range numberUpdatedChannel {
numberUpdated += v
}
c.logger.Info("Run completed", slog.Int("number_of_changes", numberUpdated))
return numberUpdated, nil return numberUpdated, nil
} }

View File

@@ -1,7 +1,6 @@
package realDynDns package realDynDns
import ( import (
"log/slog"
"net" "net"
"realdnydns/model/common" "realdnydns/model/common"
"realdnydns/pkg/config" "realdnydns/pkg/config"
@@ -73,9 +72,7 @@ func testDetectAndApplyChangesWithChanges() func(t *testing.T) {
"@", "@",
}, },
}, },
}, })
slog.Default(),
)
numberUpdated, err := changeDetector.RunOnce() numberUpdated, err := changeDetector.RunOnce()
if err != nil { if err != nil {
@@ -104,9 +101,7 @@ func testDetectAndApplyChangesWithoutChanges() func(t *testing.T) {
"@", "@",
}, },
}, },
}, })
slog.Default(),
)
numberUpdated, err := changeDetector.RunOnce() numberUpdated, err := changeDetector.RunOnce()
if err != nil { if err != nil {

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"matchPackageNames": ["golang", "gomod", "go"],
"groupName": "go version",
"updateTypes": ["major", "minor", "patch"]
}
]
}