client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "net/url"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const defaultServer = "console.ves.volterra.io"
18
19 const authorizationHeader = "Authorization"
20
21 // Client the F5 XC API client.
22 type Client struct {
23 apiToken string
24
25 baseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(apiToken, tenantName, server string) (*Client, error) {
31 if apiToken == "" {
32 return nil, errors.New("credentials missing")
33 }
34
35 baseURL, err := createBaseURL(tenantName, server)
36 if err != nil {
37 return nil, err
38 }
39
40 return &Client{
41 apiToken: apiToken,
42 baseURL: baseURL,
43 HTTPClient: &http.Client{Timeout: 10 * time.Second},
44 }, nil
45 }
46
47 // CreateRRSet creates RRSet.
48 // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Create
49 func (c *Client) CreateRRSet(ctx context.Context, dnsZoneName, groupName string, rrSet RRSet) (*APIRRSet, error) {
50 endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName)
51
52 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, APIRRSet{
53 DNSZoneName: dnsZoneName,
54 GroupName: groupName,
55 RRSet: rrSet,
56 })
57 if err != nil {
58 return nil, err
59 }
60
61 result := &APIRRSet{}
62
63 err = c.do(req, result)
64 if err != nil {
65 return nil, err
66 }
67
68 return result, nil
69 }
70
71 // GetRRSet gets RRSets.
72 // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Get
73 func (c *Client) GetRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {
74 endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
75
76 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
77 if err != nil {
78 return nil, err
79 }
80
81 result := &APIRRSet{}
82
83 err = c.do(req, result)
84 if err != nil {
85 usce := &APIError{}
86 if errors.As(err, &usce) && usce.StatusCode == http.StatusNotFound {
87 return nil, nil
88 }
89
90 return nil, err
91 }
92
93 return result, nil
94 }
95
96 // DeleteRRSet deletes RRSet.
97 // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Delete
98 func (c *Client) DeleteRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {
99 endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
100
101 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
102 if err != nil {
103 return nil, err
104 }
105
106 result := &APIRRSet{}
107
108 err = c.do(req, result)
109 if err != nil {
110 return nil, err
111 }
112
113 return result, nil
114 }
115
116 // ReplaceRRSet replaces RRSet.
117 // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Replace
118 func (c *Client) ReplaceRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string, rrSet RRSet) (*APIRRSet, error) {
119 endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
120
121 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, APIRRSet{
122 DNSZoneName: dnsZoneName,
123 GroupName: groupName,
124 RRSet: rrSet,
125 Type: recordType,
126 })
127 if err != nil {
128 return nil, err
129 }
130
131 result := &APIRRSet{}
132
133 err = c.do(req, result)
134 if err != nil {
135 return nil, err
136 }
137
138 return result, nil
139 }
140
141 func (c *Client) do(req *http.Request, result any) error {
142 req.Header.Set(authorizationHeader, "APIToken "+c.apiToken)
143
144 resp, err := c.HTTPClient.Do(req)
145 if err != nil {
146 return errutils.NewHTTPDoError(req, err)
147 }
148
149 defer func() { _ = resp.Body.Close() }()
150
151 if resp.StatusCode/100 != 2 {
152 return parseError(req, resp)
153 }
154
155 if result == nil {
156 return nil
157 }
158
159 raw, err := io.ReadAll(resp.Body)
160 if err != nil {
161 return errutils.NewReadResponseError(req, resp.StatusCode, err)
162 }
163
164 err = json.Unmarshal(raw, result)
165 if err != nil {
166 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
167 }
168
169 return nil
170 }
171
172 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
173 buf := new(bytes.Buffer)
174
175 if payload != nil {
176 err := json.NewEncoder(buf).Encode(payload)
177 if err != nil {
178 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
179 }
180 }
181
182 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
183 if err != nil {
184 return nil, fmt.Errorf("unable to create request: %w", err)
185 }
186
187 req.Header.Set("Accept", "application/json")
188
189 if payload != nil {
190 req.Header.Set("Content-Type", "application/json")
191 }
192
193 return req, nil
194 }
195
196 func parseError(req *http.Request, resp *http.Response) error {
197 raw, _ := io.ReadAll(resp.Body)
198
199 apiErr := APIError{StatusCode: resp.StatusCode}
200
201 err := json.Unmarshal(raw, &apiErr)
202 if err != nil {
203 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
204 }
205
206 return &apiErr
207 }
208
209 func createBaseURL(tenant, server string) (*url.URL, error) {
210 if tenant == "" {
211 return nil, errors.New("missing tenant name")
212 }
213
214 if server == "" {
215 server = defaultServer
216 }
217
218 baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenant, server))
219 if err != nil {
220 return nil, fmt.Errorf("parse base URL: %w", err)
221 }
222
223 return baseURL, nil
224 }
225