8 Commits
0.0.2 ... 0.0.6

Author SHA1 Message Date
485d5b6f22 Revert "choreL: update test tooling (#72)" (#74)
All checks were successful
CD / Create tag (push) Successful in 5s
CD / test (push) Successful in 1m43s
CD / Build and push (amd64) (push) Successful in 37s
CD / Build and push (arm64) (push) Successful in 2m11s
CD / Create manifest (push) Successful in 7s
This reverts commit 2bfb4419d3.

Reviewed-on: #74
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2026-01-10 21:25:12 +01:00
2bfb4419d3 choreL: update test tooling (#72)
Some checks failed
CD / Create tag (push) Successful in 5s
CD / test (push) Failing after 3m2s
CD / Build and push (amd64) (push) Has been skipped
CD / Build and push (arm64) (push) Has been skipped
CD / Create manifest (push) Has been skipped
Reviewed-on: #72
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2026-01-10 21:18:09 +01:00
ac73cf14eb refactor: to use ionosDnsClient (#71)
All checks were successful
CD / Create tag (push) Successful in 14s
CD / test (push) Successful in 32s
CD / Build and push (amd64) (push) Successful in 43s
CD / Build and push (arm64) (push) Successful in 2m23s
CD / Create manifest (push) Successful in 34s
Reviewed-on: #71
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2026-01-10 21:00:43 +01:00
15431166e0 chore(deps): update https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment action to v0.1.22 (#68)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment](https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment) | action | patch | `0.1.20` → `0.1.22` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the Dependency Dashboard for more information.

---

### Release Notes

<details>
<summary>t.behrendt/conventional-semantic-git-tag-increment (https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment)</summary>

### [`v0.1.22`](https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment/compare/0.1.21...0.1.22)

[Compare Source](https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment/compare/0.1.21...0.1.22)

### [`v0.1.21`](https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment/compare/0.1.20...0.1.21)

[Compare Source](https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment/compare/0.1.20...0.1.21)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi42OS4yIiwidXBkYXRlZEluVmVyIjoiNDIuNjkuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiYWN0aW9uIiwiZGVwcyJdfQ==-->

Reviewed-on: #68
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
2026-01-08 19:25:18 +01:00
caf13306d4 chore(deps): update module github.com/go-co-op/gocron/v2 to v2.19.0 (#58)
All checks were successful
CD / Create tag (push) Successful in 14s
CD / test (push) Successful in 42s
CD / Build and push (amd64) (push) Successful in 37s
CD / Build and push (arm64) (push) Successful in 2m2s
CD / Create manifest (push) Successful in 9s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/go-co-op/gocron/v2](https://github.com/go-co-op/gocron) | `v2.18.0` -> `v2.19.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-co-op%2fgocron%2fv2/v2.19.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-co-op%2fgocron%2fv2/v2.18.0/v2.19.0?slim=true) |

---

### Release Notes

<details>
<summary>go-co-op/gocron (github.com/go-co-op/gocron/v2)</summary>

### [`v2.19.0`](https://github.com/go-co-op/gocron/releases/tag/v2.19.0)

[Compare Source](https://github.com/go-co-op/gocron/compare/v2.18.2...v2.19.0)

#### What's Changed

- feat: add scheduler lifecycle monitoring <> [#&#8203;785](https://github.com/go-co-op/gocron/issues/785) by [@&#8203;iyashjayesh](https://github.com/iyashjayesh) in [#&#8203;889](https://github.com/go-co-op/gocron/pull/889)
- feat: expose default cron implementation by [@&#8203;JohnRoesler](https://github.com/JohnRoesler) in [#&#8203;906](https://github.com/go-co-op/gocron/pull/906)
- build(deps): bump golangci/golangci-lint-action from 9.1.0 to 9.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;903](https://github.com/go-co-op/gocron/pull/903)

**Full Changelog**: <https://github.com/go-co-op/gocron/compare/v2.18.2...v2.19.0>

### [`v2.18.2`](https://github.com/go-co-op/gocron/releases/tag/v2.18.2)

[Compare Source](https://github.com/go-co-op/gocron/compare/v2.18.1...v2.18.2)

#### What's Changed

- build(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;899](https://github.com/go-co-op/gocron/pull/899)
- build(deps): bump actions/checkout from 5 to 6 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;898](https://github.com/go-co-op/gocron/pull/898)
- fix: calling start multiple times should no-op by [@&#8203;JohnRoesler](https://github.com/JohnRoesler) in [#&#8203;901](https://github.com/go-co-op/gocron/pull/901)

**Full Changelog**: <https://github.com/go-co-op/gocron/compare/v2.18.1...v2.18.2>

### [`v2.18.1`](https://github.com/go-co-op/gocron/releases/tag/v2.18.1)

[Compare Source](https://github.com/go-co-op/gocron/compare/v2.18.0...v2.18.1)

#### What's Changed

- build(deps): bump golangci/golangci-lint-action from 8.0.0 to 9.0.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;894](https://github.com/go-co-op/gocron/pull/894)
- fix: WithStartDateTimePast now correctly calculates from past time by [@&#8203;JohnRoesler](https://github.com/JohnRoesler) in [#&#8203;897](https://github.com/go-co-op/gocron/pull/897)

**Full Changelog**: <https://github.com/go-co-op/gocron/compare/v2.18.0...v2.18.1>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4xMC41IiwidXBkYXRlZEluVmVyIjoiNDIuNTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://gitea.t000-n.de/t.behrendt/realDynDNS/pulls/58
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
2025-12-21 10:45:46 +01:00
9291687b35 chore(deps): update docker/setup-buildx-action digest to 8d2750c (#66)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) ([changelog](e468171a9d..8d2750c68a)) | action | digest | `e468171` -> `8d2750c` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi40Ny4wIiwidXBkYXRlZEluVmVyIjoiNDIuNDcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiYWN0aW9uIiwiZGVwcyJdfQ==-->

Reviewed-on: #66
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
2025-12-19 19:35:10 +01:00
735a106826 chore(deps): update actions/cache action to v5 (#63)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/cache](https://github.com/actions/cache) | action | major | `v4` -> `v5` |

---

### Release Notes

<details>
<summary>actions/cache (actions/cache)</summary>

### [`v5`](https://github.com/actions/cache/compare/v4...v5)

[Compare Source](https://github.com/actions/cache/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zNy4xIiwidXBkYXRlZEluVmVyIjoiNDIuMzcuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiYWN0aW9uIiwiZGVwcyJdfQ==-->

Reviewed-on: #63
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
2025-12-19 19:34:23 +01:00
d0bce1b77c chore(config): migrate Renovate config (#64)
The Renovate config in this repository needs migrating. Typically this is because one or more configuration options you are using have been renamed.

  You don't need to merge this PR right away, because Renovate will continue to migrate these fields internally each time it runs. But later some of these fields may be fully deprecated and the migrations removed. So it's a good idea to merge this migration PR soon.

🔕 **Ignore**: Close this PR and you won't be reminded about config migration again, but one day your current config may no longer be valid.

 Got questions? Does something look wrong to you? Please don't hesitate to [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: #64
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
2025-12-14 09:16:22 +01:00
10 changed files with 1616 additions and 638 deletions

View File

@@ -37,7 +37,7 @@ jobs:
go.sum
- name: cache go
id: cache-go
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: |
/go_path
@@ -64,7 +64,7 @@ jobs:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Login to Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
@@ -97,7 +97,7 @@ jobs:
- 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
- uses: https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment@9841c96e72d4fc1ecec620e91e213895341cce86 # 0.1.22
id: tag
with:
token: ${{ secrets.GITEA_TOKEN }}

View File

@@ -28,7 +28,7 @@ jobs:
go.sum
- name: cache go
id: cache-go
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: |
/go_path

View File

@@ -8,6 +8,10 @@ dns_provider:
config:
api_key: <your-api-key>
base_url: https://api.hosting.ionos.com/dns
# Optional: default TTL to use when patching records with missing TTL (default: 300)
# default_ttl: 300
# Optional: default priority to use when patching records with missing priority (default: 0)
# default_prio: 0
notification_provider:
type: gotify
config:

4
go.mod
View File

@@ -3,13 +3,15 @@ module realdnydns
go 1.25.0
require (
gitea.t000-n.de/t.behrendt/ionosDnsClient v1.0.2
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
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/oapi-codegen/runtime v1.1.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/atomic v1.11.0 // indirect

14
go.sum
View File

@@ -1,13 +1,19 @@
gitea.t000-n.de/t.behrendt/ionosDnsClient v1.0.2 h1:EWz4kLLv7lSZx/F8K0/WxN5xeVNh7z4qbeDurZYBcNk=
gitea.t000-n.de/t.behrendt/ionosDnsClient v1.0.2/go.mod h1:HZfdMF7X9LK/3FP1jJVzMWbM5BcgcZg/Qwx2WClFipg=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -16,6 +22,8 @@ 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -24,6 +32,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
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=
@@ -31,8 +40,7 @@ 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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=

View File

@@ -1,224 +0,0 @@
package ionosAPI
import (
"bytes"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/url"
"realdnydns/model/common"
)
/**
* Docs: https://developer.hosting.ionos.com/docs/dns
*/
type IonosAPI interface {
GetARecord(tld string, subdomain string) (*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)
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)
}
type IonosAPIImpl struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
type ZonesResponse []struct {
Name string `json:"name"`
Id string `json:"id"`
Type string `json:"type"`
}
type ZoneResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Records []RecordResponse `json:"records"`
}
type RecordResponse struct {
Id string `json:"id"`
Name string `json:"name"`
RootName string `json:"rootName"`
Type string `json:"type"`
Content string `json:"content"`
ChangeDate string `json:"changeDate"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
type ChangeRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
type ChangeRecordRequest struct {
Content string `json:"content"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
func New(APIKey string, BaseURL string) IonosAPI {
return &IonosAPIImpl{
APIKey: APIKey,
BaseURL: BaseURL,
HTTPClient: &http.Client{},
}
}
func (i *IonosAPIImpl) HttpCall(method string, path string, body io.Reader, queryParams map[string]string) (*http.Response, error) {
requestUrl, _ := url.Parse(i.BaseURL + path)
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 {
return nil, err
}
req.Header.Add("X-API-Key", i.APIKey)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
return i.HTTPClient.Do(req)
}
func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
res, err := i.HttpCall("GET", "/v1/zones", nil, nil)
if err != nil {
return "", err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
zones := []ZoneResponse{}
json.Unmarshal(responseBody, &zones)
for _, z := range zones {
if z.Name == tld {
return z.Id, nil
}
}
return "", errors.New("zone not found")
}
func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) {
var domain string
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 {
return "", err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
zone := ZoneResponse{}
json.Unmarshal(responseBody, &zone)
for _, record := range zone.Records {
if record.Type == recordType && record.Name == domain {
return record.Id, nil
}
}
return "", errors.New("record not found")
}
func (i *IonosAPIImpl) SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
zoneId, err := i.GetZoneId(tld)
if err != nil {
return nil, err
}
recordId, err := i.GetRecordId(zoneId, tld, subdomain, "A")
if err != nil {
return nil, err
}
changeRecordRequest, err := json.Marshal(ChangeRecordRequest{
Content: ip.String(),
TTL: ttl,
Prio: prio,
Disabled: false,
})
if err != nil {
return nil, err
}
res, err := i.HttpCall("PUT", "/v1/zones/"+zoneId+"/records/"+recordId, bytes.NewReader(changeRecordRequest), nil)
if err != nil {
return nil, err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
if res.StatusCode != 200 {
return nil, errors.New("error updating record")
}
changeRecord := ChangeRecord{}
json.Unmarshal(responseBody, &changeRecord)
return &common.ARecord{
Domain: changeRecord.Name,
IP: changeRecord.Content,
TTL: changeRecord.TTL,
Prio: changeRecord.Prio,
Disabled: changeRecord.Disabled,
}, nil
}
func (ionos *IonosAPIImpl) GetARecord(tld string, subdomain string) (*common.ARecord, error) {
zoneId, err := ionos.GetZoneId(tld)
if err != nil {
return nil, err
}
recordId, err := ionos.GetRecordId(zoneId, tld, subdomain, "A")
if err != nil {
return nil, err
}
res, err := ionos.HttpCall("GET", "/v1/zones/"+zoneId+"/records/"+recordId, nil, nil)
if err != nil {
return nil, err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
record := RecordResponse{}
json.Unmarshal(responseBody, &record)
return &common.ARecord{
Domain: record.Name,
IP: record.Content,
TTL: record.TTL,
Prio: record.Prio,
Disabled: record.Disabled,
}, nil
}

View File

@@ -1,285 +0,0 @@
package ionosAPI
import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"testing"
)
func utilMockServerImpl() func(w http.ResponseWriter, r *http.Request) {
zonesResponse := []ZoneResponse{
{
Name: "example.com",
Id: "1234567890",
Type: "NATIVE",
},
{
Name: "notTheExample.org",
Id: "0987654321",
Type: "SLAVE",
},
}
zonesResponseJson, _ := json.Marshal(zonesResponse)
recordResponseSub := RecordResponse{
Id: "abcdefghij",
Name: "example.com",
RootName: "example.com",
Type: "A",
Content: "127.0.0.1",
ChangeDate: "2019-12-09T13:04:25.772Z",
TTL: 300,
Prio: 0,
Disabled: false,
}
recordResponseSubJson, _ := json.Marshal(recordResponseSub)
recordResponseTLD := RecordResponse{
Id: "jihgfedcba",
Name: "sub.example.com",
RootName: "example.com",
Type: "A",
Content: "127.0.0.2",
ChangeDate: "2019-12-09T13:04:25.772Z",
TTL: 300,
Prio: 0,
Disabled: false,
}
recordResponseTLDJson, _ := json.Marshal(recordResponseTLD)
zoneResponse := ZoneResponse{
Id: "1234567890",
Name: "example.com",
Type: "NATIVE",
Records: []RecordResponse{
recordResponseSub,
recordResponseTLD,
},
}
zoneResponseJson, _ := json.Marshal(zoneResponse)
changeRecord := ChangeRecord{
Name: "sub.example.com",
Type: "A",
Content: "127.0.0.1",
TTL: 300,
Prio: 0,
Disabled: false,
}
changeRecordJson, _ := json.Marshal(changeRecord)
return func(w http.ResponseWriter, r *http.Request) {
var response []byte
if r.Method == "GET" {
if r.RequestURI == "/v1/zones" {
response = zonesResponseJson
} else if r.RequestURI == "/v1/zones/1234567890?recordName=example.com&recordType=A" ||
r.RequestURI == "/v1/zones/1234567890?recordName=sub.example.com&recordType=A" {
response = zoneResponseJson
} else if r.RequestURI == "/v1/zones/1234567890/records/abcdefghij" {
response = recordResponseSubJson
} else if r.RequestURI == "/v1/zones/1234567890/records/jihgfedcba" {
response = recordResponseTLDJson
}
} else if r.Method == "PUT" {
response = changeRecordJson
} else {
response = []byte("404")
}
w.Write(response)
}
}
func TestHttpCall(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
defer mockServer.Close()
ionosAPI := New("dummyKey", mockServer.URL)
t.Run("returns response for GET request", testHttpCallGet(ionosAPI))
t.Run("returns response for PUT request", testHttpCallPut(ionosAPI))
t.Run("returns error for non existing endpoint", testHttpCallNonExistingEndpoint())
}
func testHttpCallGet(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
res, err := api.HttpCall("GET", "/v1/zones", nil, nil)
if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err)
}
if res.StatusCode != 200 {
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 200", res.StatusCode)
}
}
}
func testHttpCallPut(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
res, err := api.HttpCall("PUT", "/v1/zones/1234567890/records/abcdefghij", nil, nil)
if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err)
}
if res.StatusCode != 200 {
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 200", res.StatusCode)
}
}
}
func testHttpCallNonExistingEndpoint() func(t *testing.T) {
return func(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
}))
defer mockServer.Close()
api := New("dummyKey", mockServer.URL)
res, err := api.HttpCall("GET", "/v1/nonExistingEndpoint", nil, nil)
if err != nil {
t.Fatalf("HttpCall() returned unexpected error: %v", err)
}
if res.StatusCode != 404 {
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 404", res.StatusCode)
}
}
}
func TestGetZoneId(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
ionosAPI := New("dummyKey", mockServer.URL)
t.Run("returns zoneId for tracked domain", testGetZoneIdSuccess(ionosAPI))
t.Run("returns error for non tracked domain", testGetZoneIdNoMatch(ionosAPI))
}
func testGetZoneIdSuccess(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
zoneId, err := api.GetZoneId("example.com")
if err != nil {
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
}
if zoneId != "1234567890" {
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of 1234567890", zoneId)
}
}
}
func testGetZoneIdNoMatch(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
zoneId, err := api.GetZoneId("nonTrackedDomain.com")
if err == nil || zoneId != "" {
t.Fatalf("GetZoneId() did not return an error for a non tracked domain")
}
}
}
func TestGetRecordId(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
defer mockServer.Close()
ionosAPI := New("dummyKey", mockServer.URL)
t.Run("returns zoneId for top level domain", testGetRecordIdForTopLevelDomain(ionosAPI))
t.Run("returns zoneId for subdomain", testGetRecordIdForSubdomain(ionosAPI))
t.Run("returns error for untracked subdomain", testGetRecordIdWithUntrackedSubdomain(ionosAPI))
}
func testGetRecordIdForTopLevelDomain(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
recordId, err := api.GetRecordId("1234567890", "example.com", "", "A")
if err != nil {
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
}
if recordId != "abcdefghij" {
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of abcdefghij", recordId)
}
}
}
func testGetRecordIdForSubdomain(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
recordId, err := api.GetRecordId("1234567890", "example.com", "sub", "A")
if err != nil {
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
}
if recordId != "jihgfedcba" {
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of jihgfedcba", recordId)
}
}
}
func testGetRecordIdWithUntrackedSubdomain(api IonosAPI) func(t *testing.T) {
return func(t *testing.T) {
recordId, err := api.GetRecordId("1234567890", "example.com", "untrackedSub", "A")
if err == nil && recordId == "" {
t.Fatalf("GetZoneId() did not return an error for a non tracked domain")
}
}
}
func TestGetARecord(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
defer mockServer.Close()
ionosAPI := New("dummyKey", mockServer.URL)
t.Run("returns A record for top level domain", testGetARecordFor(ionosAPI, "example.com", ""))
t.Run("returns A record for subdomain", testGetARecordFor(ionosAPI, "example.com", "sub"))
}
func testGetARecordFor(api IonosAPI, tld string, subdomain string) func(t *testing.T) {
return func(t *testing.T) {
var domain string
if subdomain == "" {
domain = tld
} else {
domain = subdomain + "." + tld
}
record, err := api.GetARecord(tld, subdomain)
if err != nil {
t.Fatalf("GetARecord() returned unexpected error: %v", err)
}
if record.Domain != domain {
t.Fatalf("GetARecord() returned unexpected record for: %v instead of %v", record.Domain, domain)
}
}
}
func TestSetARecord(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
defer mockServer.Close()
ionosAPI := New("dummyKey", mockServer.URL)
changedARecord, err := ionosAPI.SetARecord("example.com", "sub", net.ParseIP("127.0.0.1"), 300, 0, false)
if err != nil {
t.Fatalf("SetARecord() returned unexpected error: %v", err)
}
if changedARecord.Domain != "sub.example.com" {
t.Fatalf("SetARecord() returned unexpected record for: %v instead of sub.example.com", changedARecord.Domain)
}
}

View File

@@ -1,20 +1,30 @@
package ionosDnsProvider
import (
"context"
"errors"
"net"
"net/http"
"realdnydns/model/common"
"realdnydns/pkg/dnsProvider"
ionosAPI "realdnydns/pkg/dnsProvider/ionos/api"
"sync"
ionosDnsClient "gitea.t000-n.de/t.behrendt/ionosDnsClient"
)
type IONOS struct {
API ionosAPI.IonosAPI
client *ionosDnsClient.ClientWithResponses
zoneIdMap map[string]string
defaultTtl int
defaultPrio int
zoneIdMapMutex sync.Mutex
}
type IONOSConfig struct {
APIKey string `yaml:"api_key"`
BaseURL string `yaml:"base_url"`
DefaultTTL *int `yaml:"default_ttl,omitempty"`
DefaultPrio *int `yaml:"default_prio,omitempty"`
}
func NewIonos(config *IONOSConfig) (dnsProvider.DNSProvider, error) {
@@ -26,15 +36,231 @@ func NewIonos(config *IONOSConfig) (dnsProvider.DNSProvider, error) {
return nil, errors.New("base_url is required")
}
options := []ionosDnsClient.ClientOption{
ionosDnsClient.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
req.Header.Set("X-API-Key", config.APIKey)
return nil
}),
}
client, err := ionosDnsClient.NewClientWithResponses(config.BaseURL, options...)
if err != nil {
return nil, err
}
// Set default values for TTL and Prio if not provided
defaultTtl := 300
if config.DefaultTTL != nil {
defaultTtl = *config.DefaultTTL
}
defaultPrio := 0
if config.DefaultPrio != nil {
defaultPrio = *config.DefaultPrio
}
return &IONOS{
ionosAPI.New(config.APIKey, config.BaseURL),
client: client,
zoneIdMap: make(map[string]string),
defaultTtl: defaultTtl,
defaultPrio: defaultPrio,
}, nil
}
func (ionos *IONOS) UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
return ionos.API.SetARecord(tld, subdomain, ip, ttl, prio, disabled)
func (i *IONOS) UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
zoneId, err := i.getZoneIdForTld(tld)
if err != nil {
return nil, err
}
domain := assembleTldSubdomainToDomain(tld, subdomain)
recordId, err := i.getRecordIdForDomain(zoneId, domain)
if err != nil {
return nil, err
}
ipString := ip.String()
res, err := i.client.UpdateRecordWithResponse(context.Background(), zoneId, recordId, ionosDnsClient.RecordUpdate{
Content: &ipString,
Ttl: &ttl,
Prio: &prio,
Disabled: &disabled,
})
if err != nil || res.StatusCode() != 200 {
return nil, errors.New("failed to update record")
}
if res.JSON200 == nil {
return nil, errors.New("record response is empty")
}
record := *res.JSON200
if record.Name == nil || record.Content == nil {
return nil, errors.New("record is empty")
}
// Handle optional fields with defaults
recordTtl := 0
if record.Ttl != nil {
recordTtl = *record.Ttl
}
recordPrio := 0
if record.Prio != nil {
recordPrio = *record.Prio
}
recordDisabled := false
if record.Disabled != nil {
recordDisabled = *record.Disabled
}
return &common.ARecord{
Domain: *record.Name,
IP: *record.Content,
TTL: recordTtl,
Prio: recordPrio,
Disabled: recordDisabled,
}, nil
}
func (ionos *IONOS) GetRecord(tld string, subdomain string) (*common.ARecord, error) {
return ionos.API.GetARecord(tld, subdomain)
func (i *IONOS) getZoneIdForTld(tld string) (string, error) {
i.zoneIdMapMutex.Lock()
defer i.zoneIdMapMutex.Unlock()
if zoneId, ok := i.zoneIdMap[tld]; ok {
return zoneId, nil
}
res, err := i.client.GetZonesWithResponse(context.Background())
if err != nil {
return "", err
} else if res.StatusCode() != 200 {
return "", errors.New("failed to get zones")
}
if res.JSON200 == nil {
return "", errors.New("zones response is empty")
}
zones := *res.JSON200
for _, zone := range zones {
if *zone.Name == tld {
if zone.Id == nil || *zone.Id == "" {
return "", errors.New("zone id is empty")
}
i.zoneIdMap[tld] = *zone.Id
return *zone.Id, nil
}
}
return "", errors.New("no zone found")
}
var recordTypeA = "A"
func assembleTldSubdomainToDomain(tld string, subdomain string) string {
if subdomain == "@" || subdomain == "" {
return tld
}
return subdomain + "." + tld
}
func (i *IONOS) getRecordIdForDomain(zoneId string, domain string) (string, error) {
res, err := i.client.GetZoneWithResponse(context.Background(), zoneId, &ionosDnsClient.GetZoneParams{
RecordName: &domain,
RecordType: &recordTypeA,
})
if err != nil || res.StatusCode() != 200 {
return "", errors.New("failed to get zone")
}
if res.JSON200 == nil {
return "", errors.New("zone response is empty")
}
zone := *res.JSON200
for _, record := range *zone.Records {
if *record.Name == domain && *record.Type == ionosDnsClient.RecordTypes("A") {
return *record.Id, nil
}
}
return "", errors.New("no record found")
}
func (i *IONOS) GetRecord(tld string, subdomain string) (*common.ARecord, error) {
zoneId, err := i.getZoneIdForTld(tld)
if err != nil {
return nil, err
}
domain := assembleTldSubdomainToDomain(tld, subdomain)
recordId, err := i.getRecordIdForDomain(zoneId, domain)
if err != nil {
return nil, err
}
res, err := i.client.GetRecordWithResponse(context.Background(), zoneId, recordId)
if err != nil || res.StatusCode() != 200 || res.JSON200 == nil {
return nil, errors.New("failed to get record")
}
record := *res.JSON200
if record.Name == nil || *record.Name == "" || *record.Name != domain {
return nil, errors.New("record name does not match or is empty")
}
if record.Content == nil || *record.Content == "" {
return nil, errors.New("record content is empty")
}
needsUpdate := false
ttl := i.defaultTtl
if record.Ttl != nil {
ttl = *record.Ttl
} else {
needsUpdate = true
}
prio := i.defaultPrio
if record.Prio != nil {
prio = *record.Prio
} else {
needsUpdate = true
}
disabled := false
if record.Disabled != nil {
disabled = *record.Disabled
} else {
needsUpdate = true
}
// realDynDns requires every field to be set, so we need to update the record if any of the fields are missing
if needsUpdate {
ip := net.ParseIP(*record.Content)
if ip == nil {
return nil, errors.New("invalid IP address in record content")
}
updatedRecord, err := i.UpdateRecord(tld, subdomain, ip, ttl, prio, disabled)
if err != nil {
return nil, err
}
return updatedRecord, nil
}
return &common.ARecord{
Domain: *record.Name,
IP: *record.Content,
TTL: ttl,
Prio: prio,
Disabled: disabled,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,17 @@
],
"packageRules": [
{
"matchPackageNames": ["golang", "gomod", "go"],
"matchPackageNames": [
"golang",
"gomod",
"go"
],
"groupName": "go version",
"updateTypes": ["major", "minor", "patch"]
"matchUpdateTypes": [
"major",
"minor",
"patch"
]
}
]
}