github.com/IBM-Cloud/bluemix-go@v0.0.0-20240423071914-9e96525baef4/client/client.go (about) 1 //Package client provides a generic client to be used by all services 2 package client 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "log" 8 "net" 9 gohttp "net/http" 10 "path" 11 "strings" 12 "sync" 13 "time" 14 15 bluemix "github.com/IBM-Cloud/bluemix-go" 16 "github.com/IBM-Cloud/bluemix-go/bmxerror" 17 "github.com/IBM-Cloud/bluemix-go/http" 18 "github.com/IBM-Cloud/bluemix-go/rest" 19 ) 20 21 //TokenProvider ... 22 type TokenProvider interface { 23 RefreshToken() (string, error) 24 GetPasscode() (string, error) 25 AuthenticatePassword(string, string) error 26 AuthenticateAPIKey(string) error 27 } 28 29 /*type PaginatedResourcesHandler interface { 30 Resources(rawResponse []byte, curPath string) (resources []interface{}, nextPath string, err error) 31 } 32 33 //HandlePagination ... 34 type HandlePagination func(c *Client, path string, paginated PaginatedResourcesHandler, cb func(interface{}) bool) (resp *gohttp.Response, err error) 35 */ 36 37 //Client is the base client for all service api client 38 type Client struct { 39 Config *bluemix.Config 40 DefaultHeader gohttp.Header 41 ServiceName bluemix.ServiceName 42 TokenRefresher TokenProvider 43 //HandlePagination HandlePagination 44 45 headerLock sync.Mutex 46 } 47 48 //Config stores any generic service client configurations 49 type Config struct { 50 Config *bluemix.Config 51 Endpoint string 52 } 53 54 //New ... 55 func New(c *bluemix.Config, serviceName bluemix.ServiceName, refresher TokenProvider) *Client { 56 return &Client{ 57 Config: c, 58 ServiceName: serviceName, 59 TokenRefresher: refresher, 60 //HandlePagination: pagination, 61 DefaultHeader: getDefaultAuthHeaders(serviceName, c), 62 } 63 } 64 65 //SendRequest ... 66 func (c *Client) SendRequest(r *rest.Request, respV interface{}) (*gohttp.Response, error) { 67 68 retries := *c.Config.MaxRetries 69 if retries < 1 { 70 return c.MakeRequest(r, respV) 71 } 72 wait := *c.Config.RetryDelay 73 74 return c.tryHTTPRequest(retries, wait, r, respV) 75 } 76 77 // MakeRequest ... 78 func (c *Client) MakeRequest(r *rest.Request, respV interface{}) (*gohttp.Response, error) { 79 httpClient := c.Config.HTTPClient 80 if httpClient == nil { 81 httpClient = gohttp.DefaultClient 82 } 83 restClient := &rest.Client{ 84 DefaultHeader: c.DefaultHeader, 85 HTTPClient: httpClient, 86 } 87 resp, err := restClient.Do(r, respV, nil) 88 // The response returned by go HTTP client.Do() could be nil if request timeout. 89 // For convenience, we ensure that response returned by this method is always not nil. 90 if resp == nil { 91 return new(gohttp.Response), err 92 } 93 if err != nil { 94 if (resp.StatusCode == 401 || resp.StatusCode == 403) && c.TokenRefresher != nil { 95 log.Println("Authentication failed. Trying token refresh") 96 c.headerLock.Lock() 97 defer c.headerLock.Unlock() 98 var err error 99 if c.Config.BluemixAPIKey != "" { 100 log.Println("Retrying authentication using API Key") 101 err = c.TokenRefresher.AuthenticateAPIKey(c.Config.BluemixAPIKey) 102 } else { 103 log.Println("Retrying authentication using Refresh Token") 104 _, err = c.TokenRefresher.RefreshToken() 105 } 106 switch err.(type) { 107 case nil: 108 restClient.DefaultHeader = getDefaultAuthHeaders(c.ServiceName, c.Config) 109 for k := range c.DefaultHeader { 110 r.Del(k) 111 } 112 c.DefaultHeader = restClient.DefaultHeader 113 resp, err := restClient.Do(r, respV, nil) 114 if resp == nil { 115 return new(gohttp.Response), err 116 } 117 if err != nil { 118 err = bmxerror.WrapNetworkErrors(resp.Request.URL.Host, err) 119 } 120 return resp, err 121 case *bmxerror.InvalidTokenError: 122 return resp, bmxerror.NewRequestFailure("InvalidToken", fmt.Sprintf("%v", err), 401) 123 default: 124 return resp, fmt.Errorf("Authentication failed, Unable to refresh auth token: %v. Try again later", err) 125 } 126 } 127 128 } 129 return resp, err 130 } 131 132 func (c *Client) tryHTTPRequest(retries int, wait time.Duration, r *rest.Request, respV interface{}) (*gohttp.Response, error) { 133 134 resp, err := c.MakeRequest(r, respV) 135 if err != nil { 136 if !isRetryable(err) { 137 if resp == nil { 138 return new(gohttp.Response), err 139 } 140 return resp, err 141 } 142 if retries--; retries >= 0 { 143 time.Sleep(wait) 144 return c.tryHTTPRequest( 145 retries, wait, r, respV) 146 } 147 } 148 if resp == nil { 149 return new(gohttp.Response), err 150 } 151 return resp, err 152 } 153 154 //Get ... 155 func (c *Client) Get(path string, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 156 r := rest.GetRequest(c.URL(path)) 157 for _, t := range extraHeader { 158 addToRequestHeader(t, r) 159 } 160 return c.SendRequest(r, respV) 161 } 162 163 //Put ... 164 func (c *Client) Put(path string, data interface{}, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 165 r := rest.PutRequest(c.URL(path)).Body(data) 166 for _, t := range extraHeader { 167 addToRequestHeader(t, r) 168 } 169 return c.SendRequest(r, respV) 170 } 171 172 //Patch ... 173 func (c *Client) Patch(path string, data interface{}, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 174 r := rest.PatchRequest(c.URL(path)).Body(data) 175 for _, t := range extraHeader { 176 addToRequestHeader(t, r) 177 } 178 return c.SendRequest(r, respV) 179 } 180 181 //Post ... 182 func (c *Client) Post(path string, data interface{}, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 183 r := rest.PostRequest(c.URL(path)).Body(data) 184 for _, t := range extraHeader { 185 addToRequestHeader(t, r) 186 } 187 188 return c.SendRequest(r, respV) 189 } 190 191 //PostWithForm ... 192 func (c *Client) PostWithForm(path string, form interface{}, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 193 r := rest.PostRequest(c.URL(path)) 194 for _, t := range extraHeader { 195 addToRequestHeader(t, r) 196 } 197 addToRequestForm(form, r) 198 199 return c.SendRequest(r, respV) 200 } 201 202 //Delete ... 203 func (c *Client) Delete(path string, extraHeader ...interface{}) (*gohttp.Response, error) { 204 r := rest.DeleteRequest(c.URL(path)) 205 for _, t := range extraHeader { 206 addToRequestHeader(t, r) 207 } 208 return c.SendRequest(r, nil) 209 } 210 211 //DeleteWithResp ... 212 func (c *Client) DeleteWithResp(path string, respV interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 213 r := rest.DeleteRequest(c.URL(path)) 214 for _, t := range extraHeader { 215 addToRequestHeader(t, r) 216 } 217 return c.SendRequest(r, respV) 218 } 219 220 //DeleteWithBody ... 221 func (c *Client) DeleteWithBody(path string, data interface{}, extraHeader ...interface{}) (*gohttp.Response, error) { 222 r := rest.DeleteRequest(c.URL(path)).Body(data) 223 for _, t := range extraHeader { 224 addToRequestHeader(t, r) 225 } 226 return c.SendRequest(r, nil) 227 } 228 229 func addToRequestHeader(h interface{}, r *rest.Request) { 230 switch v := h.(type) { 231 case map[string]string: 232 for key, value := range v { 233 r.Set(key, value) 234 } 235 } 236 } 237 238 func addToRequestForm(h interface{}, r *rest.Request) { 239 switch v := h.(type) { 240 case map[string]string: 241 for key, value := range v { 242 r.Field(key, value) 243 } 244 } 245 } 246 247 /*//GetPaginated ... 248 func (c *Client) GetPaginated(path string, paginated PaginatedResourcesHandler, cb func(interface{}) bool) (resp *gohttp.Response, err error) { 249 return c.HandlePagination(c, path, paginated, cb) 250 }*/ 251 252 type PaginatedResourcesHandler interface { 253 Resources(rawResponse []byte, curPath string) (resources []interface{}, nextPath string, err error) 254 } 255 256 func (c *Client) GetPaginated(path string, paginated PaginatedResourcesHandler, cb func(interface{}) bool) (resp *gohttp.Response, err error) { 257 for path != "" { 258 var raw json.RawMessage 259 resp, err = c.Get(path, &raw) 260 if err != nil { 261 return 262 } 263 264 var resources []interface{} 265 var nextPath string 266 resources, nextPath, err = paginated.Resources([]byte(raw), path) 267 if err != nil { 268 err = fmt.Errorf("%s: Error parsing JSON", err.Error()) 269 return 270 } 271 272 for _, resource := range resources { 273 if !cb(resource) { 274 return 275 } 276 } 277 278 path = nextPath 279 } 280 return 281 } 282 283 //URL ... 284 func (c *Client) URL(path string) string { 285 return *c.Config.Endpoint + cleanPath(path) 286 } 287 288 func cleanPath(p string) string { 289 if p == "" { 290 return "/" 291 } 292 if !strings.HasPrefix(p, "/") { 293 p = "/" + p 294 } 295 return path.Clean(p) 296 } 297 298 const ( 299 userAgentHeader = "User-Agent" 300 originalUserAgentHeader = "X-Original-User-Agent" 301 authorizationHeader = "Authorization" 302 uaaAccessTokenHeader = "X-Auth-Uaa-Token" 303 userAccessTokenHeader = "X-Auth-User-Token" 304 iamRefreshTokenHeader = "X-Auth-Refresh-Token" 305 crRefreshTokenHeader = "RefreshToken" 306 ) 307 308 func getDefaultAuthHeaders(serviceName bluemix.ServiceName, c *bluemix.Config) gohttp.Header { 309 h := gohttp.Header{} 310 h.Set(originalUserAgentHeader, c.UserAgent) 311 switch serviceName { 312 case bluemix.MccpService, bluemix.AccountService: 313 h.Set(userAgentHeader, http.UserAgent()) 314 h.Set(authorizationHeader, c.UAAAccessToken) 315 case bluemix.ContainerService: 316 h.Set(userAgentHeader, http.UserAgent()) 317 h.Set(authorizationHeader, c.IAMAccessToken) 318 h.Set(iamRefreshTokenHeader, c.IAMRefreshToken) 319 h.Set(uaaAccessTokenHeader, c.UAAAccessToken) 320 case bluemix.VpcContainerService: 321 h.Set(userAgentHeader, http.UserAgent()) 322 h.Set(authorizationHeader, c.IAMAccessToken) 323 h.Set(iamRefreshTokenHeader, c.IAMRefreshToken) 324 case bluemix.SchematicsService: 325 h.Set(userAgentHeader, http.UserAgent()) 326 h.Set(authorizationHeader, c.IAMAccessToken) 327 h.Set(iamRefreshTokenHeader, c.IAMRefreshToken) 328 case bluemix.ContainerRegistryService: 329 h.Set(userAgentHeader, http.UserAgent()) 330 h.Set(authorizationHeader, c.IAMAccessToken) 331 h.Set(crRefreshTokenHeader, c.IAMRefreshToken) 332 case bluemix.IAMPAPService, bluemix.AccountServicev1, bluemix.ResourceCatalogrService, bluemix.ResourceControllerService, bluemix.ResourceControllerServicev2, bluemix.ResourceManagementService, bluemix.ResourceManagementServicev2, bluemix.IAMService, bluemix.IAMUUMService, bluemix.IAMUUMServicev2, bluemix.IAMPAPServicev2, bluemix.CseService: 333 h.Set(authorizationHeader, c.IAMAccessToken) 334 h.Set(userAgentHeader, http.UserAgent()) 335 case bluemix.UserManagement: 336 h.Set(userAgentHeader, http.UserAgent()) 337 h.Set(authorizationHeader, c.IAMAccessToken) 338 case bluemix.CisService: 339 h.Set(userAgentHeader, http.UserAgent()) 340 h.Set(userAccessTokenHeader, c.IAMAccessToken) 341 case bluemix.GlobalSearchService, bluemix.GlobalTaggingService: 342 h.Set(userAgentHeader, http.UserAgent()) 343 h.Set(authorizationHeader, c.IAMAccessToken) 344 h.Set(iamRefreshTokenHeader, c.IAMRefreshToken) 345 case bluemix.ICDService: 346 h.Set(userAgentHeader, http.UserAgent()) 347 h.Set(authorizationHeader, c.IAMAccessToken) 348 case bluemix.CertificateManager: 349 h.Set(userAgentHeader, http.UserAgent()) 350 h.Set(authorizationHeader, c.IAMAccessToken) 351 case bluemix.HPCService: 352 h.Set(userAgentHeader, http.UserAgent()) 353 h.Set(authorizationHeader, c.IAMAccessToken) 354 case bluemix.FunctionsService: 355 h.Set(userAgentHeader, http.UserAgent()) 356 h.Set(authorizationHeader, c.IAMAccessToken) 357 358 default: 359 log.Println("Unknown service - No auth headers set") 360 } 361 return h 362 } 363 364 func isTimeout(err error) bool { 365 if bmErr, ok := err.(bmxerror.RequestFailure); ok { 366 switch bmErr.StatusCode() { 367 case 408, 504, 599, 429, 500, 502, 520, 503, 403: 368 return true 369 } 370 } 371 372 if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 373 return true 374 } 375 376 if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { 377 return true 378 } 379 380 if netErr, ok := err.(net.UnknownNetworkError); ok && netErr.Timeout() { 381 return true 382 } 383 384 return false 385 } 386 387 func isRetryable(err error) bool { 388 return isTimeout(err) 389 }