client.go raw
1 /*
2 Package internal Civo API client.
3
4 Because the dependencies on k8s, the official client cannot be used.
5 - https://github.com/civo/civogo/blob/v0.2.99/go.mod -> k8s.io/client-go
6 - https://github.com/civo/civogo/blob/v0.3.34/go.mod -> k8s.io/api
7 - https://github.com/civo/civogo/blob/v0.3.38/go.mod -> k8s.io/api + k8s.io/apimachinery
8 - Current version -> https://github.com/civo/civogo/blob/v0.6.1/go.mod
9 */
10 package internal
11
12 import (
13 "bytes"
14 "context"
15 "encoding/json"
16 "fmt"
17 "io"
18 "net/http"
19 "net/url"
20 "time"
21
22 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
23 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
24 "golang.org/x/oauth2"
25 )
26
27 const defaultBaseURL = "https://api.civo.com/v2"
28
29 // Client the Civo API client.
30 type Client struct {
31 region string
32
33 BaseURL *url.URL
34 HTTPClient *http.Client
35 }
36
37 // NewClient creates a new Client.
38 func NewClient(hc *http.Client, region string) (*Client, error) {
39 baseURL, _ := url.Parse(defaultBaseURL)
40
41 if hc == nil {
42 hc = &http.Client{Timeout: 10 * time.Second}
43 }
44
45 return &Client{
46 region: region,
47 BaseURL: baseURL,
48 HTTPClient: hc,
49 }, nil
50 }
51
52 // ListDomains a list of all domain names within the account.
53 // https://www.civo.com/api/dns#list-domain-names
54 func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
55 endpoint := c.BaseURL.JoinPath("dns")
56
57 req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
58 if err != nil {
59 return nil, err
60 }
61
62 var result []Domain
63
64 err = c.do(req, &result)
65 if err != nil {
66 return nil, err
67 }
68
69 return result, nil
70 }
71
72 // ListDNSRecords a list of all DNS records in the specified domain.
73 // https://www.civo.com/api/dns#list-dns-records
74 func (c *Client) ListDNSRecords(ctx context.Context, domainID string) ([]Record, error) {
75 endpoint := c.BaseURL.JoinPath("dns", domainID, "records")
76
77 req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
78 if err != nil {
79 return nil, err
80 }
81
82 var result []Record
83
84 err = c.do(req, &result)
85 if err != nil {
86 return nil, err
87 }
88
89 return result, nil
90 }
91
92 // CreateDNSRecord creates DNS records for a specific domain.
93 // https://www.civo.com/api/dns#create-a-new-dns-record
94 func (c *Client) CreateDNSRecord(ctx context.Context, domainID string, record Record) (*Record, error) {
95 endpoint := c.BaseURL.JoinPath("dns", domainID, "records")
96
97 req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)
98 if err != nil {
99 return nil, err
100 }
101
102 var result Record
103
104 err = c.do(req, &result)
105 if err != nil {
106 return nil, err
107 }
108
109 return &result, nil
110 }
111
112 // DeleteDNSRecord remove a DNS record from a domain.
113 // https://www.civo.com/api/dns#deleting-a-dns-record
114 func (c *Client) DeleteDNSRecord(ctx context.Context, record Record) error {
115 endpoint := c.BaseURL.JoinPath("dns", record.DomainID, "records", record.ID)
116
117 req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
118 if err != nil {
119 return err
120 }
121
122 return c.do(req, nil)
123 }
124
125 func (c *Client) do(req *http.Request, result any) error {
126 resp, err := c.HTTPClient.Do(req)
127 if err != nil {
128 return errutils.NewHTTPDoError(req, err)
129 }
130
131 defer func() { _ = resp.Body.Close() }()
132
133 if resp.StatusCode/100 != 2 {
134 return parseError(req, resp)
135 }
136
137 if result == nil {
138 return nil
139 }
140
141 raw, err := io.ReadAll(resp.Body)
142 if err != nil {
143 return errutils.NewReadResponseError(req, resp.StatusCode, err)
144 }
145
146 err = json.Unmarshal(raw, result)
147 if err != nil {
148 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
149 }
150
151 return nil
152 }
153
154 func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
155 buf := new(bytes.Buffer)
156
157 if payload != nil {
158 err := json.NewEncoder(buf).Encode(payload)
159 if err != nil {
160 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
161 }
162 }
163
164 if method == http.MethodGet || method == http.MethodDelete {
165 query := endpoint.Query()
166 query.Set("region", c.region)
167
168 endpoint.RawQuery = query.Encode()
169 }
170
171 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
172 if err != nil {
173 return nil, fmt.Errorf("unable to create request: %w", err)
174 }
175
176 req.Header.Set("Accept", "application/json")
177
178 if payload != nil {
179 req.Header.Set("Content-Type", "application/json")
180 }
181
182 useragent.SetHeader(req.Header)
183
184 return req, nil
185 }
186
187 func parseError(req *http.Request, resp *http.Response) error {
188 raw, _ := io.ReadAll(resp.Body)
189
190 var errAPI APIError
191
192 err := json.Unmarshal(raw, &errAPI)
193 if err != nil {
194 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
195 }
196
197 return &errAPI
198 }
199
200 // OAuthStaticAccessToken Authorization header.
201 // https://www.civo.com/api#authentication
202 func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
203 if client == nil {
204 client = &http.Client{Timeout: 5 * time.Second}
205 }
206
207 client.Transport = &oauth2.Transport{
208 Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
209 Base: client.Transport,
210 }
211
212 return client
213 }
214