client.go raw
1 package api
2
3 import (
4 "bytes"
5 "context"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "net/http"
10 "os"
11 "reflect"
12 "time"
13
14 "golang.org/x/time/rate"
15 )
16
17 const DefaultEndpoint = "https://api.dns-platform.jp/dpf/v1"
18
19 type ClientInterface interface {
20 SetRoundTripper(rt http.RoundTripper)
21 Read(ctx context.Context, s Spec) (string, error)
22 List(ctx context.Context, s ListSpec, keywords SearchParams) (string, error)
23 ListAll(ctx context.Context, s CountableListSpec, keywords SearchParams) (string, error)
24 Count(ctx context.Context, s CountableListSpec, keywords SearchParams) (string, error)
25 Update(ctx context.Context, s Spec, body interface{}) (string, error)
26 Create(ctx context.Context, s Spec, body interface{}) (string, error)
27 Apply(ctx context.Context, s Spec, body interface{}) (string, error)
28 Delete(ctx context.Context, s Spec) (string, error)
29 Cancel(ctx context.Context, s Spec) (string, error)
30 WatchRead(ctx context.Context, interval time.Duration, s Spec) error
31 WatchList(ctx context.Context, interval time.Duration, s ListSpec, keyword SearchParams) error
32 WatchListAll(ctx context.Context, interval time.Duration, s CountableListSpec, keyword SearchParams) error
33 }
34
35 var _ ClientInterface = &Client{}
36
37 type Client struct {
38 Endpoint string
39 Token string
40
41 logger Logger
42 client *http.Client
43
44 LastRequest *RequestInfo
45 LastResponse *ResponseInfo
46 }
47
48 type RequestInfo struct {
49 Method string
50 URL string
51 Body []byte
52 }
53
54 type ResponseInfo struct {
55 Response *http.Response
56 Body []byte
57 }
58
59 type RateRoundTripper struct {
60 RroundTripper http.RoundTripper
61 Limiter *rate.Limiter
62 }
63
64 func NewRateRoundTripper(rt http.RoundTripper, limiter *rate.Limiter) *RateRoundTripper {
65 return &RateRoundTripper{
66 RroundTripper: rt,
67 Limiter: limiter,
68 }
69 }
70
71 func (r *RateRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
72 if r.Limiter == nil {
73 r.Limiter = rate.NewLimiter(rate.Limit(1.0), 5)
74 }
75 if r.RroundTripper == nil {
76 r.RroundTripper = http.DefaultTransport
77 }
78 if err := r.Limiter.Wait(req.Context()); err != nil {
79 return nil, fmt.Errorf("request rate-limit by client side: %w", err)
80 }
81 return r.RroundTripper.RoundTrip(req)
82 }
83
84 func NewClient(token string, endpoint string, logger Logger) *Client {
85 if endpoint == "" {
86 endpoint = DefaultEndpoint
87 }
88 if logger == nil {
89 logger = NewStdLogger(os.Stderr, "dpf-client", 0, 4)
90 }
91 return &Client{Endpoint: endpoint, Token: token, logger: logger, client: &http.Client{Transport: NewRateRoundTripper(nil, nil)}}
92 }
93
94 func (c *Client) SetRoundTripper(rt http.RoundTripper) {
95 c.client.Transport = rt
96 }
97
98 func (c *Client) marshalJSON(action Action, body interface{}) ([]byte, error) {
99 var (
100 jsonBody []byte
101 err error
102 )
103 switch action {
104 case ActionCreate:
105 jsonBody, err = JSON.MarshalCreate(body)
106 case ActionUpdate:
107 jsonBody, err = JSON.MarshalUpdate(body)
108 case ActionApply:
109 jsonBody, err = JSON.MarshalApply(body)
110 default:
111 return nil, fmt.Errorf("not support action `%s` with body request", action)
112 }
113 if err != nil {
114 return nil, fmt.Errorf("failed to encode body to json: %w", err)
115 }
116 return jsonBody, nil
117 }
118
119 func (c *Client) doSetup(ctx context.Context, spec Spec, action Action, body interface{}, params SearchParams) (*http.Request, error) {
120 var r io.Reader
121 if action == ActionCount {
122 _, ok := spec.(CountableListSpec)
123 if !ok {
124 return nil, fmt.Errorf("spec is not CountableListSpec")
125 }
126 }
127 c.LastRequest = &RequestInfo{}
128 c.LastResponse = nil
129 // create URL
130 method, path := spec.GetPathMethod(action)
131 if path == "" {
132 return nil, fmt.Errorf("not support action %s", action)
133 }
134 c.LastRequest.Method = method
135 url := c.Endpoint + path
136 if params != nil {
137 p, err := params.GetValues()
138 if err != nil {
139 return nil, fmt.Errorf("failed to get search params: %w", err)
140 }
141 url += "?" + p.Encode()
142 }
143 c.LastRequest.URL = url
144 c.logger.Debugf("method: %s request-url: %s", method, url)
145 // make request body
146 if body != nil {
147 jsonBody, err := c.marshalJSON(action, body)
148 if err != nil {
149 return nil, err
150 }
151 c.logger.Tracef("request-body: `%s`", string(jsonBody))
152 c.LastRequest.Body = jsonBody
153 r = bytes.NewBuffer(jsonBody)
154 }
155
156 // make request
157 req, err := http.NewRequest(method, url, r)
158 if err != nil {
159 return nil, fmt.Errorf("failed to create http request: %w", err)
160 }
161 // authorized
162 req.Header.Add("Authorization", "Bearer "+c.Token)
163 req.Header.Add("Content-Type", "application/json")
164
165 return req.WithContext(ctx), nil
166 }
167
168 func (c *Client) Do(ctx context.Context, spec Spec, action Action, body interface{}, params SearchParams) (string, error) {
169 req, err := c.doSetup(ctx, spec, action, body, params)
170 if err != nil {
171 return "", err
172 }
173 // request
174 resp, err := c.client.Do(req)
175 if err != nil {
176 return "", fmt.Errorf("failed to get http response: %w", err)
177 }
178 defer resp.Body.Close()
179 c.LastResponse = &ResponseInfo{
180 Response: resp,
181 }
182 // get body
183 bs, err := ioutil.ReadAll(resp.Body)
184 if err != nil {
185 return "", fmt.Errorf("failed to get http response body: %w", err)
186 }
187 c.LastResponse.Body = bs
188 c.logger.Debugf("status-code: `%d`", resp.StatusCode)
189 c.logger.Tracef("response-body: `%s`", string(bs))
190
191 // if statiscode is error, response body type is BadResponse or Plantext
192 if resp.StatusCode >= http.StatusBadRequest {
193 badRequest := &BadResponse{StatusCode: resp.StatusCode}
194 if err := UnmarshalRead(bs, badRequest); err != nil {
195 return "", fmt.Errorf("failed to request: status code: %d body: %s err: %w", resp.StatusCode, string(bs), err)
196 }
197 return badRequest.RequestID, badRequest
198 }
199
200 // parse raw response
201 rawResponse := &RawResponse{}
202 if err := UnmarshalRead(bs, rawResponse); err != nil {
203 // maybe not executed
204 return "", fmt.Errorf("failed to parse get response: %w", err)
205 }
206 if req.Method == http.MethodGet {
207 if err := c.doReadResponse(action, spec, bs, rawResponse); err != nil {
208 return rawResponse.RequestID, err
209 }
210 }
211
212 // initialize process
213 if d, ok := spec.(Initializer); ok {
214 d.Init()
215 }
216
217 return rawResponse.RequestID, nil
218 }
219
220 func (c *Client) doReadResponse(action Action, spec Spec, bs []byte, rawResponse *RawResponse) error {
221 switch {
222 case action == ActionCount:
223 // ActionCount
224 count := &Count{}
225 if err := UnmarshalRead(rawResponse.Result, count); err != nil {
226 return fmt.Errorf("failed to parse count response result: %w", err)
227 }
228 if cl, ok := spec.(CountableListSpec); ok {
229 cl.SetCount(count.Count)
230 }
231 case rawResponse.Result != nil:
232 // ActionRead
233 if err := UnmarshalRead(rawResponse.Result, spec); err != nil {
234 return fmt.Errorf("failed to parse response result: %w", err)
235 }
236 case rawResponse.Results != nil:
237 // ActionList
238 listSpec, ok := spec.(ListSpec)
239 if !ok {
240 return fmt.Errorf("not support ListSpec %s", spec.GetName())
241 }
242 items := listSpec.GetItems()
243 if err := UnmarshalRead(rawResponse.Results, items); err != nil {
244 return fmt.Errorf("failed to parse list response results: %w", err)
245 }
246 default:
247 if err := UnmarshalRead(bs, spec); err != nil {
248 return fmt.Errorf("failed to parse response result: %w", err)
249 }
250 }
251 return nil
252 }
253
254 func (c *Client) Read(ctx context.Context, s Spec) (string, error) {
255 return c.Do(ctx, s, ActionRead, nil, nil)
256 }
257
258 func (c *Client) List(ctx context.Context, s ListSpec, keywords SearchParams) (string, error) {
259 return c.Do(ctx, s, ActionList, nil, keywords)
260 }
261
262 func (c *Client) ListAll(ctx context.Context, s CountableListSpec, keywords SearchParams) (string, error) {
263 req, err := c.Count(ctx, s, keywords)
264 if err != nil {
265 return req, err
266 }
267
268 if keywords == nil {
269 keywords = &CommonSearchParams{}
270 keywords.SetLimit(s.GetMaxLimit())
271 }
272
273 count := s.GetCount()
274 cList := DeepCopyCountableListSpec(s)
275
276 for offset := int32(0); offset < count; offset += keywords.GetLimit() {
277 keywords.SetOffset(offset)
278 req, err = c.List(ctx, cList, keywords)
279 if err != nil {
280 return req, err
281 }
282 for i := 0; i < cList.Len(); i++ {
283 s.AddItem(cList.Index(i))
284 }
285 }
286 return req, nil
287 }
288
289 func (c *Client) Count(ctx context.Context, s CountableListSpec, keywords SearchParams) (string, error) {
290 return c.Do(ctx, s, ActionCount, nil, keywords)
291 }
292
293 func (c *Client) Update(ctx context.Context, s Spec, body interface{}) (string, error) {
294 if body == nil {
295 body = s
296 }
297 return c.Do(ctx, s, ActionUpdate, body, nil)
298 }
299
300 func (c *Client) Create(ctx context.Context, s Spec, body interface{}) (string, error) {
301 if body == nil {
302 body = s
303 }
304 return c.Do(ctx, s, ActionCreate, body, nil)
305 }
306
307 func (c *Client) Apply(ctx context.Context, s Spec, body interface{}) (string, error) {
308 if body == nil {
309 body = s
310 }
311 return c.Do(ctx, s, ActionApply, body, nil)
312 }
313
314 func (c *Client) Delete(ctx context.Context, s Spec) (string, error) {
315 return c.Do(ctx, s, ActionDelete, nil, nil)
316 }
317
318 func (c *Client) Cancel(ctx context.Context, s Spec) (string, error) {
319 return c.Do(ctx, s, ActionCancel, nil, nil)
320 }
321
322 func (c *Client) watch(ctx context.Context, interval time.Duration, f func(context.Context) (keep bool, err error)) error {
323 if interval < time.Second {
324 return fmt.Errorf("interval must greater than equals to 1s")
325 }
326 ticker := time.NewTicker(interval)
327 defer ticker.Stop()
328 LOOP:
329 for {
330 select {
331 case <-ticker.C:
332 loopBreak, err := f(ctx)
333 if err != nil {
334 return err
335 }
336 if loopBreak {
337 break LOOP
338 }
339 case <-ctx.Done():
340 break LOOP
341 }
342 }
343 return ctx.Err()
344 }
345
346 // ctx should set Deadline or Timeout
347 // interval must be grater than equals to 1s
348 // s is Readable Spec.
349 func (c *Client) WatchRead(ctx context.Context, interval time.Duration, s Spec) error {
350 org := DeepCopySpec(s)
351 return c.watch(ctx, interval, func(cctx context.Context) (bool, error) {
352 _, err := c.Read(cctx, s)
353 if err != nil {
354 return true, err
355 }
356 if reflect.DeepEqual(s, org) {
357 return false, nil
358 }
359 return true, nil
360 })
361 }
362
363 // ctx should set Deadline or Timeout
364 // interval must be grater than equals to 1s
365 // s is ListAble Spec.
366 func (c *Client) WatchList(ctx context.Context, interval time.Duration, s ListSpec, keyword SearchParams) error {
367 org := DeepCopyListSpec(s)
368 return c.watch(ctx, interval, func(cctx context.Context) (bool, error) {
369 _, err := c.List(cctx, s, keyword)
370 if err != nil {
371 return true, err
372 }
373 if reflect.DeepEqual(s, org) {
374 return false, nil
375 }
376 return true, nil
377 })
378 }
379
380 // ctx should set Deadline or Timeout
381 // interval must be grater than equals to 1s
382 // s is CountableListSpec Spec.
383 func (c *Client) WatchListAll(ctx context.Context, interval time.Duration, s CountableListSpec, keyword SearchParams) error {
384 copySpec := DeepCopyCountableListSpec(s)
385 copySpec.ClearItems()
386 err := c.watch(ctx, interval, func(cctx context.Context) (bool, error) {
387 _, err := c.ListAll(cctx, copySpec, keyword)
388 if err != nil {
389 return true, err
390 }
391 if reflect.DeepEqual(s, copySpec) {
392 return false, nil
393 }
394 return true, nil
395 })
396 if err != nil {
397 return err
398 }
399 s.ClearItems()
400 for i := 0; i < copySpec.Len(); i++ {
401 s.AddItem(copySpec.Index(i))
402 }
403 s.SetCount(int32(copySpec.Len()))
404 return nil
405 }
406