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