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  }