|
|
|
|
@ -48,6 +48,9 @@ var (
|
|
|
|
|
ErrResourceNotFound = errors.New("resource not found")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RetryLogHook allows a function to run before each retry.
|
|
|
|
|
type RetryLogHook func(attemptNum int, resp *http.Response)
|
|
|
|
|
|
|
|
|
|
// Config provides configuration details to the API client.
|
|
|
|
|
type Config struct {
|
|
|
|
|
// The address of the Terraform Enterprise API.
|
|
|
|
|
@ -64,6 +67,9 @@ type Config struct {
|
|
|
|
|
|
|
|
|
|
// A custom HTTP client to use.
|
|
|
|
|
HTTPClient *http.Client
|
|
|
|
|
|
|
|
|
|
// RetryLogHook is invoked each time a request is retried.
|
|
|
|
|
RetryLogHook RetryLogHook
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DefaultConfig returns a default config structure.
|
|
|
|
|
@ -90,11 +96,13 @@ func DefaultConfig() *Config {
|
|
|
|
|
// Client is the Terraform Enterprise API client. It provides the basic
|
|
|
|
|
// connectivity and configuration for accessing the TFE API.
|
|
|
|
|
type Client struct {
|
|
|
|
|
baseURL *url.URL
|
|
|
|
|
token string
|
|
|
|
|
headers http.Header
|
|
|
|
|
http *retryablehttp.Client
|
|
|
|
|
limiter *rate.Limiter
|
|
|
|
|
baseURL *url.URL
|
|
|
|
|
token string
|
|
|
|
|
headers http.Header
|
|
|
|
|
http *retryablehttp.Client
|
|
|
|
|
limiter *rate.Limiter
|
|
|
|
|
retryLogHook RetryLogHook
|
|
|
|
|
retryServerErrors bool
|
|
|
|
|
|
|
|
|
|
Applies Applies
|
|
|
|
|
ConfigurationVersions ConfigurationVersions
|
|
|
|
|
@ -139,6 +147,9 @@ func NewClient(cfg *Config) (*Client, error) {
|
|
|
|
|
if cfg.HTTPClient != nil {
|
|
|
|
|
config.HTTPClient = cfg.HTTPClient
|
|
|
|
|
}
|
|
|
|
|
if cfg.RetryLogHook != nil {
|
|
|
|
|
config.RetryLogHook = cfg.RetryLogHook
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the address to make sure its a valid URL.
|
|
|
|
|
@ -159,18 +170,20 @@ func NewClient(cfg *Config) (*Client, error) {
|
|
|
|
|
|
|
|
|
|
// Create the client.
|
|
|
|
|
client := &Client{
|
|
|
|
|
baseURL: baseURL,
|
|
|
|
|
token: config.Token,
|
|
|
|
|
headers: config.Headers,
|
|
|
|
|
http: &retryablehttp.Client{
|
|
|
|
|
Backoff: rateLimitBackoff,
|
|
|
|
|
CheckRetry: rateLimitRetry,
|
|
|
|
|
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
|
|
|
|
HTTPClient: config.HTTPClient,
|
|
|
|
|
RetryWaitMin: 100 * time.Millisecond,
|
|
|
|
|
RetryWaitMax: 400 * time.Millisecond,
|
|
|
|
|
RetryMax: 30,
|
|
|
|
|
},
|
|
|
|
|
baseURL: baseURL,
|
|
|
|
|
token: config.Token,
|
|
|
|
|
headers: config.Headers,
|
|
|
|
|
retryLogHook: config.RetryLogHook,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.http = &retryablehttp.Client{
|
|
|
|
|
Backoff: client.retryHTTPBackoff,
|
|
|
|
|
CheckRetry: client.retryHTTPCheck,
|
|
|
|
|
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
|
|
|
|
HTTPClient: config.HTTPClient,
|
|
|
|
|
RetryWaitMin: 100 * time.Millisecond,
|
|
|
|
|
RetryWaitMax: 400 * time.Millisecond,
|
|
|
|
|
RetryMax: 30,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure the rate limiter.
|
|
|
|
|
@ -203,24 +216,46 @@ func NewClient(cfg *Config) (*Client, error) {
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rateLimitRetry provides a callback for Client.CheckRetry, which will only
|
|
|
|
|
// retry when receiving a 429 response which indicates being rate limited.
|
|
|
|
|
func rateLimitRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
|
|
|
|
// Do not retry on context.Canceled or context.DeadlineExceeded.
|
|
|
|
|
// RetryServerErrors configures the retry HTTP check to also retry
|
|
|
|
|
// unexpected errors or requests that failed with a server error.
|
|
|
|
|
func (c *Client) RetryServerErrors(retry bool) {
|
|
|
|
|
c.retryServerErrors = retry
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retryHTTPCheck provides a callback for Client.CheckRetry which
|
|
|
|
|
// will retry both rate limit (429) and server (>= 500) errors.
|
|
|
|
|
func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
|
return false, ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
// Do not retry on any unexpected errors.
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
return c.retryServerErrors, err
|
|
|
|
|
}
|
|
|
|
|
// Only retry when we are rate limited.
|
|
|
|
|
if resp.StatusCode == 429 {
|
|
|
|
|
if resp.StatusCode == 429 || (c.retryServerErrors && resp.StatusCode >= 500) {
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retryHTTPBackoff provides a generic callback for Client.Backoff which
|
|
|
|
|
// will pass through all calls based on the status code of the response.
|
|
|
|
|
func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
|
|
|
|
if c.retryLogHook != nil {
|
|
|
|
|
c.retryLogHook(attemptNum, resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use the rate limit backoff function when we are rate limited.
|
|
|
|
|
if resp.StatusCode == 429 {
|
|
|
|
|
return rateLimitBackoff(min, max, attemptNum, resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set custom duration's when we experience a service interruption.
|
|
|
|
|
min = 700 * time.Millisecond
|
|
|
|
|
max = 900 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rateLimitBackoff provides a callback for Client.Backoff which will use the
|
|
|
|
|
// X-RateLimit_Reset header to determine the time to wait. We add some jitter
|
|
|
|
|
// to prevent a thundering herd.
|
|
|
|
|
|