Archived
Add dynu_dns_record CRUD resource and write-capable client
This commit is contained in:
1 parent
d6b52c1f44
commit
7d636b6371
9 files changed
+970
-49
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package dynuclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -106,6 +107,26 @@ type DNSRecord struct {
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type CreateDNSRecordRequest struct {
|
||||
NodeName string `json:"nodeName,omitempty"`
|
||||
RecordType string `json:"recordType"`
|
||||
Content string `json:"content"`
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
State *bool `json:"state,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateDNSRecordRequest struct {
|
||||
NodeName string `json:"nodeName,omitempty"`
|
||||
RecordType string `json:"recordType"`
|
||||
Content string `json:"content"`
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
State *bool `json:"state,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
type listDomainsResponse struct {
|
||||
apiResponse
|
||||
Domains []Domain `json:"domains"`
|
||||
@@ -121,6 +142,11 @@ type listDNSRecordsResponse struct {
|
||||
DNSRecords []DNSRecord `json:"dnsRecords"`
|
||||
}
|
||||
|
||||
type getDNSRecordResponse struct {
|
||||
apiResponse
|
||||
DNSRecord
|
||||
}
|
||||
|
||||
type getRootResponse struct {
|
||||
apiResponse
|
||||
ID int64 `json:"id"`
|
||||
@@ -131,7 +157,7 @@ type getRootResponse struct {
|
||||
|
||||
func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
|
||||
var resp listDomainsResponse
|
||||
if err := c.doGET(ctx, "/dns", &resp); err != nil {
|
||||
if err := c.doRequest(ctx, http.MethodGet, "/dns", nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Domains, nil
|
||||
@@ -139,7 +165,7 @@ func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
|
||||
|
||||
func (c *Client) GetDomainByID(ctx context.Context, domainID int64) (*Domain, error) {
|
||||
var resp getDomainResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/%d", domainID), &resp); err != nil {
|
||||
if err := c.doRequest(ctx, http.MethodGet, fmt.Sprintf("/dns/%d", domainID), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Domain, nil
|
||||
@@ -147,7 +173,7 @@ func (c *Client) GetDomainByID(ctx context.Context, domainID int64) (*Domain, er
|
||||
|
||||
func (c *Client) GetRootDomain(ctx context.Context, hostname string) (int64, string, error) {
|
||||
var resp getRootResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/getroot/%s", url.PathEscape(hostname)), &resp); err != nil {
|
||||
if err := c.doRequest(ctx, http.MethodGet, fmt.Sprintf("/dns/getroot/%s", url.PathEscape(hostname)), nil, &resp); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@@ -160,21 +186,62 @@ func (c *Client) GetRootDomain(ctx context.Context, hostname string) (int64, str
|
||||
|
||||
func (c *Client) ListDNSRecords(ctx context.Context, domainID int64) ([]DNSRecord, error) {
|
||||
var resp listDNSRecordsResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/%d/record", domainID), &resp); err != nil {
|
||||
if err := c.doRequest(ctx, http.MethodGet, fmt.Sprintf("/dns/%d/record", domainID), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.DNSRecords, nil
|
||||
}
|
||||
|
||||
func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
func (c *Client) GetDNSRecord(ctx context.Context, domainID int64, recordID int64) (*DNSRecord, error) {
|
||||
var resp getDNSRecordResponse
|
||||
if err := c.doRequest(ctx, http.MethodGet, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.DNSRecord, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateDNSRecord(ctx context.Context, domainID int64, req CreateDNSRecordRequest) (*DNSRecord, error) {
|
||||
var resp getDNSRecordResponse
|
||||
if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record", domainID), req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.DNSRecord, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateDNSRecord(ctx context.Context, domainID int64, recordID int64, req UpdateDNSRecordRequest) (*DNSRecord, error) {
|
||||
var resp getDNSRecordResponse
|
||||
if err := c.doRequest(ctx, http.MethodPut, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.DNSRecord, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteDNSRecord(ctx context.Context, domainID int64, recordID int64) error {
|
||||
return c.doRequest(ctx, http.MethodDelete, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method string, path string, requestBody any, target any) error {
|
||||
requestURL := c.baseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
|
||||
var bodyReader io.Reader
|
||||
if requestBody != nil {
|
||||
body, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode dynu API request: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewBuffer(body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, requestURL, bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("API-Key", c.apiKey)
|
||||
if requestBody != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
res, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -195,8 +262,10 @@ func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
return fmt.Errorf("dynu API returned status %d: %s", res.StatusCode, strings.TrimSpace(string(payload)))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(payload, target); err != nil {
|
||||
return fmt.Errorf("failed to decode dynu API response: %w", err)
|
||||
if len(bytes.TrimSpace(payload)) > 0 && target != nil {
|
||||
if err := json.Unmarshal(payload, target); err != nil {
|
||||
return fmt.Errorf("failed to decode dynu API response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if apiErr != nil {
|
||||
|
||||
@@ -2,10 +2,10 @@ package dynuclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
"github.com/dynu/terraform-provider-dynu/internal/testutil/fakedynu"
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestClientListDomainsSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoGetNon2xxStatus(t *testing.T) {
|
||||
func TestClientDoRequestNon2xxStatus(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetRawResponse("/dns", 401, `{"message":"nope"}`)
|
||||
@@ -35,7 +35,7 @@ func TestClientDoGetNon2xxStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoGetAPIExceptionPayload(t *testing.T) {
|
||||
func TestClientDoRequestAPIExceptionPayload(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetAPIError("/dns", fakedynu.APIError{HTTPStatus: 400, StatusCode: 400, Type: "Validation Exception", Message: "bad hostname"})
|
||||
@@ -47,7 +47,7 @@ func TestClientDoGetAPIExceptionPayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoGetMalformedJSON(t *testing.T) {
|
||||
func TestClientDoRequestMalformedJSON(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetRawResponse("/dns", 200, `{"statusCode":200,"domains":[`)
|
||||
@@ -82,3 +82,68 @@ func TestClientGetRootDomainEscapesHostname(t *testing.T) {
|
||||
t.Fatalf("expected hostname not found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDNSRecordCRUD(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
state := true
|
||||
created, err := client.CreateDNSRecord(context.Background(), 1001, dynuclient.CreateDNSRecordRequest{
|
||||
NodeName: "api",
|
||||
RecordType: "TXT",
|
||||
Content: "created",
|
||||
TTL: 120,
|
||||
State: &state,
|
||||
Group: "integration",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDNSRecord() error = %v", err)
|
||||
}
|
||||
if created.ID == 0 {
|
||||
t.Fatal("expected created record id")
|
||||
}
|
||||
|
||||
got, err := client.GetDNSRecord(context.Background(), 1001, created.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDNSRecord() error = %v", err)
|
||||
}
|
||||
if got.Content != "created" {
|
||||
t.Fatalf("expected content created, got %q", got.Content)
|
||||
}
|
||||
|
||||
updated, err := client.UpdateDNSRecord(context.Background(), 1001, created.ID, dynuclient.UpdateDNSRecordRequest{
|
||||
NodeName: "api",
|
||||
RecordType: "TXT",
|
||||
Content: "updated",
|
||||
TTL: 180,
|
||||
State: &state,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateDNSRecord() error = %v", err)
|
||||
}
|
||||
if updated.Content != "updated" || updated.TTL != 180 {
|
||||
t.Fatalf("unexpected update response: %#v", updated)
|
||||
}
|
||||
|
||||
if err := client.DeleteDNSRecord(context.Background(), 1001, created.ID); err != nil {
|
||||
t.Fatalf("DeleteDNSRecord() error = %v", err)
|
||||
}
|
||||
|
||||
_, err = client.GetDNSRecord(context.Background(), 1001, created.ID)
|
||||
if err == nil || !strings.Contains(err.Error(), "record not found") {
|
||||
t.Fatalf("expected record not found after delete, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDNSRecordWriteAPIError(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetAPIError("/dns/1001/record", fakedynu.APIError{HTTPStatus: 400, StatusCode: 400, Type: "Validation Exception", Message: "recordType invalid"})
|
||||
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, err := client.CreateDNSRecord(context.Background(), 1001, dynuclient.CreateDNSRecordRequest{RecordType: "", Content: "x"})
|
||||
if err == nil || !strings.Contains(err.Error(), "Validation Exception") {
|
||||
t.Fatalf("expected validation API error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user