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 defaultBaseURL = "https://spaceship.dev/api/v1/"
18
19 // Client the Spaceship API client.
20 type Client struct {
21 apiKey string
22 apiSecret string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(apiKey, apiSecret string) (*Client, error) {
30 if apiKey == "" || apiSecret == "" {
31 return nil, errors.New("credentials missing")
32 }
33
34 baseURL, _ := url.Parse(defaultBaseURL)
35
36 return &Client{
37 apiKey: apiKey,
38 apiSecret: apiSecret,
39 baseURL: baseURL,
40 HTTPClient: &http.Client{Timeout: 10 * time.Second},
41 }, nil
42 }
43
44 func (c *Client) do(req *http.Request, result any) error {
45 req.Header.Add("X-Api-Secret", c.apiSecret)
46 req.Header.Add("X-Api-Key", c.apiKey)
47
48 resp, err := c.HTTPClient.Do(req)
49 if err != nil {
50 return errutils.NewHTTPDoError(req, err)
51 }
52
53 defer func() { _ = resp.Body.Close() }()
54
55 if resp.StatusCode/100 != 2 {
56 return parseError(req, resp)
57 }
58
59 if result == nil {
60 return nil
61 }
62
63 raw, err := io.ReadAll(resp.Body)
64 if err != nil {
65 return errutils.NewReadResponseError(req, resp.StatusCode, err)
66 }
67
68 err = json.Unmarshal(raw, result)
69 if err != nil {
70 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
71 }
72
73 return nil
74 }
75
76 func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error {
77 endpoint := c.baseURL.JoinPath("dns", "records", domain)
78
79 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, Foo{Items: []Record{record}})
80 if err != nil {
81 return err
82 }
83
84 err = c.do(req, nil)
85 if err != nil {
86 return err
87 }
88
89 return nil
90 }
91
92 func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error {
93 endpoint := c.baseURL.JoinPath("dns", "records", domain)
94
95 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{record})
96 if err != nil {
97 return err
98 }
99
100 err = c.do(req, nil)
101 if err != nil {
102 return err
103 }
104
105 return nil
106 }
107
108 func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {
109 endpoint := c.baseURL.JoinPath("dns", "records", domain)
110
111 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
112 if err != nil {
113 return nil, err
114 }
115
116 var result GetRecordsResponse
117
118 err = c.do(req, &result)
119 if err != nil {
120 return nil, err
121 }
122
123 return result.Items, nil
124 }
125
126 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
127 buf := new(bytes.Buffer)
128
129 if payload != nil {
130 err := json.NewEncoder(buf).Encode(payload)
131 if err != nil {
132 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
133 }
134 }
135
136 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
137 if err != nil {
138 return nil, fmt.Errorf("unable to create request: %w", err)
139 }
140
141 req.Header.Set("Accept", "application/json")
142
143 if payload != nil {
144 req.Header.Set("Content-Type", "application/json")
145 }
146
147 return req, nil
148 }
149
150 func parseError(req *http.Request, resp *http.Response) error {
151 raw, _ := io.ReadAll(resp.Body)
152
153 var errAPI APIError
154
155 err := json.Unmarshal(raw, &errAPI)
156 if err != nil {
157 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
158 }
159
160 return &errAPI
161 }
162