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  }