client_option.go raw

   1  package scw
   2  
   3  import (
   4  	"net/http"
   5  	"strings"
   6  
   7  	"github.com/scaleway/scaleway-sdk-go/errors"
   8  	"github.com/scaleway/scaleway-sdk-go/internal/auth"
   9  	"github.com/scaleway/scaleway-sdk-go/validation"
  10  )
  11  
  12  // ClientOption is a function which applies options to a settings object.
  13  type ClientOption func(*settings)
  14  
  15  // httpClient wraps the net/http Client Do method
  16  type httpClient interface {
  17  	Do(*http.Request) (*http.Response, error)
  18  }
  19  
  20  // WithHTTPClient client option allows passing a custom http.Client which will be used for all requests.
  21  func WithHTTPClient(httpClient httpClient) ClientOption {
  22  	return func(s *settings) {
  23  		s.httpClient = httpClient
  24  	}
  25  }
  26  
  27  // WithoutAuth client option sets the client token to an empty token.
  28  func WithoutAuth() ClientOption {
  29  	return func(s *settings) {
  30  		s.token = auth.NewNoAuth()
  31  	}
  32  }
  33  
  34  // WithAuth client option sets the client access key and secret key.
  35  func WithAuth(accessKey, secretKey string) ClientOption {
  36  	return func(s *settings) {
  37  		s.token = auth.NewToken(accessKey, secretKey)
  38  	}
  39  }
  40  
  41  // WithJWT client option sets the client session token.
  42  func WithJWT(token string) ClientOption {
  43  	return func(s *settings) {
  44  		s.token = auth.NewJWT(token)
  45  	}
  46  }
  47  
  48  // WithAPIURL client option overrides the API URL of the Scaleway API to the given URL.
  49  func WithAPIURL(apiURL string) ClientOption {
  50  	return func(s *settings) {
  51  		s.apiURL = apiURL
  52  	}
  53  }
  54  
  55  // WithInsecure client option enables insecure transport on the client.
  56  func WithInsecure() ClientOption {
  57  	return func(s *settings) {
  58  		s.insecure = true
  59  	}
  60  }
  61  
  62  // WithUserAgent client option append a user agent to the default user agent of the SDK.
  63  func WithUserAgent(ua string) ClientOption {
  64  	return func(s *settings) {
  65  		if s.userAgent != "" && ua != "" {
  66  			s.userAgent += " "
  67  		}
  68  		s.userAgent += ua
  69  	}
  70  }
  71  
  72  // withDefaultUserAgent client option overrides the default user agent of the SDK.
  73  func withDefaultUserAgent(ua string) ClientOption {
  74  	return func(s *settings) {
  75  		s.userAgent = ua
  76  	}
  77  }
  78  
  79  // WithProfile client option configures a client from the given profile.
  80  func WithProfile(p *Profile) ClientOption {
  81  	return func(s *settings) {
  82  		accessKey := ""
  83  		if p.AccessKey != nil {
  84  			accessKey = *p.AccessKey
  85  			s.token = auth.NewAccessKeyOnly(accessKey)
  86  		}
  87  
  88  		if p.SecretKey != nil {
  89  			s.token = auth.NewToken(accessKey, *p.SecretKey)
  90  		}
  91  
  92  		if p.APIURL != nil {
  93  			s.apiURL = *p.APIURL
  94  		}
  95  
  96  		if p.Insecure != nil {
  97  			s.insecure = *p.Insecure
  98  		}
  99  
 100  		if p.DefaultOrganizationID != nil {
 101  			organizationID := *p.DefaultOrganizationID
 102  			s.defaultOrganizationID = &organizationID
 103  		}
 104  
 105  		if p.DefaultProjectID != nil {
 106  			projectID := *p.DefaultProjectID
 107  			s.defaultProjectID = &projectID
 108  		}
 109  
 110  		if p.DefaultRegion != nil {
 111  			defaultRegion := Region(*p.DefaultRegion)
 112  			s.defaultRegion = &defaultRegion
 113  		}
 114  
 115  		if p.DefaultZone != nil {
 116  			defaultZone := Zone(*p.DefaultZone)
 117  			s.defaultZone = &defaultZone
 118  		}
 119  	}
 120  }
 121  
 122  // WithEnv client option configures a client from the environment variables.
 123  func WithEnv() ClientOption {
 124  	return WithProfile(LoadEnvProfile())
 125  }
 126  
 127  // WithDefaultOrganizationID client option sets the client default organization ID.
 128  //
 129  // It will be used as the default value of the organization_id field in all requests made with this client.
 130  func WithDefaultOrganizationID(organizationID string) ClientOption {
 131  	return func(s *settings) {
 132  		s.defaultOrganizationID = &organizationID
 133  	}
 134  }
 135  
 136  // WithDefaultProjectID client option sets the client default project ID.
 137  //
 138  // It will be used as the default value of the projectID field in all requests made with this client.
 139  func WithDefaultProjectID(projectID string) ClientOption {
 140  	return func(s *settings) {
 141  		s.defaultProjectID = &projectID
 142  	}
 143  }
 144  
 145  // WithDefaultRegion client option sets the client default region.
 146  //
 147  // It will be used as the default value of the region field in all requests made with this client.
 148  func WithDefaultRegion(region Region) ClientOption {
 149  	return func(s *settings) {
 150  		s.defaultRegion = &region
 151  	}
 152  }
 153  
 154  // WithDefaultZone client option sets the client default zone.
 155  //
 156  // It will be used as the default value of the zone field in all requests made with this client.
 157  func WithDefaultZone(zone Zone) ClientOption {
 158  	return func(s *settings) {
 159  		s.defaultZone = &zone
 160  	}
 161  }
 162  
 163  // WithDefaultPageSize client option overrides the default page size of the SDK.
 164  //
 165  // It will be used as the default value of the page_size field in all requests made with this client.
 166  func WithDefaultPageSize(pageSize uint32) ClientOption {
 167  	return func(s *settings) {
 168  		s.defaultPageSize = &pageSize
 169  	}
 170  }
 171  
 172  // settings hold the values of all client options
 173  type settings struct {
 174  	apiURL                string
 175  	token                 auth.Auth
 176  	userAgent             string
 177  	httpClient            httpClient
 178  	insecure              bool
 179  	defaultOrganizationID *string
 180  	defaultProjectID      *string
 181  	defaultRegion         *Region
 182  	defaultZone           *Zone
 183  	defaultPageSize       *uint32
 184  }
 185  
 186  func newSettings() *settings {
 187  	return &settings{}
 188  }
 189  
 190  func (s *settings) apply(opts []ClientOption) {
 191  	for _, opt := range opts {
 192  		opt(s)
 193  	}
 194  }
 195  
 196  func (s *settings) validate() error {
 197  	// Auth.
 198  	if s.token == nil {
 199  		// It should not happen, WithoutAuth option is used by default.
 200  		panic(errors.New("no credential option provided"))
 201  	}
 202  	if token, isToken := s.token.(*auth.Token); isToken {
 203  		if token.AccessKey == "" {
 204  			return NewInvalidClientOptionError("access key cannot be empty")
 205  		}
 206  		if !validation.IsAccessKey(token.AccessKey) {
 207  			return NewInvalidClientOptionError("invalid access key format '%s', expected SCWXXXXXXXXXXXXXXXXX format", token.AccessKey)
 208  		}
 209  		if token.SecretKey == "" {
 210  			return NewInvalidClientOptionError("secret key cannot be empty")
 211  		}
 212  		if !validation.IsSecretKey(token.SecretKey) {
 213  			return NewInvalidClientOptionError("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token.SecretKey)
 214  		}
 215  	}
 216  
 217  	// Default Organization ID.
 218  	if s.defaultOrganizationID != nil {
 219  		if *s.defaultOrganizationID == "" {
 220  			return NewInvalidClientOptionError("default organization ID cannot be empty")
 221  		}
 222  		if !validation.IsOrganizationID(*s.defaultOrganizationID) {
 223  			return NewInvalidClientOptionError("invalid organization ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultOrganizationID)
 224  		}
 225  	}
 226  
 227  	// Default Project ID.
 228  	if s.defaultProjectID != nil {
 229  		if *s.defaultProjectID == "" {
 230  			return NewInvalidClientOptionError("default project ID cannot be empty")
 231  		}
 232  		if !validation.IsProjectID(*s.defaultProjectID) {
 233  			return NewInvalidClientOptionError("invalid project ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultProjectID)
 234  		}
 235  	}
 236  
 237  	// Default Region.
 238  	if s.defaultRegion != nil {
 239  		if *s.defaultRegion == "" {
 240  			return NewInvalidClientOptionError("default region cannot be empty")
 241  		}
 242  		if !validation.IsRegion(string(*s.defaultRegion)) {
 243  			regions := []string(nil)
 244  			for _, r := range AllRegions {
 245  				regions = append(regions, string(r))
 246  			}
 247  			return NewInvalidClientOptionError("invalid default region format '%s', available regions are: %s", *s.defaultRegion, strings.Join(regions, ", "))
 248  		}
 249  	}
 250  
 251  	// Default Zone.
 252  	if s.defaultZone != nil {
 253  		if *s.defaultZone == "" {
 254  			return NewInvalidClientOptionError("default zone cannot be empty")
 255  		}
 256  		if !validation.IsZone(string(*s.defaultZone)) {
 257  			zones := []string(nil)
 258  			for _, z := range AllZones {
 259  				zones = append(zones, string(z))
 260  			}
 261  			return NewInvalidClientOptionError("invalid default zone format '%s', available zones are: %s", *s.defaultZone, strings.Join(zones, ", "))
 262  		}
 263  	}
 264  
 265  	// API URL.
 266  	if !validation.IsURL(s.apiURL) {
 267  		return NewInvalidClientOptionError("invalid API url '%s'", s.apiURL)
 268  	}
 269  	if s.apiURL[len(s.apiURL)-1:] == "/" {
 270  		return NewInvalidClientOptionError("invalid API url '%s' it should not have a trailing slash", s.apiURL)
 271  	}
 272  
 273  	// TODO: check for max s.defaultPageSize
 274  
 275  	return nil
 276  }
 277