client.go raw

   1  package linodego
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"fmt"
   8  	"io"
   9  	"log"
  10  	"net/http"
  11  	"net/url"
  12  	"os"
  13  	"path"
  14  	"path/filepath"
  15  	"reflect"
  16  	"regexp"
  17  	"strconv"
  18  	"strings"
  19  	"sync"
  20  	"text/template"
  21  	"time"
  22  
  23  	"github.com/go-resty/resty/v2"
  24  )
  25  
  26  const (
  27  	// APIConfigEnvVar environment var to get path to Linode config
  28  	APIConfigEnvVar = "LINODE_CONFIG"
  29  	// APIConfigProfileEnvVar specifies the profile to use when loading from a Linode config
  30  	APIConfigProfileEnvVar = "LINODE_PROFILE"
  31  	// APIHost Linode API hostname
  32  	APIHost = "api.linode.com"
  33  	// APIHostVar environment var to check for alternate API URL
  34  	APIHostVar = "LINODE_URL"
  35  	// APIHostCert environment var containing path to CA cert to validate against.
  36  	// Note that the custom CA cannot be configured together with a custom HTTP Transport.
  37  	APIHostCert = "LINODE_CA"
  38  	// APIVersion Linode API version
  39  	APIVersion = "v4"
  40  	// APIVersionVar environment var to check for alternate API Version
  41  	APIVersionVar = "LINODE_API_VERSION"
  42  	// APIProto connect to API with http(s)
  43  	APIProto = "https"
  44  	// APIEnvVar environment var to check for API token
  45  	APIEnvVar = "LINODE_TOKEN"
  46  	// APISecondsPerPoll how frequently to poll for new Events or Status in WaitFor functions
  47  	APISecondsPerPoll = 3
  48  	// APIRetryMaxWaitTime is the maximum wait time for retries
  49  	APIRetryMaxWaitTime       = time.Duration(30) * time.Second
  50  	APIDefaultCacheExpiration = time.Minute * 15
  51  )
  52  
  53  //nolint:unused
  54  var (
  55  	reqLogTemplate = template.Must(template.New("request").Parse(`Sending request:
  56  Method: {{.Method}}
  57  URL: {{.URL}}
  58  Headers: {{.Headers}}
  59  Body: {{.Body}}`))
  60  
  61  	respLogTemplate = template.Must(template.New("response").Parse(`Received response:
  62  Status: {{.Status}}
  63  Headers: {{.Headers}}
  64  Body: {{.Body}}`))
  65  )
  66  
  67  var envDebug = false
  68  
  69  // Client is a wrapper around the Resty client
  70  type Client struct {
  71  	resty             *resty.Client
  72  	userAgent         string
  73  	debug             bool
  74  	retryConditionals []RetryConditional
  75  
  76  	pollInterval time.Duration
  77  
  78  	baseURL         string
  79  	apiVersion      string
  80  	apiProto        string
  81  	selectedProfile string
  82  	loadedProfile   string
  83  
  84  	configProfiles map[string]ConfigProfile
  85  
  86  	// Fields for caching endpoint responses
  87  	shouldCache     bool
  88  	cacheExpiration time.Duration
  89  	cachedEntries   map[string]clientCacheEntry
  90  	cachedEntryLock *sync.RWMutex
  91  }
  92  
  93  type EnvDefaults struct {
  94  	Token   string
  95  	Profile string
  96  }
  97  
  98  type clientCacheEntry struct {
  99  	Created time.Time
 100  	Data    any
 101  	// If != nil, use this instead of the
 102  	// global expiry
 103  	ExpiryOverride *time.Duration
 104  }
 105  
 106  type (
 107  	Request  = resty.Request
 108  	Response = resty.Response
 109  	Logger   = resty.Logger
 110  )
 111  
 112  func init() {
 113  	// Whether we will enable Resty debugging output
 114  	if apiDebug, ok := os.LookupEnv("LINODE_DEBUG"); ok {
 115  		if parsed, err := strconv.ParseBool(apiDebug); err == nil {
 116  			envDebug = parsed
 117  			log.Println("[INFO] LINODE_DEBUG being set to", envDebug)
 118  		} else {
 119  			log.Println("[WARN] LINODE_DEBUG should be an integer, 0 or 1")
 120  		}
 121  	}
 122  }
 123  
 124  // NewClient factory to create new Client struct
 125  func NewClient(hc *http.Client) (client Client) {
 126  	if hc != nil {
 127  		client.resty = resty.NewWithClient(hc)
 128  	} else {
 129  		client.resty = resty.New()
 130  	}
 131  
 132  	client.shouldCache = true
 133  	client.cacheExpiration = APIDefaultCacheExpiration
 134  	client.cachedEntries = make(map[string]clientCacheEntry)
 135  	client.cachedEntryLock = &sync.RWMutex{}
 136  
 137  	client.SetUserAgent(DefaultUserAgent)
 138  
 139  	baseURL, baseURLExists := os.LookupEnv(APIHostVar)
 140  
 141  	if baseURLExists {
 142  		client.SetBaseURL(baseURL)
 143  	}
 144  
 145  	apiVersion, apiVersionExists := os.LookupEnv(APIVersionVar)
 146  	if apiVersionExists {
 147  		client.SetAPIVersion(apiVersion)
 148  	} else {
 149  		client.SetAPIVersion(APIVersion)
 150  	}
 151  
 152  	certPath, certPathExists := os.LookupEnv(APIHostCert)
 153  
 154  	if certPathExists && !hasCustomTransport(hc) {
 155  		cert, err := os.ReadFile(filepath.Clean(certPath))
 156  		if err != nil {
 157  			log.Fatalf("[ERROR] Error when reading cert at %s: %s\n", certPath, err.Error())
 158  		}
 159  
 160  		client.SetRootCertificate(certPath)
 161  
 162  		if envDebug {
 163  			log.Printf("[DEBUG] Set API root certificate to %s with contents %s\n", certPath, cert)
 164  		}
 165  	}
 166  
 167  	client.
 168  		SetRetryWaitTime(APISecondsPerPoll * time.Second).
 169  		SetPollDelay(APISecondsPerPoll * time.Second).
 170  		SetRetries().
 171  		SetDebug(envDebug).
 172  		enableLogSanitization()
 173  
 174  	return client
 175  }
 176  
 177  // NewClientFromEnv creates a Client and initializes it with values
 178  // from the LINODE_CONFIG file and the LINODE_TOKEN environment variable.
 179  func NewClientFromEnv(hc *http.Client) (*Client, error) {
 180  	client := NewClient(hc)
 181  
 182  	// Users are expected to chain NewClient(...) and LoadConfig(...) to customize these options
 183  	configPath, err := resolveValidConfigPath()
 184  	if err != nil {
 185  		return nil, err
 186  	}
 187  
 188  	// Populate the token from the environment.
 189  	// Tokens should be first priority to maintain backwards compatibility
 190  	if token, ok := os.LookupEnv(APIEnvVar); ok && token != "" {
 191  		client.SetToken(token)
 192  		return &client, nil
 193  	}
 194  
 195  	if p, ok := os.LookupEnv(APIConfigEnvVar); ok {
 196  		configPath = p
 197  	} else if !ok && configPath == "" {
 198  		return nil, fmt.Errorf("no linode config file or token found")
 199  	}
 200  
 201  	configProfile := DefaultConfigProfile
 202  
 203  	if p, ok := os.LookupEnv(APIConfigProfileEnvVar); ok {
 204  		configProfile = p
 205  	}
 206  
 207  	client.selectedProfile = configProfile
 208  
 209  	// We should only load the config if the config file exists
 210  	if _, err = os.Stat(configPath); err != nil {
 211  		return nil, fmt.Errorf("error loading config file %s: %w", configPath, err)
 212  	}
 213  
 214  	err = client.preLoadConfig(configPath)
 215  
 216  	return &client, err
 217  }
 218  
 219  // SetUserAgent sets a custom user-agent for HTTP requests
 220  func (c *Client) SetUserAgent(ua string) *Client {
 221  	c.userAgent = ua
 222  	c.resty.SetHeader("User-Agent", c.userAgent)
 223  
 224  	return c
 225  }
 226  
 227  type RequestParams struct {
 228  	Body     any
 229  	Response any
 230  }
 231  
 232  // Generic helper to execute HTTP requests using the net/http package
 233  //
 234  // nolint:unused, funlen, gocognit
 235  func (c *httpClient) doRequest(ctx context.Context, method, url string, params RequestParams) error {
 236  	var (
 237  		req        *http.Request
 238  		bodyBuffer *bytes.Buffer
 239  		resp       *http.Response
 240  		err        error
 241  	)
 242  
 243  	for range httpDefaultRetryCount {
 244  		req, bodyBuffer, err = c.createRequest(ctx, method, url, params)
 245  		if err != nil {
 246  			return err
 247  		}
 248  
 249  		if err = c.applyBeforeRequest(req); err != nil {
 250  			return err
 251  		}
 252  
 253  		if c.debug && c.logger != nil {
 254  			c.logRequest(req, method, url, bodyBuffer)
 255  		}
 256  
 257  		processResponse := func() error {
 258  			defer func() {
 259  				closeErr := resp.Body.Close()
 260  				if closeErr != nil && err == nil {
 261  					err = closeErr
 262  				}
 263  			}()
 264  
 265  			if err = c.checkHTTPError(resp); err != nil {
 266  				return err
 267  			}
 268  
 269  			if c.debug && c.logger != nil {
 270  				var logErr error
 271  
 272  				resp, logErr = c.logResponse(resp)
 273  				if logErr != nil {
 274  					return logErr
 275  				}
 276  			}
 277  
 278  			if params.Response != nil {
 279  				if err = c.decodeResponseBody(resp, params.Response); err != nil {
 280  					return err
 281  				}
 282  			}
 283  
 284  			// Apply after-response mutations
 285  			if err = c.applyAfterResponse(resp); err != nil {
 286  				return err
 287  			}
 288  
 289  			return nil
 290  		}
 291  
 292  		resp, err = c.sendRequest(req)
 293  		if err == nil {
 294  			if err = processResponse(); err == nil {
 295  				return nil
 296  			}
 297  		}
 298  
 299  		if !c.shouldRetry(resp, err) {
 300  			break
 301  		}
 302  
 303  		retryAfter, retryErr := c.retryAfter(resp)
 304  		if retryErr != nil {
 305  			return retryErr
 306  		}
 307  
 308  		// Sleep for the specified duration before retrying.
 309  		// If retryAfter is 0 (i.e., Retry-After header is not found),
 310  		// no delay is applied.
 311  		time.Sleep(retryAfter)
 312  	}
 313  
 314  	return err
 315  }
 316  
 317  // nolint:unused
 318  func (c *httpClient) shouldRetry(resp *http.Response, err error) bool {
 319  	for _, retryConditional := range c.retryConditionals {
 320  		if retryConditional(resp, err) {
 321  			return true
 322  		}
 323  	}
 324  
 325  	return false
 326  }
 327  
 328  // nolint:unused
 329  func (c *httpClient) createRequest(ctx context.Context, method, url string, params RequestParams) (*http.Request, *bytes.Buffer, error) {
 330  	var (
 331  		bodyReader io.Reader
 332  		bodyBuffer *bytes.Buffer
 333  	)
 334  
 335  	if params.Body != nil {
 336  		bodyBuffer = new(bytes.Buffer)
 337  		if err := json.NewEncoder(bodyBuffer).Encode(params.Body); err != nil {
 338  			if c.debug && c.logger != nil {
 339  				c.logger.Errorf("failed to encode body: %v", err)
 340  			}
 341  
 342  			return nil, nil, fmt.Errorf("failed to encode body: %w", err)
 343  		}
 344  
 345  		bodyReader = bodyBuffer
 346  	}
 347  
 348  	req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
 349  	if err != nil {
 350  		if c.debug && c.logger != nil {
 351  			c.logger.Errorf("failed to create request: %v", err)
 352  		}
 353  
 354  		return nil, nil, fmt.Errorf("failed to create request: %w", err)
 355  	}
 356  
 357  	req.Header.Set("Content-Type", "application/json")
 358  	req.Header.Set("Accept", "application/json")
 359  
 360  	if c.userAgent != "" {
 361  		req.Header.Set("User-Agent", c.userAgent)
 362  	}
 363  
 364  	return req, bodyBuffer, nil
 365  }
 366  
 367  // nolint:unused
 368  func (c *httpClient) applyBeforeRequest(req *http.Request) error {
 369  	for _, mutate := range c.onBeforeRequest {
 370  		if err := mutate(req); err != nil {
 371  			if c.debug && c.logger != nil {
 372  				c.logger.Errorf("failed to mutate before request: %v", err)
 373  			}
 374  
 375  			return fmt.Errorf("failed to mutate before request: %w", err)
 376  		}
 377  	}
 378  
 379  	return nil
 380  }
 381  
 382  // nolint:unused
 383  func (c *httpClient) applyAfterResponse(resp *http.Response) error {
 384  	for _, mutate := range c.onAfterResponse {
 385  		if err := mutate(resp); err != nil {
 386  			if c.debug && c.logger != nil {
 387  				c.logger.Errorf("failed to mutate after response: %v", err)
 388  			}
 389  
 390  			return fmt.Errorf("failed to mutate after response: %w", err)
 391  		}
 392  	}
 393  
 394  	return nil
 395  }
 396  
 397  // nolint:unused
 398  func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffer *bytes.Buffer) {
 399  	var reqBody string
 400  	if bodyBuffer != nil {
 401  		reqBody = bodyBuffer.String()
 402  	} else {
 403  		reqBody = "nil"
 404  	}
 405  
 406  	var logBuf bytes.Buffer
 407  
 408  	err := reqLogTemplate.Execute(&logBuf, map[string]any{
 409  		"Method":  method,
 410  		"URL":     url,
 411  		"Headers": req.Header,
 412  		"Body":    reqBody,
 413  	})
 414  	if err == nil {
 415  		c.logger.Debugf(logBuf.String())
 416  	}
 417  }
 418  
 419  // nolint:unused
 420  func (c *httpClient) sendRequest(req *http.Request) (*http.Response, error) {
 421  	resp, err := c.httpClient.Do(req)
 422  	if err != nil {
 423  		if c.debug && c.logger != nil {
 424  			c.logger.Errorf("failed to send request: %v", err)
 425  		}
 426  
 427  		return nil, fmt.Errorf("failed to send request: %w", err)
 428  	}
 429  
 430  	return resp, nil
 431  }
 432  
 433  // nolint:unused
 434  func (c *httpClient) checkHTTPError(resp *http.Response) error {
 435  	_, err := coupleAPIErrorsHTTP(resp, nil)
 436  	if err != nil {
 437  		if c.debug && c.logger != nil {
 438  			c.logger.Errorf("received HTTP error: %v", err)
 439  		}
 440  
 441  		return err
 442  	}
 443  
 444  	return nil
 445  }
 446  
 447  // nolint:unused
 448  func (c *httpClient) logResponse(resp *http.Response) (*http.Response, error) {
 449  	var respBody bytes.Buffer
 450  	if _, err := io.Copy(&respBody, resp.Body); err != nil {
 451  		c.logger.Errorf("failed to read response body: %v", err)
 452  	}
 453  
 454  	var logBuf bytes.Buffer
 455  
 456  	err := respLogTemplate.Execute(&logBuf, map[string]any{
 457  		"Status":  resp.Status,
 458  		"Headers": resp.Header,
 459  		"Body":    respBody.String(),
 460  	})
 461  	if err == nil {
 462  		c.logger.Debugf(logBuf.String())
 463  	}
 464  
 465  	resp.Body = io.NopCloser(bytes.NewReader(respBody.Bytes()))
 466  
 467  	return resp, nil
 468  }
 469  
 470  // nolint:unused
 471  func (c *httpClient) decodeResponseBody(resp *http.Response, response any) error {
 472  	if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
 473  		if c.debug && c.logger != nil {
 474  			c.logger.Errorf("failed to decode response: %v", err)
 475  		}
 476  
 477  		return fmt.Errorf("failed to decode response: %w", err)
 478  	}
 479  
 480  	return nil
 481  }
 482  
 483  // R wraps resty's R method
 484  func (c *Client) R(ctx context.Context) *resty.Request {
 485  	return c.resty.R().
 486  		ExpectContentType("application/json").
 487  		SetHeader("Content-Type", "application/json").
 488  		SetContext(ctx).
 489  		SetError(APIError{})
 490  }
 491  
 492  // SetDebug sets the debug on resty's client
 493  func (c *Client) SetDebug(debug bool) *Client {
 494  	c.debug = debug
 495  	c.resty.SetDebug(debug)
 496  
 497  	return c
 498  }
 499  
 500  // SetLogger allows the user to override the output
 501  // logger for debug logs.
 502  func (c *Client) SetLogger(logger Logger) *Client {
 503  	c.resty.SetLogger(logger)
 504  
 505  	return c
 506  }
 507  
 508  //nolint:unused
 509  func (c *httpClient) httpSetDebug(debug bool) *httpClient {
 510  	c.debug = debug
 511  
 512  	return c
 513  }
 514  
 515  //nolint:unused
 516  func (c *httpClient) httpSetLogger(logger httpLogger) *httpClient {
 517  	c.logger = logger
 518  
 519  	return c
 520  }
 521  
 522  // OnBeforeRequest adds a handler to the request body to run before the request is sent
 523  func (c *Client) OnBeforeRequest(m func(request *Request) error) {
 524  	c.resty.OnBeforeRequest(func(_ *resty.Client, req *resty.Request) error {
 525  		return m(req)
 526  	})
 527  }
 528  
 529  // OnAfterResponse adds a handler to the request body to run before the request is sent
 530  func (c *Client) OnAfterResponse(m func(response *Response) error) {
 531  	c.resty.OnAfterResponse(func(_ *resty.Client, req *resty.Response) error {
 532  		return m(req)
 533  	})
 534  }
 535  
 536  // nolint:unused
 537  func (c *httpClient) httpOnBeforeRequest(m func(*http.Request) error) *httpClient {
 538  	c.onBeforeRequest = append(c.onBeforeRequest, m)
 539  
 540  	return c
 541  }
 542  
 543  // nolint:unused
 544  func (c *httpClient) httpOnAfterResponse(m func(*http.Response) error) *httpClient {
 545  	c.onAfterResponse = append(c.onAfterResponse, m)
 546  
 547  	return c
 548  }
 549  
 550  // UseURL parses the individual components of the given API URL and configures the client
 551  // accordingly. For example, a valid URL.
 552  // For example:
 553  //
 554  //	client.UseURL("https://api.test.linode.com/v4beta")
 555  func (c *Client) UseURL(apiURL string) (*Client, error) {
 556  	parsedURL, err := url.Parse(apiURL)
 557  	if err != nil {
 558  		return nil, fmt.Errorf("failed to parse URL: %w", err)
 559  	}
 560  
 561  	if parsedURL.Scheme == "" || parsedURL.Host == "" {
 562  		return nil, fmt.Errorf("need both scheme and host in API URL, got %q", apiURL)
 563  	}
 564  
 565  	// Create a new URL excluding the path to use as the base URL
 566  	baseURL := &url.URL{
 567  		Host:   parsedURL.Host,
 568  		Scheme: parsedURL.Scheme,
 569  	}
 570  
 571  	c.SetBaseURL(baseURL.String())
 572  
 573  	versionMatches := regexp.MustCompile(`/v[a-zA-Z0-9]+`).FindAllString(parsedURL.Path, -1)
 574  
 575  	// Only set the version if a version is found in the URL, else use the default
 576  	if len(versionMatches) > 0 {
 577  		c.SetAPIVersion(
 578  			strings.Trim(versionMatches[len(versionMatches)-1], "/"),
 579  		)
 580  	}
 581  
 582  	return c, nil
 583  }
 584  
 585  // SetBaseURL sets the base URL of the Linode v4 API (https://api.linode.com/v4)
 586  func (c *Client) SetBaseURL(baseURL string) *Client {
 587  	baseURLPath, _ := url.Parse(baseURL)
 588  
 589  	c.baseURL = path.Join(baseURLPath.Host, baseURLPath.Path)
 590  	c.apiProto = baseURLPath.Scheme
 591  
 592  	c.updateHostURL()
 593  
 594  	return c
 595  }
 596  
 597  // SetAPIVersion sets the version of the API to interface with
 598  func (c *Client) SetAPIVersion(apiVersion string) *Client {
 599  	c.apiVersion = apiVersion
 600  
 601  	c.updateHostURL()
 602  
 603  	return c
 604  }
 605  
 606  // SetRootCertificate adds a root certificate to the underlying TLS client config
 607  func (c *Client) SetRootCertificate(path string) *Client {
 608  	c.resty.SetRootCertificate(path)
 609  	return c
 610  }
 611  
 612  // SetToken sets the API token for all requests from this client
 613  // Only necessary if you haven't already provided the http client to NewClient() configured with the token.
 614  func (c *Client) SetToken(token string) *Client {
 615  	c.resty.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token))
 616  	return c
 617  }
 618  
 619  // SetRetries adds retry conditions for "Linode Busy." errors and 429s.
 620  func (c *Client) SetRetries() *Client {
 621  	c.
 622  		addRetryConditional(linodeBusyRetryCondition).
 623  		addRetryConditional(tooManyRequestsRetryCondition).
 624  		addRetryConditional(serviceUnavailableRetryCondition).
 625  		addRetryConditional(requestTimeoutRetryCondition).
 626  		addRetryConditional(requestGOAWAYRetryCondition).
 627  		addRetryConditional(requestNGINXRetryCondition).
 628  		SetRetryMaxWaitTime(APIRetryMaxWaitTime)
 629  	configureRetries(c)
 630  
 631  	return c
 632  }
 633  
 634  // AddRetryCondition adds a RetryConditional function to the Client
 635  func (c *Client) AddRetryCondition(retryCondition RetryConditional) *Client {
 636  	c.resty.AddRetryCondition(resty.RetryConditionFunc(retryCondition))
 637  	return c
 638  }
 639  
 640  // InvalidateCache clears all cached responses for all endpoints.
 641  func (c *Client) InvalidateCache() {
 642  	c.cachedEntryLock.Lock()
 643  	defer c.cachedEntryLock.Unlock()
 644  
 645  	// GC will handle the old map
 646  	c.cachedEntries = make(map[string]clientCacheEntry)
 647  }
 648  
 649  // InvalidateCacheEndpoint invalidates a single cached endpoint.
 650  func (c *Client) InvalidateCacheEndpoint(endpoint string) error {
 651  	u, err := url.Parse(endpoint)
 652  	if err != nil {
 653  		return fmt.Errorf("failed to parse URL for caching: %w", err)
 654  	}
 655  
 656  	c.cachedEntryLock.Lock()
 657  	defer c.cachedEntryLock.Unlock()
 658  
 659  	delete(c.cachedEntries, u.Path)
 660  
 661  	return nil
 662  }
 663  
 664  // SetGlobalCacheExpiration sets the desired time for any cached response
 665  // to be valid for.
 666  func (c *Client) SetGlobalCacheExpiration(expiryTime time.Duration) {
 667  	c.cacheExpiration = expiryTime
 668  }
 669  
 670  // UseCache sets whether response caching should be used
 671  func (c *Client) UseCache(value bool) {
 672  	c.shouldCache = value
 673  }
 674  
 675  // SetRetryMaxWaitTime sets the maximum delay before retrying a request.
 676  func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {
 677  	c.resty.SetRetryMaxWaitTime(maxWaitTime)
 678  	return c
 679  }
 680  
 681  // SetRetryWaitTime sets the default (minimum) delay before retrying a request.
 682  func (c *Client) SetRetryWaitTime(minWaitTime time.Duration) *Client {
 683  	c.resty.SetRetryWaitTime(minWaitTime)
 684  	return c
 685  }
 686  
 687  // SetRetryAfter sets the callback function to be invoked with a failed request
 688  // to determine wben it should be retried.
 689  func (c *Client) SetRetryAfter(callback RetryAfter) *Client {
 690  	c.resty.SetRetryAfter(resty.RetryAfterFunc(callback))
 691  	return c
 692  }
 693  
 694  // SetRetryCount sets the maximum retry attempts before aborting.
 695  func (c *Client) SetRetryCount(count int) *Client {
 696  	c.resty.SetRetryCount(count)
 697  	return c
 698  }
 699  
 700  // SetPollDelay sets the number of milliseconds to wait between events or status polls.
 701  // Affects all WaitFor* functions and retries.
 702  func (c *Client) SetPollDelay(delay time.Duration) *Client {
 703  	c.pollInterval = delay
 704  	return c
 705  }
 706  
 707  // GetPollDelay gets the number of milliseconds to wait between events or status polls.
 708  // Affects all WaitFor* functions and retries.
 709  func (c *Client) GetPollDelay() time.Duration {
 710  	return c.pollInterval
 711  }
 712  
 713  // SetHeader sets a custom header to be used in all API requests made with the current
 714  // client.
 715  // NOTE: Some headers may be overridden by the individual request functions.
 716  func (c *Client) SetHeader(name, value string) {
 717  	c.resty.SetHeader(name, value)
 718  }
 719  
 720  func (c *Client) addRetryConditional(retryConditional RetryConditional) *Client {
 721  	c.retryConditionals = append(c.retryConditionals, retryConditional)
 722  	return c
 723  }
 724  
 725  func (c *Client) addCachedResponse(endpoint string, response any, expiry *time.Duration) {
 726  	if !c.shouldCache {
 727  		return
 728  	}
 729  
 730  	responseValue := reflect.ValueOf(response)
 731  
 732  	entry := clientCacheEntry{
 733  		Created:        time.Now(),
 734  		ExpiryOverride: expiry,
 735  	}
 736  
 737  	switch responseValue.Kind() {
 738  	case reflect.Ptr:
 739  		// We want to automatically deref pointers to
 740  		// avoid caching mutable data.
 741  		entry.Data = responseValue.Elem().Interface()
 742  	default:
 743  		entry.Data = response
 744  	}
 745  
 746  	c.cachedEntryLock.Lock()
 747  	defer c.cachedEntryLock.Unlock()
 748  
 749  	c.cachedEntries[endpoint] = entry
 750  }
 751  
 752  func (c *Client) getCachedResponse(endpoint string) any {
 753  	if !c.shouldCache {
 754  		return nil
 755  	}
 756  
 757  	c.cachedEntryLock.RLock()
 758  
 759  	// Hacky logic to dynamically RUnlock
 760  	// only if it is still locked by the
 761  	// end of the function.
 762  	// This is necessary as we take write
 763  	// access if the entry has expired.
 764  	rLocked := true
 765  
 766  	defer func() {
 767  		if rLocked {
 768  			c.cachedEntryLock.RUnlock()
 769  		}
 770  	}()
 771  
 772  	entry, ok := c.cachedEntries[endpoint]
 773  	if !ok {
 774  		return nil
 775  	}
 776  
 777  	// Handle expired entries
 778  	elapsedTime := time.Since(entry.Created)
 779  
 780  	hasExpired := elapsedTime > c.cacheExpiration
 781  	if entry.ExpiryOverride != nil {
 782  		hasExpired = elapsedTime > *entry.ExpiryOverride
 783  	}
 784  
 785  	if hasExpired {
 786  		// We need to give up our read access and request read-write access
 787  		c.cachedEntryLock.RUnlock()
 788  
 789  		rLocked = false
 790  
 791  		c.cachedEntryLock.Lock()
 792  		defer c.cachedEntryLock.Unlock()
 793  
 794  		delete(c.cachedEntries, endpoint)
 795  
 796  		return nil
 797  	}
 798  
 799  	return c.cachedEntries[endpoint].Data
 800  }
 801  
 802  func (c *Client) updateHostURL() {
 803  	apiProto := APIProto
 804  	baseURL := APIHost
 805  	apiVersion := APIVersion
 806  
 807  	if c.baseURL != "" {
 808  		baseURL = c.baseURL
 809  	}
 810  
 811  	if c.apiVersion != "" {
 812  		apiVersion = c.apiVersion
 813  	}
 814  
 815  	if c.apiProto != "" {
 816  		apiProto = c.apiProto
 817  	}
 818  
 819  	c.resty.SetBaseURL(
 820  		fmt.Sprintf(
 821  			"%s://%s/%s",
 822  			apiProto,
 823  			baseURL,
 824  			url.PathEscape(apiVersion),
 825  		),
 826  	)
 827  }
 828  
 829  func (c *Client) enableLogSanitization() *Client {
 830  	c.resty.OnRequestLog(func(r *resty.RequestLog) error {
 831  		// masking authorization header
 832  		r.Header.Set("Authorization", "Bearer *******************************")
 833  		return nil
 834  	})
 835  
 836  	return c
 837  }
 838  
 839  func (c *Client) preLoadConfig(configPath string) error {
 840  	if envDebug {
 841  		log.Printf("[INFO] Loading profile from %s\n", configPath)
 842  	}
 843  
 844  	if err := c.LoadConfig(&LoadConfigOptions{
 845  		Path:            configPath,
 846  		SkipLoadProfile: true,
 847  	}); err != nil {
 848  		return err
 849  	}
 850  
 851  	// We don't want to load the profile until the user is actually making requests
 852  	c.OnBeforeRequest(func(_ *Request) error {
 853  		if c.loadedProfile != c.selectedProfile {
 854  			if err := c.UseProfile(c.selectedProfile); err != nil {
 855  				return err
 856  			}
 857  		}
 858  
 859  		return nil
 860  	})
 861  
 862  	return nil
 863  }
 864  
 865  func copyBool(bPtr *bool) *bool {
 866  	if bPtr == nil {
 867  		return nil
 868  	}
 869  
 870  	t := *bPtr
 871  
 872  	return &t
 873  }
 874  
 875  func copyInt(iPtr *int) *int {
 876  	if iPtr == nil {
 877  		return nil
 878  	}
 879  
 880  	t := *iPtr
 881  
 882  	return &t
 883  }
 884  
 885  func copyString(sPtr *string) *string {
 886  	if sPtr == nil {
 887  		return nil
 888  	}
 889  
 890  	t := *sPtr
 891  
 892  	return &t
 893  }
 894  
 895  // copyValue returns a pointer to a new value copied from the value
 896  // at the given pointer.
 897  func copyValue[T any](ptr *T) *T {
 898  	if ptr == nil {
 899  		return nil
 900  	}
 901  
 902  	t := *ptr
 903  
 904  	return &t
 905  }
 906  
 907  func copyTime(tPtr *time.Time) *time.Time {
 908  	if tPtr == nil {
 909  		return nil
 910  	}
 911  
 912  	t := *tPtr
 913  
 914  	return &t
 915  }
 916  
 917  func generateListCacheURL(endpoint string, opts *ListOptions) (string, error) {
 918  	if opts == nil {
 919  		return endpoint, nil
 920  	}
 921  
 922  	hashedOpts, err := opts.Hash()
 923  	if err != nil {
 924  		return endpoint, err
 925  	}
 926  
 927  	return fmt.Sprintf("%s:%s", endpoint, hashedOpts), nil
 928  }
 929  
 930  func hasCustomTransport(hc *http.Client) bool {
 931  	if hc == nil || hc.Transport == nil {
 932  		return false
 933  	}
 934  
 935  	if _, ok := hc.Transport.(*http.Transport); !ok {
 936  		log.Println("[WARN] Custom transport is not allowed with a custom root CA.")
 937  		return true
 938  	}
 939  
 940  	return false
 941  }
 942