Skip to content

Commit 390219e

Browse files
CodelaxremyleoneMonitob
authored
feat(client): add WithZones and WithRegions options (#1416)
Co-authored-by: Rémy Léone <[email protected]> Co-authored-by: jaime Bernabe <[email protected]>
1 parent f833201 commit 390219e

File tree

6 files changed

+288
-0
lines changed

6 files changed

+288
-0
lines changed

example_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,30 @@ func Example_listServers() {
8080
fmt.Println(response)
8181
}
8282

83+
func Example_listServersWithZones() {
84+
// Create a Scaleway client
85+
client, err := scw.NewClient(
86+
scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/project/credentials
87+
)
88+
if err != nil {
89+
// handle error
90+
}
91+
92+
// Create SDK objects for Scaleway Instance product
93+
instanceAPI := instance.NewAPI(client)
94+
95+
// Call the ListServers method on the Instance SDK
96+
response, err := instanceAPI.ListServers(&instance.ListServersRequest{},
97+
// Add WithZones option to list servers from multiple zones
98+
scw.WithZones(scw.ZoneFrPar1, scw.ZoneNlAms1, scw.ZonePlWaw1))
99+
if err != nil {
100+
// handle error
101+
}
102+
103+
// Do something with the response...
104+
fmt.Println(response)
105+
}
106+
83107
func Example_createServer() {
84108
// Create a Scaleway client
85109
client, err := scw.NewClient(

internal/generic/sort.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package generic
2+
3+
import (
4+
"reflect"
5+
"sort"
6+
)
7+
8+
// SortSliceByField sorts given slice of struct by passing the specified field to given compare function
9+
// given slice must be a slice of Ptr
10+
func SortSliceByField(list interface{}, field string, compare func(interface{}, interface{}) bool) {
11+
listValue := reflect.ValueOf(list)
12+
sort.SliceStable(list, func(i, j int) bool {
13+
field1 := listValue.Index(i).Elem().FieldByName(field).Interface()
14+
field2 := listValue.Index(j).Elem().FieldByName(field).Interface()
15+
return compare(field1, field2)
16+
})
17+
}

internal/generic/sort_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package generic
2+
3+
import (
4+
"testing"
5+
6+
"github.com/scaleway/scaleway-sdk-go/internal/testhelpers"
7+
)
8+
9+
func Test_SortSliceByField(t *testing.T) {
10+
type Elem struct {
11+
Field string
12+
}
13+
elems := []*Elem{
14+
{"2"},
15+
{"1"},
16+
{"3"},
17+
}
18+
SortSliceByField(elems, "Field", func(i interface{}, i2 interface{}) bool {
19+
return i.(string) < i2.(string)
20+
})
21+
testhelpers.Assert(t, elems[0].Field == "1", "slice is not sorted")
22+
testhelpers.Assert(t, elems[1].Field == "2", "slice is not sorted")
23+
testhelpers.Assert(t, elems[2].Field == "3", "slice is not sorted")
24+
}

scw/client.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ package scw
33
import (
44
"crypto/tls"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"math"
89
"net"
910
"net/http"
1011
"net/http/httputil"
1112
"reflect"
1213
"strconv"
14+
"strings"
15+
"sync"
1316
"sync/atomic"
1417
"time"
1518

1619
"github.com/scaleway/scaleway-sdk-go/internal/auth"
1720
"github.com/scaleway/scaleway-sdk-go/internal/errors"
21+
"github.com/scaleway/scaleway-sdk-go/internal/generic"
1822
"github.com/scaleway/scaleway-sdk-go/logger"
1923
)
2024

@@ -164,6 +168,13 @@ func (c *Client) Do(req *ScalewayRequest, res interface{}, opts ...RequestOption
164168
req.auth = c.auth
165169
}
166170

171+
if req.zones != nil {
172+
return c.doListZones(req, res, req.zones)
173+
}
174+
if req.regions != nil {
175+
return c.doListRegions(req, res, req.regions)
176+
}
177+
167178
if req.allPages {
168179
return c.doListAll(req, res)
169180
}
@@ -340,6 +351,182 @@ func (c *Client) doListAll(req *ScalewayRequest, res interface{}) (err error) {
340351
return errors.New("%T does not support pagination", res)
341352
}
342353

354+
// doListLocalities collects all localities using mutliple list requests and aggregate all results on a lister response
355+
// results is sorted by locality
356+
func (c *Client) doListLocalities(req *ScalewayRequest, res lister, localities []string) (err error) {
357+
path := req.Path
358+
if !strings.Contains(path, "%locality%") {
359+
return fmt.Errorf("request is not a valid locality request")
360+
}
361+
// Requests are parallelized
362+
responseMutex := sync.Mutex{}
363+
requestGroup := sync.WaitGroup{}
364+
errChan := make(chan error, len(localities))
365+
366+
requestGroup.Add(len(localities))
367+
for _, locality := range localities {
368+
go func(locality string) {
369+
defer requestGroup.Done()
370+
// Request is cloned as doListAll will change header
371+
// We remove zones as it would recurse in the same function
372+
req := req.clone()
373+
req.zones = []Zone(nil)
374+
req.Path = strings.ReplaceAll(path, "%locality%", locality)
375+
376+
// We create a new response that we append to main response
377+
zoneResponse := newVariableFromType(res)
378+
err := c.Do(req, zoneResponse)
379+
if err != nil {
380+
errChan <- err
381+
}
382+
responseMutex.Lock()
383+
_, err = res.UnsafeAppend(zoneResponse)
384+
responseMutex.Unlock()
385+
if err != nil {
386+
errChan <- err
387+
}
388+
}(locality)
389+
}
390+
requestGroup.Wait()
391+
392+
L: // We gather potential errors and return them all together
393+
for {
394+
select {
395+
case newErr := <-errChan:
396+
err = errors.Wrap(err, newErr.Error())
397+
default:
398+
break L
399+
}
400+
}
401+
close(errChan)
402+
if err != nil {
403+
return err
404+
}
405+
return nil
406+
}
407+
408+
// doListZones collects all zones using multiple list requests and aggregate all results on a single response.
409+
// result is sorted by zone
410+
func (c *Client) doListZones(req *ScalewayRequest, res interface{}, zones []Zone) (err error) {
411+
if response, isLister := res.(lister); isLister {
412+
// Prepare request with %zone% that can be replaced with actual zone
413+
for _, zone := range AllZones {
414+
if strings.Contains(req.Path, string(zone)) {
415+
req.Path = strings.ReplaceAll(req.Path, string(zone), "%locality%")
416+
break
417+
}
418+
}
419+
if !strings.Contains(req.Path, "%locality%") {
420+
return fmt.Errorf("request is not a valid zoned request")
421+
}
422+
localities := make([]string, 0, len(zones))
423+
for _, zone := range zones {
424+
localities = append(localities, string(zone))
425+
}
426+
427+
err := c.doListLocalities(req, response, localities)
428+
if err != nil {
429+
return fmt.Errorf("failed to list localities: %w", err)
430+
}
431+
432+
sortResponseByZones(res, zones)
433+
return nil
434+
}
435+
436+
return errors.New("%T does not support pagination", res)
437+
}
438+
439+
// doListRegions collects all regions using multiple list requests and aggregate all results on a single response.
440+
// result is sorted by region
441+
func (c *Client) doListRegions(req *ScalewayRequest, res interface{}, regions []Region) (err error) {
442+
if response, isLister := res.(lister); isLister {
443+
// Prepare request with %locality% that can be replaced with actual region
444+
for _, region := range AllRegions {
445+
if strings.Contains(req.Path, string(region)) {
446+
req.Path = strings.ReplaceAll(req.Path, string(region), "%locality%")
447+
break
448+
}
449+
}
450+
if !strings.Contains(req.Path, "%locality%") {
451+
return fmt.Errorf("request is not a valid zoned request")
452+
}
453+
localities := make([]string, 0, len(regions))
454+
for _, region := range regions {
455+
localities = append(localities, string(region))
456+
}
457+
458+
err := c.doListLocalities(req, response, localities)
459+
if err != nil {
460+
return fmt.Errorf("failed to list localities: %w", err)
461+
}
462+
463+
sortResponseByRegions(res, regions)
464+
return nil
465+
}
466+
467+
return errors.New("%T does not support pagination", res)
468+
}
469+
470+
// sortSliceByZones sorts a slice of struct using a Zone field that should exist
471+
func sortSliceByZones(list interface{}, zones []Zone) {
472+
zoneMap := map[Zone]int{}
473+
for i, zone := range zones {
474+
zoneMap[zone] = i
475+
}
476+
generic.SortSliceByField(list, "Zone", func(i interface{}, i2 interface{}) bool {
477+
return zoneMap[i.(Zone)] < zoneMap[i2.(Zone)]
478+
})
479+
}
480+
481+
// sortSliceByRegions sorts a slice of struct using a Region field that should exist
482+
func sortSliceByRegions(list interface{}, regions []Region) {
483+
regionMap := map[Region]int{}
484+
for i, region := range regions {
485+
regionMap[region] = i
486+
}
487+
generic.SortSliceByField(list, "Region", func(i interface{}, i2 interface{}) bool {
488+
return regionMap[i.(Region)] < regionMap[i2.(Region)]
489+
})
490+
}
491+
492+
// sortResponseByZones find first field that is a slice in a struct and sort it by zone
493+
func sortResponseByZones(res interface{}, zones []Zone) {
494+
// res may be ListServersResponse
495+
//
496+
// type ListServersResponse struct {
497+
// TotalCount uint32 `json:"total_count"`
498+
// Servers []*Server `json:"servers"`
499+
// }
500+
// We iterate over fields searching for the slice one to sort it
501+
resType := reflect.TypeOf(res).Elem()
502+
fields := reflect.VisibleFields(resType)
503+
for _, field := range fields {
504+
if field.Type.Kind() == reflect.Slice {
505+
sortSliceByZones(reflect.ValueOf(res).Elem().FieldByName(field.Name).Interface(), zones)
506+
return
507+
}
508+
}
509+
}
510+
511+
// sortResponseByRegions find first field that is a slice in a struct and sort it by region
512+
func sortResponseByRegions(res interface{}, regions []Region) {
513+
// res may be ListServersResponse
514+
//
515+
// type ListServersResponse struct {
516+
// TotalCount uint32 `json:"total_count"`
517+
// Servers []*Server `json:"servers"`
518+
// }
519+
// We iterate over fields searching for the slice one to sort it
520+
resType := reflect.TypeOf(res).Elem()
521+
fields := reflect.VisibleFields(resType)
522+
for _, field := range fields {
523+
if field.Type.Kind() == reflect.Slice {
524+
sortSliceByRegions(reflect.ValueOf(res).Elem().FieldByName(field.Name).Interface(), regions)
525+
return
526+
}
527+
}
528+
}
529+
343530
// newVariableFromType returns a variable set to the zero value of the given type
344531
func newVariableFromType(t interface{}) interface{} {
345532
// reflect.New always create a pointer, that's why we use reflect.Indirect before

scw/request.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type ScalewayRequest struct {
2424
ctx context.Context
2525
auth auth.Auth
2626
allPages bool
27+
zones []Zone
28+
regions []Region
2729
}
2830

2931
// getAllHeaders constructs a http.Header object and aggregates all headers into the object.
@@ -102,3 +104,19 @@ func (req *ScalewayRequest) validate() error {
102104
// nothing so far
103105
return nil
104106
}
107+
108+
func (req *ScalewayRequest) clone() *ScalewayRequest {
109+
clonedReq := &ScalewayRequest{
110+
Method: req.Method,
111+
Path: req.Path,
112+
Headers: req.Headers.Clone(),
113+
ctx: req.ctx,
114+
auth: req.auth,
115+
allPages: req.allPages,
116+
zones: req.zones,
117+
}
118+
if req.Query != nil {
119+
clonedReq.Query = url.Values(http.Header(req.Query).Clone())
120+
}
121+
return clonedReq
122+
}

scw/request_option.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,21 @@ func WithAuthRequest(accessKey, secretKey string) RequestOption {
3030
s.auth = auth.NewToken(accessKey, secretKey)
3131
}
3232
}
33+
34+
// WithZones aggregate results from requested zones in the response of a List request.
35+
// response rows are sorted by zone using order of given zones
36+
// Will error when pagination is not supported on the request.
37+
func WithZones(zones ...Zone) RequestOption {
38+
return func(s *ScalewayRequest) {
39+
s.zones = append(s.zones, zones...)
40+
}
41+
}
42+
43+
// WithRegions aggregate results from requested regions in the response of a List request.
44+
// response rows are sorted by region using order of given regions
45+
// Will error when pagination is not supported on the request.
46+
func WithRegions(regions ...Region) RequestOption {
47+
return func(s *ScalewayRequest) {
48+
s.regions = append(s.regions, regions...)
49+
}
50+
}

0 commit comments

Comments
 (0)