client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
14 )
15
16 const defaultBaseURL = "https://api.variomedia.de"
17
18 const authorizationHeader = "Authorization"
19
20 // Client the API client for Variomedia.
21 type Client struct {
22 apiToken string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(apiToken string) *Client {
30 baseURL, _ := url.Parse(defaultBaseURL)
31
32 return &Client{
33 apiToken: apiToken,
34 baseURL: baseURL,
35 HTTPClient: &http.Client{Timeout: 10 * time.Second},
36 }
37 }
38
39 // CreateDNSRecord creates a new DNS entry.
40 // https://api.variomedia.de/docs/dns-records.html#erstellen
41 func (c *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*CreateDNSRecordResponse, error) {
42 endpoint := c.baseURL.JoinPath("dns-records")
43
44 data := CreateDNSRecordRequest{Data: Data{
45 Type: "dns-record",
46 Attributes: record,
47 }}
48
49 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, data)
50 if err != nil {
51 return nil, err
52 }
53
54 var result CreateDNSRecordResponse
55
56 err = c.do(req, &result)
57 if err != nil {
58 return nil, err
59 }
60
61 return &result, nil
62 }
63
64 // DeleteDNSRecord deletes a DNS record.
65 // https://api.variomedia.de/docs/dns-records.html#l%C3%B6schen
66 func (c *Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordResponse, error) {
67 endpoint := c.baseURL.JoinPath("dns-records", id)
68
69 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
70 if err != nil {
71 return nil, err
72 }
73
74 var result DeleteRecordResponse
75
76 err = c.do(req, &result)
77 if err != nil {
78 return nil, err
79 }
80
81 return &result, nil
82 }
83
84 // GetJob returns a single job based on its ID.
85 // https://api.variomedia.de/docs/job-queue.html
86 func (c *Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) {
87 endpoint := c.baseURL.JoinPath("queue-jobs", id)
88
89 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
90 if err != nil {
91 return nil, err
92 }
93
94 var result GetJobResponse
95
96 err = c.do(req, &result)
97 if err != nil {
98 return nil, err
99 }
100
101 return &result, nil
102 }
103
104 func (c *Client) do(req *http.Request, data any) error {
105 req.Header.Set(authorizationHeader, "token "+c.apiToken)
106
107 resp, err := c.HTTPClient.Do(req)
108 if err != nil {
109 return errutils.NewHTTPDoError(req, err)
110 }
111
112 defer func() { _ = resp.Body.Close() }()
113
114 if resp.StatusCode/100 != 2 {
115 return parseError(req, resp)
116 }
117
118 raw, err := io.ReadAll(resp.Body)
119 if err != nil {
120 return errutils.NewReadResponseError(req, resp.StatusCode, err)
121 }
122
123 err = json.Unmarshal(raw, data)
124 if err != nil {
125 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
126 }
127
128 return nil
129 }
130
131 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
132 buf := new(bytes.Buffer)
133
134 if payload != nil {
135 err := json.NewEncoder(buf).Encode(payload)
136 if err != nil {
137 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
138 }
139 }
140
141 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
142 if err != nil {
143 return nil, fmt.Errorf("unable to create request: %w", err)
144 }
145
146 req.Header.Set("Accept", "application/vnd.variomedia.v1+json")
147
148 if payload != nil {
149 req.Header.Set("Content-Type", "application/vnd.api+json")
150 }
151
152 return req, nil
153 }
154
155 func parseError(req *http.Request, resp *http.Response) error {
156 raw, _ := io.ReadAll(resp.Body)
157
158 var errAPI APIError
159
160 err := json.Unmarshal(raw, &errAPI)
161 if err != nil {
162 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
163 }
164
165 return errAPI
166 }
167