autodns: use the right response structure (#2737)

This commit is contained in:
Ludovic Fernandez
2025-12-01 20:50:46 +01:00
committed by GitHub
parent 742741fe05
commit cc83c025b5
9 changed files with 370 additions and 79 deletions

View File

@@ -128,7 +128,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
Value: info.Value,
}}
_, err := d.client.AddTxtRecords(context.Background(), info.EffectiveFQDN, records)
_, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records)
if err != nil {
return fmt.Errorf("autodns: %w", err)
}
@@ -147,7 +147,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
Value: info.Value,
}}
if err := d.client.RemoveTXTRecords(context.Background(), info.EffectiveFQDN, records); err != nil {
_, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records)
if err != nil {
return fmt.Errorf("autodns: %w", err)
}

View File

@@ -43,24 +43,22 @@ func NewClient(username, password string, clientContext int) *Client {
}
}
// AddTxtRecords adds TXT records.
func (c *Client) AddTxtRecords(ctx context.Context, domain string, records []*ResourceRecord) (*Zone, error) {
// AddRecords adds records.
func (c *Client) AddRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {
zoneStream := &ZoneStream{Adds: records}
return c.updateZone(ctx, domain, zoneStream)
}
// RemoveTXTRecords removes TXT records.
func (c *Client) RemoveTXTRecords(ctx context.Context, domain string, records []*ResourceRecord) error {
// RemoveRecords removes records.
func (c *Client) RemoveRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {
zoneStream := &ZoneStream{Removes: records}
_, err := c.updateZone(ctx, domain, zoneStream)
return err
return c.updateZone(ctx, domain, zoneStream)
}
// https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L21090
func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*Zone, error) {
func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*DataZoneResponse, error) {
endpoint := c.BaseURL.JoinPath("zone", domain, "_stream")
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zoneStream)
@@ -68,12 +66,12 @@ func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *Zone
return nil, err
}
var zone *Zone
if err := c.do(req, &zone); err != nil {
var resp *DataZoneResponse
if err := c.do(req, &resp); err != nil {
return nil, err
}
return zone, nil
return resp, nil
}
func (c *Client) do(req *http.Request, result any) error {
@@ -88,7 +86,7 @@ func (c *Client) do(req *http.Request, result any) error {
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode/100 != 2 {
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
return parseError(req, resp)
}
if result == nil {
@@ -131,3 +129,16 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo
return req, nil
}
func parseError(req *http.Request, resp *http.Response) error {
raw, _ := io.ReadAll(resp.Body)
var errAPI APIError
err := json.Unmarshal(raw, &errAPI)
if err != nil {
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
}
return &errAPI
}

View File

@@ -1,6 +1,7 @@
package internal
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
@@ -24,7 +25,7 @@ func mockBuilder() *servermock.Builder[*Client] {
WithJSONHeaders())
}
func TestClient_AddTxtRecords(t *testing.T) {
func TestClient_AddRecords(t *testing.T) {
client := mockBuilder().
Route("POST /zone/example.com/_stream",
servermock.ResponseFromFixture("add_record.json"),
@@ -33,28 +34,81 @@ func TestClient_AddTxtRecords(t *testing.T) {
With("X-Domainrobot-Context", "123")).
Build(t)
records := []*ResourceRecord{{}}
records := []*ResourceRecord{{
Name: "example.com",
TTL: 600,
Type: "TXT",
Value: "txtTXTtxt",
}}
zone, err := client.AddTxtRecords(t.Context(), "example.com", records)
resp, err := client.AddRecords(t.Context(), "example.com", records)
require.NoError(t, err)
expected := &Zone{
Name: "example.com",
ResourceRecords: []*ResourceRecord{{
Name: "example.com",
TTL: 120,
Type: "TXT",
Value: "txt",
Pref: 1,
}},
Action: "xxx",
VirtualNameServer: "yyy",
expected := &DataZoneResponse{
STID: "20251121-appf4923-126284",
CTID: "",
Messages: []ResponseMessage{
{
Text: "string",
Messages: []string{
"string",
},
Objects: []GenericObject{
{
Type: "string",
Value: "string",
},
},
Code: "string",
Status: "SUCCESS",
},
},
Status: &ResponseStatus{
Code: "S0301",
Text: "Zone was updated successfully on the name server.",
Type: "SUCCESS",
},
Object: nil,
Data: []Zone{
{
Name: "example.com",
ResourceRecords: []ResourceRecord{
{
Name: "example.com",
TTL: 120,
Type: "TXT",
Value: "txt",
Pref: 1,
},
},
Action: "xxx",
VirtualNameServer: "yyy",
},
},
}
assert.Equal(t, expected, zone)
assert.Equal(t, expected, resp)
}
func TestClient_RemoveTXTRecords(t *testing.T) {
func TestClient_AddRecords_error(t *testing.T) {
client := mockBuilder().
Route("POST /zone/example.com/_stream",
servermock.ResponseFromFixture("error.json").
WithStatusCode(http.StatusBadRequest)).
Build(t)
records := []*ResourceRecord{{
Name: "example.com",
TTL: 600,
Type: "TXT",
Value: "txtTXTtxt",
}}
_, err := client.AddRecords(t.Context(), "example.com", records)
require.EqualError(t, err, `STID: 20251121-appf4923-126284, status: code: E0202002, text: Zone konnte auf dem Nameserver nicht aktualisiert werden., type: ERROR, message: code: EF02022, text: Der Zusatzeintrag wurde doppelt eingetragen., status: ERROR, object: OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]: _acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT "rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc"`)
}
func TestClient_RemoveRecords(t *testing.T) {
client := mockBuilder().
Route("POST /zone/example.com/_stream",
servermock.ResponseFromFixture("remove_record.json"),
@@ -63,8 +117,58 @@ func TestClient_RemoveTXTRecords(t *testing.T) {
With("X-Domainrobot-Context", "123")).
Build(t)
records := []*ResourceRecord{{}}
records := []*ResourceRecord{{
Name: "example.com",
TTL: 600,
Type: "TXT",
Value: "txtTXTtxt",
}}
err := client.RemoveTXTRecords(t.Context(), "example.com", records)
resp, err := client.RemoveRecords(t.Context(), "example.com", records)
require.NoError(t, err)
expected := &DataZoneResponse{
STID: "20251121-appf4923-126284",
CTID: "",
Messages: []ResponseMessage{
{
Text: "string",
Messages: []string{
"string",
},
Objects: []GenericObject{
{
Type: "string",
Value: "string",
},
},
Code: "string",
Status: "SUCCESS",
},
},
Status: &ResponseStatus{
Code: "S0301",
Text: "Zone was updated successfully on the name server.",
Type: "SUCCESS",
},
Object: nil,
Data: []Zone{
{
Name: "example.com",
ResourceRecords: []ResourceRecord{
{
Name: "example.com",
TTL: 120,
Type: "TXT",
Value: "txt",
Pref: 1,
},
},
Action: "xxx",
VirtualNameServer: "yyy",
},
},
}
assert.Equal(t, expected, resp)
}

View File

@@ -1,10 +1,10 @@
{
"adds": [
{
"name": "",
"ttl": 0,
"type": "",
"value": ""
"name": "example.com",
"ttl": 600,
"type": "TXT",
"value": "txtTXTtxt"
}
],
"rems": null

View File

@@ -1,14 +1,41 @@
{
"origin": "example.com",
"resourceRecords": [
"stid": "20251121-appf4923-126284",
"messages": [
{
"name": "example.com",
"ttl": 120,
"type": "TXT",
"value": "txt",
"pref": 1
"text": "string",
"notice": "string",
"messages": [
"string"
],
"objects": [
{
"type": "string",
"value": "string"
}
],
"code": "string",
"status": "SUCCESS"
}
],
"action": "xxx",
"virtualNameServer": "yyy"
"status": {
"code": "S0301",
"text": "Zone was updated successfully on the name server.",
"type": "SUCCESS"
},
"data": [
{
"origin": "example.com",
"resourceRecords": [
{
"name": "example.com",
"ttl": 120,
"type": "TXT",
"value": "txt",
"pref": 1
}
],
"action": "xxx",
"virtualNameServer": "yyy"
}
]
}

View File

@@ -0,0 +1,21 @@
{
"stid": "20251121-appf4923-126284",
"messages": [
{
"text": "Der Zusatzeintrag wurde doppelt eingetragen.",
"objects": [
{
"type": "OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]",
"value": "_acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT \"rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc\""
}
],
"code": "EF02022",
"status": "ERROR"
}
],
"status": {
"code": "E0202002",
"text": "Zone konnte auf dem Nameserver nicht aktualisiert werden.",
"type": "ERROR"
}
}

View File

@@ -2,10 +2,10 @@
"adds": null,
"rems": [
{
"name": "",
"ttl": 0,
"type": "",
"value": ""
"name": "example.com",
"ttl": 600,
"type": "TXT",
"value": "txtTXTtxt"
}
]
}

View File

@@ -1,14 +1,41 @@
{
"origin": "example.com",
"resourceRecords": [
"stid": "20251121-appf4923-126284",
"messages": [
{
"name": "example.com",
"ttl": 120,
"type": "TXT",
"value": "txt",
"pref": 1
"text": "string",
"notice": "string",
"messages": [
"string"
],
"objects": [
{
"type": "string",
"value": "string"
}
],
"code": "string",
"status": "SUCCESS"
}
],
"action": "xxx",
"virtualNameServer": "yyy"
"status": {
"code": "S0301",
"text": "Zone was updated successfully on the name server.",
"type": "SUCCESS"
},
"data": [
{
"origin": "example.com",
"resourceRecords": [
{
"name": "example.com",
"ttl": 120,
"type": "TXT",
"value": "txt",
"pref": 1
}
],
"action": "xxx",
"virtualNameServer": "yyy"
}
]
}

View File

@@ -1,33 +1,133 @@
package internal
import (
"fmt"
"strings"
)
type APIResponse[T any] struct {
STID string `json:"stid"`
CTID string `json:"ctid"`
Messages []ResponseMessage `json:"messages"`
Status *ResponseStatus `json:"status"`
Object *ResponseObject `json:"object"`
Data T `json:"data"`
}
type APIError APIResponse[any]
func (a *APIError) Error() string {
var parts []string
if a.STID != "" {
parts = append(parts, fmt.Sprintf("STID: %s", a.STID))
}
if a.CTID != "" {
parts = append(parts, fmt.Sprintf("CTID: %s", a.CTID))
}
if a.Status != nil {
parts = append(parts, "status: "+a.Status.String())
}
for _, message := range a.Messages {
parts = append(parts, "message: "+message.String())
}
if a.Object != nil {
parts = append(parts, "object: "+a.Object.String())
}
return strings.Join(parts, ", ")
}
type DataZoneResponse APIResponse[[]Zone]
type ResponseMessage struct {
Text string `json:"text"`
Messages []string `json:"messages"`
Objects []string `json:"objects"`
Code string `json:"code"`
Status string `json:"status"`
Text string `json:"text"`
Code string `json:"code"`
Status string `json:"status"`
Messages []string `json:"messages"`
Objects []GenericObject `json:"objects"`
}
func (r ResponseMessage) String() string {
var parts []string
if r.Code != "" {
parts = append(parts, "code: "+r.Code)
}
if r.Text != "" {
parts = append(parts, "text: "+r.Text)
}
if r.Status != "" {
parts = append(parts, "status: "+r.Status)
}
if len(r.Messages) > 0 {
parts = append(parts, "messages: "+strings.Join(r.Messages, ";"))
}
for _, object := range r.Objects {
parts = append(parts, fmt.Sprintf("object: %s", object))
}
return strings.Join(parts, ", ")
}
type GenericObject struct {
Type string `json:"type"`
Value string `json:"value"`
}
func (g GenericObject) String() string {
return g.Type + ": " + g.Value
}
type ResponseStatus struct {
Code string `json:"code"`
Text string `json:"text"`
Type string `json:"type"`
Type string `json:"type"` // SUCCESS, ERROR, NOTIFY, NOTICE, NICCOM_NOTIFY
}
func (r ResponseStatus) String() string {
return fmt.Sprintf("code: %s, text: %s, type: %s", r.Code, r.Text, r.Type)
}
type ResponseObject struct {
Type string `json:"type"`
Value string `json:"value"`
Summary int32 `json:"summary"`
Data string
Type string `json:"type"`
Value string `json:"value"`
Summary int32 `json:"summary"`
Data *ResponseObjectData `json:"data"`
}
type DataZoneResponse struct {
STID string `json:"stid"`
CTID string `json:"ctid"`
Messages []*ResponseMessage `json:"messages"`
Status *ResponseStatus `json:"status"`
Object any `json:"object"`
Data []*Zone `json:"data"`
func (r ResponseObject) String() string {
var parts []string
if r.Type != "" {
parts = append(parts, fmt.Sprintf("type: %s", r.Type))
}
if r.Value != "" {
parts = append(parts, fmt.Sprintf("value: %s", r.Value))
}
if r.Summary != 0 {
parts = append(parts, fmt.Sprintf("summary: %d", r.Summary))
}
if r.Data != nil {
parts = append(parts, fmt.Sprintf("data: %s", r.Data.Description))
}
return strings.Join(parts, ", ")
}
type ResponseObjectData struct {
Description string `json:"description"`
}
// ResourceRecord holds a resource record.
@@ -43,10 +143,10 @@ type ResourceRecord struct {
// Zone is an autodns zone record with all for us relevant fields.
// https://help.internetx.com/display/APIXMLEN/Zone+Object
type Zone struct {
Name string `json:"origin"`
ResourceRecords []*ResourceRecord `json:"resourceRecords"`
Action string `json:"action"`
VirtualNameServer string `json:"virtualNameServer"`
Name string `json:"origin"`
ResourceRecords []ResourceRecord `json:"resourceRecords"`
Action string `json:"action"`
VirtualNameServer string `json:"virtualNameServer"`
}
// ZoneStream body of the requests.