Skip to content

Commit

Permalink
Client.RemoteAPIVersion: get the remote API version number
Browse files Browse the repository at this point in the history
Newer versions of Terraform Cloud and Enterprise return the API version in
an HTTP header on all API responses.

This client implementation already always makes a pre-flight request to
a special "ping" endpoint as part of its initialization anyway, previously
to configure the rate limiting settings. To avoid additional requests and
keep this relatively simple, we'll now also use that request to populate
the remote API version number and thus make it available immediately to
callers via the RemoteAPIVersion method.
  • Loading branch information
apparentlymart committed May 15, 2020
1 parent 5bbc31f commit 2faaed6
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 13 deletions.
75 changes: 62 additions & 13 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import (
)

const (
userAgent = "go-tfe"
headerRateLimit = "X-RateLimit-Limit"
headerRateReset = "X-RateLimit-Reset"
userAgent = "go-tfe"
headerRateLimit = "X-RateLimit-Limit"
headerRateReset = "X-RateLimit-Reset"
headerAPIVersion = "TFP-API-Version"

// DefaultAddress of Terraform Enterprise.
DefaultAddress = "https://app.terraform.io"
Expand Down Expand Up @@ -105,6 +106,7 @@ type Client struct {
limiter *rate.Limiter
retryLogHook RetryLogHook
retryServerErrors bool
remoteAPIVersion string

Applies Applies
ConfigurationVersions ConfigurationVersions
Expand Down Expand Up @@ -194,11 +196,18 @@ func NewClient(cfg *Config) (*Client, error) {
RetryMax: 30,
}

// Configure the rate limiter.
if err := client.configureLimiter(); err != nil {
meta, err := client.getRawAPIMetadata()
if err != nil {
return nil, err
}

// Configure the rate limiter.
client.configureLimiter(meta.RateLimit)

// Save the API version so we can return it from the RemoteAPIVersion
// method later.
client.remoteAPIVersion = meta.APIVersion

// Create the services.
client.Applies = &applies{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
Expand Down Expand Up @@ -230,6 +239,26 @@ func NewClient(cfg *Config) (*Client, error) {
return client, nil
}

// RemoteAPIVersion returns the server's declared API version string.
//
// A Terraform Cloud or Enterprise API server returns its API version in an
// HTTP header field in all responses. The NewClient function saves the
// version number returned in its initial setup request and RemoteAPIVersion
// returns that cached value.
//
// The API protocol calls for this string to be a dotted-decimal version number
// like 2.3.0, where the first number indicates the API major version while the
// second indicates a minor version which may have introduced some
// backward-compatible additional features compared to its predecessor.
//
// Explicit API versioning was added to the Terraform Cloud and Enterprise
// APIs as a later addition, so older servers will not return version
// information. In that case, this function returns an empty string as the
// version.
func (c *Client) RemoteAPIVersion() string {
return c.remoteAPIVersion
}

// 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) {
Expand Down Expand Up @@ -298,16 +327,29 @@ func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Respons
return min + jitter
}

// configureLimiter configures the rate limiter.
func (c *Client) configureLimiter() error {
type rawAPIMetadata struct {
// APIVersion is the raw API version string reported by the server in the
// TFP-API-Version response header, or an empty string if that header
// field was not included in the response.
APIVersion string

// RateLimit is the raw API version string reported by the server in the
// X-RateLimit-Limit response header, or an empty string if that header
// field was not included in the response.
RateLimit string
}

func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) {
var meta rawAPIMetadata

// Create a new request.
u, err := c.baseURL.Parse(PingEndpoint)
if err != nil {
return err
return meta, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
return meta, err
}

// Attach the default headers.
Expand All @@ -320,15 +362,24 @@ func (c *Client) configureLimiter() error {
// Make a single request to retrieve the rate limit headers.
resp, err := c.http.HTTPClient.Do(req)
if err != nil {
return err
return meta, err
}
resp.Body.Close()

meta.APIVersion = resp.Header.Get(headerAPIVersion)
meta.RateLimit = resp.Header.Get(headerRateLimit)

return meta, nil
}

// configureLimiter configures the rate limiter.
func (c *Client) configureLimiter(rawLimit string) {

// Set default values for when rate limiting is disabled.
limit := rate.Inf
burst := 0

if v := resp.Header.Get(headerRateLimit); v != "" {
if v := rawLimit; v != "" {
if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
// Configure the limit and burst using a split of 2/3 for the limit and
// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
Expand All @@ -342,8 +393,6 @@ func (c *Client) configureLimiter() error {

// Create a new limiter using the calculated values.
c.limiter = rate.NewLimiter(limit, burst)

return nil
}

// newRequest creates an API request. A relative URL path can be provided in
Expand Down
4 changes: 4 additions & 0 deletions tfe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func TestClient_newClient(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.api+json")
w.Header().Set("X-RateLimit-Limit", "30")
w.Header().Set("TFP-API-Version", "34.21.9")
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
}))
defer ts.Close()
Expand Down Expand Up @@ -69,6 +70,9 @@ func TestClient_newClient(t *testing.T) {
if ts.Client() != client.http.HTTPClient {
t.Fatal("unexpected HTTP client value")
}
if want := "34.21.9"; client.RemoteAPIVersion() != want {
t.Errorf("unexpected remote API version %q; want %q", client.RemoteAPIVersion(), want)
}
})
}

Expand Down

0 comments on commit 2faaed6

Please sign in to comment.