github.com/opentelekomcloud/gophertelekomcloud@v0.9.3/provider_client.go (about) 1 package golangsdk 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "net/http" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" 13 ) 14 15 // DefaultUserAgent is the default User-Agent string set in the request header. 16 const DefaultUserAgent = "golangsdk/2.0.0" 17 18 // UserAgent represents a User-Agent header. 19 type UserAgent struct { 20 // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. 21 // All the strings to prepend are accumulated and prepended in the Join method. 22 prepend []string 23 } 24 25 // Prepend prepends a user-defined string to the default User-Agent string. Users 26 // may pass in one or more strings to prepend. 27 func (ua *UserAgent) Prepend(s ...string) { 28 ua.prepend = append(s, ua.prepend...) 29 } 30 31 // Join concatenates all the user-defined User-Agent strings with the default 32 // Gophercloud User-Agent string. 33 func (ua *UserAgent) Join() string { 34 uaSlice := append(ua.prepend, DefaultUserAgent) 35 return strings.Join(uaSlice, " ") 36 } 37 38 // ProviderClient stores details that are required to interact with any 39 // services within a specific provider's API. 40 // 41 // Generally, you acquire a ProviderClient by calling the NewClient method in 42 // the appropriate provider's child package, providing whatever authentication 43 // credentials are required. 44 type ProviderClient struct { 45 // IdentityBase is the base URL used for a particular provider's identity 46 // service - it will be used when issuing authentication requests. It 47 // should point to the root resource of the identity service, not a specific 48 // identity version. 49 IdentityBase string 50 51 // IdentityEndpoint is the identity endpoint. This may be a specific version 52 // of the identity service. If this is the case, this endpoint is used rather 53 // than querying versions first. 54 IdentityEndpoint string 55 56 // TokenID is the ID of the most recently issued valid token. 57 // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. 58 // To safely read or write this value, call `Token` or `SetToken`, respectively 59 TokenID string 60 61 // ProjectID is the ID of project to which User is authorized. 62 ProjectID string 63 64 // UserID is the ID of the authorized user 65 UserID string 66 67 // DomainID is the ID of project to which User is authorized. 68 DomainID string 69 70 // RegionID is the Name of region to which User is authorized. 71 RegionID string 72 73 // EndpointLocator describes how this provider discovers the endpoints for 74 // its constituent services. 75 EndpointLocator EndpointLocator 76 77 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. 78 HTTPClient http.Client 79 80 // UserAgent represents the User-Agent header in the HTTP request. 81 UserAgent UserAgent 82 83 // MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to defaultMaxBackoffRetryLimit 84 MaxBackoffRetries *int 85 // BackoffRetryTimeout specifies time before next retry on 429. When not set, defaults to defaultBackoffTimeout 86 BackoffRetryTimeout *time.Duration 87 // ReauthFunc is the function used to re-authenticate the user if the request 88 // fails with a 401 HTTP response code. This a needed because there may be multiple 89 // authentication functions for different Identity service versions. 90 ReauthFunc func() error 91 92 // AKSKAuthOptions provides the value for AK/SK authentication, it should be nil if you use token authentication, 93 // Otherwise, it must have a value 94 AKSKAuthOptions AKSKAuthOptions 95 96 mut *sync.RWMutex 97 98 reauthmut *reauthlock 99 } 100 101 type reauthlock struct { 102 sync.RWMutex 103 reauthing bool 104 } 105 106 // AuthenticatedHeaders returns a map of HTTP headers that are common for all 107 // authenticated service requests. 108 func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { 109 if client.reauthmut != nil { 110 client.reauthmut.RLock() 111 if client.reauthmut.reauthing { 112 client.reauthmut.RUnlock() 113 return 114 } 115 client.reauthmut.RUnlock() 116 } 117 t := client.Token() 118 if t == "" { 119 return 120 } 121 return map[string]string{"X-Auth-Token": t} 122 } 123 124 // UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. 125 // If the application's ProviderClient is not used concurrently, this doesn't need to be called. 126 func (client *ProviderClient) UseTokenLock() { 127 client.mut = new(sync.RWMutex) 128 client.reauthmut = new(reauthlock) 129 } 130 131 // Token safely reads the value of the auth token from the ProviderClient. Applications should 132 // call this method to access the token instead of the TokenID field 133 func (client *ProviderClient) Token() string { 134 if client.mut != nil { 135 client.mut.RLock() 136 defer client.mut.RUnlock() 137 } 138 return client.TokenID 139 } 140 141 // SetToken safely sets the value of the auth token in the ProviderClient. Applications may 142 // use this method in a custom ReauthFunc 143 func (client *ProviderClient) SetToken(t string) { 144 if client.mut != nil { 145 client.mut.Lock() 146 defer client.mut.Unlock() 147 } 148 client.TokenID = t 149 } 150 151 // RequestOpts customizes the behavior of the provider.Request() method. 152 type RequestOpts struct { 153 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The 154 // content type of the request will default to "application/json" unless overridden by MoreHeaders. 155 // It's an error to specify both a JSONBody and a RawBody. 156 JSONBody interface{} 157 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type 158 // will be set unless one is provided explicitly by MoreHeaders. 159 RawBody io.Reader 160 // JSONResponse, if provided, will be populated with the contents of the response body parsed as JSON. 161 // Not that setting it will drain and close Response.Body. 162 // Deprecated: Use http.Response Body instead. 163 JSONResponse interface{} 164 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If 165 // the response has a different code, an error will be returned. 166 OkCodes []int 167 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is 168 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress 169 // the default Accept header or an inferred Content-Type, for example. 170 MoreHeaders map[string]string 171 // ErrorContext specifies the resource error type to return if an error is encountered. 172 // This lets resources override default error messages based on the response status code. 173 ErrorContext error 174 175 // RetryCount specifies number of times retriable errors (502, 504) will be retried 176 RetryCount *int 177 // RetryTimeout specifies time before next retry 178 RetryTimeout *time.Duration 179 } 180 181 var applicationJSON = "application/json" 182 183 // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication 184 // header will automatically be provided. 185 func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { 186 var body io.Reader 187 var contentType *string 188 189 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided 190 // io.ReadSeeker as-is. Default the content-type to application/json. 191 if options.JSONBody != nil { 192 if options.RawBody != nil { 193 panic("Please provide only one of JSONBody or RawBody to golangsdk.Request().") 194 } 195 196 rendered, err := extract.JsonMarshal(options.JSONBody) 197 if err != nil { 198 return nil, err 199 } 200 201 body = bytes.NewReader(rendered) 202 contentType = &applicationJSON 203 } 204 205 if options.RawBody != nil { 206 body = options.RawBody 207 } 208 209 // Construct the http.Request. 210 req, err := http.NewRequest(method, url, body) 211 if err != nil { 212 return nil, err 213 } 214 215 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to 216 // modify or omit any header. 217 if contentType != nil { 218 req.Header.Set("Content-Type", *contentType) 219 } 220 req.Header.Set("Accept", applicationJSON) 221 222 // Set the User-Agent header 223 req.Header.Set("User-Agent", client.UserAgent.Join()) 224 225 if options.MoreHeaders != nil { 226 for k, v := range options.MoreHeaders { 227 if v != "" { 228 req.Header.Set(k, v) 229 } else { 230 req.Header.Del(k) 231 } 232 } 233 } 234 235 // get the latest token from client 236 for k, v := range client.AuthenticatedHeaders() { 237 req.Header.Set(k, v) 238 } 239 240 // Set connection parameter to close the connection immediately when we've got the response 241 req.Close = true 242 243 prereqtok := req.Header.Get("X-Auth-Token") 244 245 if client.AKSKAuthOptions.AccessKey != "" { 246 Sign(req, SignOptions{ 247 AccessKey: client.AKSKAuthOptions.AccessKey, 248 SecretKey: client.AKSKAuthOptions.SecretKey, 249 }) 250 if client.AKSKAuthOptions.ProjectId != "" && client.AKSKAuthOptions.DomainID == "" { 251 req.Header.Set("X-Project-Id", client.AKSKAuthOptions.ProjectId) 252 } 253 if client.AKSKAuthOptions.DomainID != "" { 254 req.Header.Set("X-Domain-Id", client.AKSKAuthOptions.DomainID) 255 } 256 if client.AKSKAuthOptions.SecurityToken != "" { 257 req.Header.Set("X-Security-Token", client.AKSKAuthOptions.SecurityToken) 258 } 259 } 260 261 // Issue the request. 262 resp, err := client.HTTPClient.Do(req) 263 if err != nil { 264 return nil, err 265 } 266 267 // Allow default OkCodes if none explicitly set 268 if options.OkCodes == nil { 269 options.OkCodes = defaultOkCodes(method) 270 } 271 272 if options.RetryCount == nil { 273 defaultRetryLimit := 1 274 options.RetryCount = &defaultRetryLimit 275 } 276 277 if options.RetryTimeout == nil { 278 defaultRetryTimeout := 500 * time.Millisecond 279 options.RetryTimeout = &defaultRetryTimeout 280 } 281 282 if client.MaxBackoffRetries == nil { 283 defaultMaxBackoffRetryLimit := 20 284 client.MaxBackoffRetries = &defaultMaxBackoffRetryLimit 285 } 286 287 if client.BackoffRetryTimeout == nil { 288 defaultBackoffTimeout := 60 * time.Second 289 client.BackoffRetryTimeout = &defaultBackoffTimeout 290 } 291 292 // Validate the HTTP response status. 293 var ok bool 294 for _, code := range options.OkCodes { 295 if resp.StatusCode == code { 296 ok = true 297 break 298 } 299 } 300 301 if !ok { 302 body, _ := io.ReadAll(resp.Body) 303 _ = resp.Body.Close() 304 respErr := ErrUnexpectedResponseCode{ 305 URL: url, 306 Method: method, 307 Expected: options.OkCodes, 308 Actual: resp.StatusCode, 309 Body: body, 310 } 311 312 errType := options.ErrorContext 313 switch resp.StatusCode { 314 case http.StatusBadRequest: 315 err = ErrDefault400{respErr} 316 if error400er, ok := errType.(Err400er); ok { 317 err = error400er.Error400(respErr) 318 } 319 case http.StatusUnauthorized: 320 if client.ReauthFunc != nil { 321 if client.mut != nil { 322 client.mut.Lock() 323 client.reauthmut.Lock() 324 client.reauthmut.reauthing = true 325 client.reauthmut.Unlock() 326 if curtok := client.TokenID; curtok == prereqtok { 327 err = client.ReauthFunc() 328 } 329 client.reauthmut.Lock() 330 client.reauthmut.reauthing = false 331 client.reauthmut.Unlock() 332 client.mut.Unlock() 333 } else { 334 err = client.ReauthFunc() 335 } 336 if err != nil { 337 e := &ErrUnableToReauthenticate{} 338 e.ErrOriginal = respErr 339 return nil, e 340 } 341 if options.RawBody != nil { 342 if seeker, ok := options.RawBody.(io.Seeker); ok { 343 _, e := seeker.Seek(0, 0) 344 if e != nil { 345 return nil, e 346 } 347 } 348 } 349 resp, err = client.Request(method, url, options) 350 if err != nil { 351 e := &ErrErrorAfterReauthentication{} 352 e.ErrOriginal = err 353 return nil, e 354 } 355 return resp, nil 356 } 357 err = ErrDefault401{respErr} 358 if error401er, ok := errType.(Err401er); ok { 359 err = error401er.Error401(respErr) 360 } 361 case http.StatusForbidden: 362 err = ErrDefault403{respErr} 363 if error403er, ok := errType.(Err403er); ok { 364 err = error403er.Error403(respErr) 365 } 366 case http.StatusNotFound: 367 err = ErrDefault404{respErr} 368 if error404er, ok := errType.(Err404er); ok { 369 err = error404er.Error404(respErr) 370 } 371 case http.StatusMethodNotAllowed: 372 err = ErrDefault405{respErr} 373 if error405er, ok := errType.(Err405er); ok { 374 err = error405er.Error405(respErr) 375 } 376 case http.StatusRequestTimeout: 377 err = ErrDefault408{respErr} 378 if error408er, ok := errType.(Err408er); ok { 379 err = error408er.Error408(respErr) 380 } 381 case http.StatusConflict: 382 err = ErrDefault409{respErr} 383 if error409er, ok := errType.(Err409er); ok { 384 err = error409er.Error409(respErr) 385 } 386 case http.StatusTooManyRequests: 387 err = ErrDefault429{respErr} 388 if error429er, ok := errType.(Err429er); ok { 389 err = error429er.Error429(respErr) 390 } 391 if *client.MaxBackoffRetries > 0 { 392 *client.MaxBackoffRetries -= 1 393 time.Sleep(*client.BackoffRetryTimeout) 394 return client.Request(method, url, options) 395 } 396 case http.StatusInternalServerError: 397 err = ErrDefault500{respErr} 398 if error500er, ok := errType.(Err500er); ok { 399 err = error500er.Error500(respErr) 400 } 401 case http.StatusBadGateway, http.StatusGatewayTimeout: // gateway errors 402 if *options.RetryCount > 0 { 403 *options.RetryCount -= 1 404 time.Sleep(*options.RetryTimeout) 405 return client.Request(method, url, options) 406 } 407 case http.StatusServiceUnavailable: 408 err = ErrDefault503{respErr} 409 if error503er, ok := errType.(Err503er); ok { 410 err = error503er.Error503(respErr) 411 } 412 } 413 414 if err == nil { 415 err = respErr 416 } 417 418 return resp, err 419 } 420 421 // Parse the response body as JSON, if requested to do so. 422 // TODO: When all refactoring of the extract is done, remove this. 423 if options.JSONResponse != nil && resp.StatusCode != http.StatusNoContent { 424 switch r := options.JSONResponse.(type) { 425 case *[]byte: 426 data, err := io.ReadAll(resp.Body) 427 if err != nil { 428 return nil, fmt.Errorf("error reading response body: %w", err) 429 } 430 defer func(Body io.ReadCloser) { 431 err := Body.Close() 432 if err != nil { 433 _ = fmt.Errorf("error in closing : %w", err) 434 } 435 }(resp.Body) 436 437 *r = data 438 default: 439 if err := extract.Into(resp.Body, &r); err != nil { 440 return nil, err 441 } 442 } 443 } 444 445 return resp, nil 446 } 447 448 func defaultOkCodes(method string) []int { 449 switch method { 450 case "GET": 451 return []int{200} 452 case "POST": 453 return []int{201, 202} 454 case "PUT": 455 return []int{201, 202} 456 case "PATCH": 457 return []int{200, 204} 458 case "DELETE": 459 return []int{202, 204} 460 case "HEAD": 461 return []int{204, 206} 462 } 463 return []int{} 464 }