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