-
Notifications
You must be signed in to change notification settings - Fork 44
/
Copy pathclient.go
583 lines (507 loc) · 16.2 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
package scw
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/internal/generic"
"github.com/scaleway/scaleway-sdk-go/logger"
)
// Client is the Scaleway client which performs API requests.
//
// This client should be passed in the `NewApi` functions whenever an API instance is created.
// Creating a Client is done with the `NewClient` function.
type Client struct {
httpClient httpClient
auth auth.Auth
apiURL string
userAgent string
defaultOrganizationID *string
defaultProjectID *string
defaultRegion *Region
defaultZone *Zone
defaultPageSize *uint32
}
func defaultOptions() []ClientOption {
return []ClientOption{
WithoutAuth(),
WithAPIURL("https://api.scaleway.com"),
withDefaultUserAgent(userAgent),
}
}
// NewClient instantiate a new Client object.
//
// Zero or more ClientOption object can be passed as a parameter.
// These options will then be applied to the client.
func NewClient(opts ...ClientOption) (*Client, error) {
s := newSettings()
// apply options
s.apply(append(defaultOptions(), opts...))
// validate settings
err := s.validate()
if err != nil {
return nil, err
}
// dial the API
if s.httpClient == nil {
s.httpClient = newHTTPClient()
}
// insecure mode
if s.insecure {
logger.Debugf("client: using insecure mode")
setInsecureMode(s.httpClient)
}
if logger.ShouldLog(logger.LogLevelDebug) {
logger.Debugf("client: using request logger")
setRequestLogging(s.httpClient)
}
logger.Debugf("client: using sdk version " + version)
return &Client{
auth: s.token,
httpClient: s.httpClient,
apiURL: s.apiURL,
userAgent: s.userAgent,
defaultOrganizationID: s.defaultOrganizationID,
defaultProjectID: s.defaultProjectID,
defaultRegion: s.defaultRegion,
defaultZone: s.defaultZone,
defaultPageSize: s.defaultPageSize,
}, nil
}
// GetDefaultOrganizationID returns the default organization ID
// of the client. This value can be set in the client option
// WithDefaultOrganizationID(). Be aware this value can be empty.
func (c *Client) GetDefaultOrganizationID() (organizationID string, exists bool) {
if c.defaultOrganizationID != nil {
return *c.defaultOrganizationID, true
}
return "", false
}
// GetDefaultProjectID returns the default project ID
// of the client. This value can be set in the client option
// WithDefaultProjectID(). Be aware this value can be empty.
func (c *Client) GetDefaultProjectID() (projectID string, exists bool) {
if c.defaultProjectID != nil {
return *c.defaultProjectID, true
}
return "", false
}
// GetDefaultRegion returns the default region of the client.
// This value can be set in the client option
// WithDefaultRegion(). Be aware this value can be empty.
func (c *Client) GetDefaultRegion() (region Region, exists bool) {
if c.defaultRegion != nil {
return *c.defaultRegion, true
}
return Region(""), false
}
// GetDefaultZone returns the default zone of the client.
// This value can be set in the client option
// WithDefaultZone(). Be aware this value can be empty.
func (c *Client) GetDefaultZone() (zone Zone, exists bool) {
if c.defaultZone != nil {
return *c.defaultZone, true
}
return Zone(""), false
}
func (c *Client) GetSecretKey() (secretKey string, exists bool) {
if token, isToken := c.auth.(*auth.Token); isToken {
return token.SecretKey, isToken
}
return "", false
}
func (c *Client) GetAccessKey() (accessKey string, exists bool) {
if token, isToken := c.auth.(*auth.Token); isToken {
return token.AccessKey, isToken
} else if token, isAccessKey := c.auth.(*auth.AccessKeyOnly); isAccessKey {
return token.AccessKey, isAccessKey
}
return "", false
}
// GetDefaultPageSize returns the default page size of the client.
// This value can be set in the client option
// WithDefaultPageSize(). Be aware this value can be empty.
func (c *Client) GetDefaultPageSize() (pageSize uint32, exists bool) {
if c.defaultPageSize != nil {
return *c.defaultPageSize, true
}
return 0, false
}
// Do performs HTTP request(s) based on the ScalewayRequest object.
// RequestOptions are applied prior to doing the request.
func (c *Client) Do(req *ScalewayRequest, res interface{}, opts ...RequestOption) (err error) {
// apply request options
req.apply(opts)
// validate request options
err = req.validate()
if err != nil {
return err
}
if req.auth == nil {
req.auth = c.auth
}
if req.zones != nil {
return c.doListZones(req, res, req.zones)
}
if req.regions != nil {
return c.doListRegions(req, res, req.regions)
}
if req.allPages {
return c.doListAll(req, res)
}
return c.do(req, res)
}
// do performs a single HTTP request based on the ScalewayRequest object.
func (c *Client) do(req *ScalewayRequest, res interface{}) (sdkErr error) {
if req == nil {
return errors.New("request must be non-nil")
}
// build url
url, sdkErr := req.getURL(c.apiURL)
if sdkErr != nil {
return sdkErr
}
logger.Debugf("creating %s request on %s", req.Method, url.String())
// build request
httpRequest, err := http.NewRequest(req.Method, url.String(), req.Body)
if err != nil {
return errors.Wrap(err, "could not create request")
}
httpRequest.Header = req.getAllHeaders(req.auth, c.userAgent, false)
if req.ctx != nil {
httpRequest = httpRequest.WithContext(req.ctx)
}
// execute request
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return errors.Wrap(err, "error executing request")
}
defer func() {
closeErr := httpResponse.Body.Close()
if sdkErr == nil && closeErr != nil {
sdkErr = errors.Wrap(closeErr, "could not close http response")
}
}()
sdkErr = hasResponseError(httpResponse)
if sdkErr != nil {
return sdkErr
}
if res != nil {
contentType := httpResponse.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") {
err = json.NewDecoder(httpResponse.Body).Decode(&res)
if err != nil {
return errors.Wrap(err, "could not parse %s response body", contentType)
}
} else {
buffer, isBuffer := res.(io.Writer)
if !isBuffer {
return errors.Wrap(err, "could not handle %s response body with %T result type", contentType, buffer)
}
_, err := io.Copy(buffer, httpResponse.Body)
if err != nil {
return errors.Wrap(err, "could not copy %s response body", contentType)
}
}
// Handle instance API X-Total-Count header
xTotalCountStr := httpResponse.Header.Get("X-Total-Count")
if legacyLister, isLegacyLister := res.(legacyLister); isLegacyLister && xTotalCountStr != "" {
xTotalCount, err := strconv.ParseInt(xTotalCountStr, 10, 32)
if err != nil {
return errors.Wrap(err, "could not parse X-Total-Count header")
}
legacyLister.UnsafeSetTotalCount(int(xTotalCount))
}
}
return nil
}
type lister interface {
UnsafeGetTotalCount() uint64
UnsafeAppend(interface{}) (uint64, error)
}
// Old lister for uint32
// Used for retro-compatibility with response that use uint32
type lister32 interface {
UnsafeGetTotalCount() uint32
UnsafeAppend(interface{}) (uint32, error)
}
type legacyLister interface {
UnsafeSetTotalCount(totalCount int)
}
func listerGetTotalCount(i interface{}) uint64 {
if l, isLister := i.(lister); isLister {
return l.UnsafeGetTotalCount()
}
if l32, isLister32 := i.(lister32); isLister32 {
return uint64(l32.UnsafeGetTotalCount())
}
panic(fmt.Errorf("%T does not support pagination but checks failed, should not happen", i))
}
func listerAppend(recv interface{}, elems interface{}) (uint64, error) {
if l, isLister := recv.(lister); isLister {
return l.UnsafeAppend(elems)
} else if l32, isLister32 := recv.(lister32); isLister32 {
total, err := l32.UnsafeAppend(elems)
return uint64(total), err
}
panic(fmt.Errorf("%T does not support pagination but checks failed, should not happen", recv))
}
func isLister(i interface{}) bool {
switch i.(type) {
case lister:
return true
case lister32:
return true
default:
return false
}
}
const maxPageCount uint64 = math.MaxUint32
// doListAll collects all pages of a List request and aggregate all results on a single response.
func (c *Client) doListAll(req *ScalewayRequest, res interface{}) (err error) {
// check for lister interface
if isLister(res) {
pageCount := maxPageCount
for page := uint64(1); page <= pageCount; page++ {
// set current page
req.Query.Set("page", strconv.FormatUint(page, 10))
// request the next page
nextPage := newVariableFromType(res)
err := c.do(req, nextPage)
if err != nil {
return err
}
// append results
pageSize, err := listerAppend(res, nextPage)
if err != nil {
return err
}
if pageSize == 0 {
return nil
}
// set total count on first request
if pageCount == maxPageCount {
totalCount := listerGetTotalCount(nextPage)
pageCount = (totalCount + pageSize - 1) / pageSize
}
}
return nil
}
return errors.New("%T does not support pagination", res)
}
// doListLocalities collects all localities using mutliple list requests and aggregate all results on a lister response
// results is sorted by locality
func (c *Client) doListLocalities(req *ScalewayRequest, res interface{}, localities []string) (err error) {
path := req.Path
if !strings.Contains(path, "%locality%") {
return fmt.Errorf("request is not a valid locality request")
}
// Requests are parallelized
responseMutex := sync.Mutex{}
requestGroup := sync.WaitGroup{}
errChan := make(chan error, len(localities))
requestGroup.Add(len(localities))
for _, locality := range localities {
go func(locality string) {
defer requestGroup.Done()
// Request is cloned as doListAll will change header
// We remove zones as it would recurse in the same function
req := req.clone()
req.zones = []Zone(nil)
req.Path = strings.ReplaceAll(path, "%locality%", locality)
// We create a new response that we append to main response
zoneResponse := newVariableFromType(res)
err := c.Do(req, zoneResponse)
if err != nil {
errChan <- err
}
responseMutex.Lock()
_, err = listerAppend(res, zoneResponse)
responseMutex.Unlock()
if err != nil {
errChan <- err
}
}(locality)
}
requestGroup.Wait()
L: // We gather potential errors and return them all together
for {
select {
case newErr := <-errChan:
err = errors.Wrap(err, newErr.Error())
default:
break L
}
}
close(errChan)
if err != nil {
return err
}
return nil
}
// doListZones collects all zones using multiple list requests and aggregate all results on a single response.
// result is sorted by zone
func (c *Client) doListZones(req *ScalewayRequest, res interface{}, zones []Zone) (err error) {
if isLister(res) {
// Prepare request with %zone% that can be replaced with actual zone
for _, zone := range AllZones {
if strings.Contains(req.Path, string(zone)) {
req.Path = strings.ReplaceAll(req.Path, string(zone), "%locality%")
break
}
}
if !strings.Contains(req.Path, "%locality%") {
return fmt.Errorf("request is not a valid zoned request")
}
localities := make([]string, 0, len(zones))
for _, zone := range zones {
localities = append(localities, string(zone))
}
err := c.doListLocalities(req, res, localities)
if err != nil {
return fmt.Errorf("failed to list localities: %w", err)
}
sortResponseByZones(res, zones)
return nil
}
return errors.New("%T does not support pagination", res)
}
// doListRegions collects all regions using multiple list requests and aggregate all results on a single response.
// result is sorted by region
func (c *Client) doListRegions(req *ScalewayRequest, res interface{}, regions []Region) (err error) {
if isLister(res) {
// Prepare request with %locality% that can be replaced with actual region
for _, region := range AllRegions {
if strings.Contains(req.Path, string(region)) {
req.Path = strings.ReplaceAll(req.Path, string(region), "%locality%")
break
}
}
if !strings.Contains(req.Path, "%locality%") {
return fmt.Errorf("request is not a valid zoned request")
}
localities := make([]string, 0, len(regions))
for _, region := range regions {
localities = append(localities, string(region))
}
err := c.doListLocalities(req, res, localities)
if err != nil {
return fmt.Errorf("failed to list localities: %w", err)
}
sortResponseByRegions(res, regions)
return nil
}
return errors.New("%T does not support pagination", res)
}
// sortSliceByZones sorts a slice of struct using a Zone field that should exist
func sortSliceByZones(list interface{}, zones []Zone) {
if !generic.HasField(list, "Zone") {
return
}
zoneMap := map[Zone]int{}
for i, zone := range zones {
zoneMap[zone] = i
}
generic.SortSliceByField(list, "Zone", func(i interface{}, i2 interface{}) bool {
return zoneMap[i.(Zone)] < zoneMap[i2.(Zone)]
})
}
// sortSliceByRegions sorts a slice of struct using a Region field that should exist
func sortSliceByRegions(list interface{}, regions []Region) {
if !generic.HasField(list, "Region") {
return
}
regionMap := map[Region]int{}
for i, region := range regions {
regionMap[region] = i
}
generic.SortSliceByField(list, "Region", func(i interface{}, i2 interface{}) bool {
return regionMap[i.(Region)] < regionMap[i2.(Region)]
})
}
// sortResponseByZones find first field that is a slice in a struct and sort it by zone
func sortResponseByZones(res interface{}, zones []Zone) {
// res may be ListServersResponse
//
// type ListServersResponse struct {
// TotalCount uint32 `json:"total_count"`
// Servers []*Server `json:"servers"`
// }
// We iterate over fields searching for the slice one to sort it
resType := reflect.TypeOf(res).Elem()
fields := reflect.VisibleFields(resType)
for _, field := range fields {
if field.Type.Kind() == reflect.Slice {
sortSliceByZones(reflect.ValueOf(res).Elem().FieldByName(field.Name).Interface(), zones)
return
}
}
}
// sortResponseByRegions find first field that is a slice in a struct and sort it by region
func sortResponseByRegions(res interface{}, regions []Region) {
// res may be ListServersResponse
//
// type ListServersResponse struct {
// TotalCount uint32 `json:"total_count"`
// Servers []*Server `json:"servers"`
// }
// We iterate over fields searching for the slice one to sort it
resType := reflect.TypeOf(res).Elem()
fields := reflect.VisibleFields(resType)
for _, field := range fields {
if field.Type.Kind() == reflect.Slice {
sortSliceByRegions(reflect.ValueOf(res).Elem().FieldByName(field.Name).Interface(), regions)
return
}
}
}
// newVariableFromType returns a variable set to the zero value of the given type
func newVariableFromType(t interface{}) interface{} {
// reflect.New always create a pointer, that's why we use reflect.Indirect before
return reflect.New(reflect.Indirect(reflect.ValueOf(t)).Type()).Interface()
}
func newHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: http.DefaultTransport.(*http.Transport).Clone(),
}
}
func setInsecureMode(c httpClient) {
standardHTTPClient, ok := c.(*http.Client)
if !ok {
logger.Warningf("client: cannot use insecure mode with HTTP client of type %T", c)
return
}
transportClient, ok := standardHTTPClient.Transport.(*http.Transport)
if !ok {
logger.Warningf("client: cannot use insecure mode with Transport client of type %T", standardHTTPClient.Transport)
return
}
if transportClient.TLSClientConfig == nil {
transportClient.TLSClientConfig = &tls.Config{}
}
transportClient.TLSClientConfig.InsecureSkipVerify = true
}
func setRequestLogging(c httpClient) {
standardHTTPClient, ok := c.(*http.Client)
if !ok {
logger.Warningf("client: cannot use request logger with HTTP client of type %T", c)
return
}
// Do not wrap transport if it is already a logger
// As client is a pointer, changing transport will change given client
// If the same httpClient is used in multiple scwClient, it would add multiple logger transports
_, isLogger := standardHTTPClient.Transport.(*requestLoggerTransport)
if !isLogger {
standardHTTPClient.Transport = &requestLoggerTransport{rt: standardHTTPClient.Transport}
}
}