@@ -3,18 +3,22 @@ package scw
33import (
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
344531func newVariableFromType (t interface {}) interface {} {
345532 // reflect.New always create a pointer, that's why we use reflect.Indirect before
0 commit comments