1package tfe
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"io"
10	"math/rand"
11	"net/http"
12	"net/url"
13	"os"
14	"reflect"
15	"strconv"
16	"strings"
17	"time"
18
19	"github.com/google/go-querystring/query"
20	"github.com/hashicorp/go-cleanhttp"
21	retryablehttp "github.com/hashicorp/go-retryablehttp"
22	"github.com/svanharmelen/jsonapi"
23	"golang.org/x/time/rate"
24)
25
26const (
27	userAgent        = "go-tfe"
28	headerRateLimit  = "X-RateLimit-Limit"
29	headerRateReset  = "X-RateLimit-Reset"
30	headerAPIVersion = "TFP-API-Version"
31
32	// DefaultAddress of Terraform Enterprise.
33	DefaultAddress = "https://app.terraform.io"
34	// DefaultBasePath on which the API is served.
35	DefaultBasePath = "/api/v2/"
36	// PingEndpoint is a no-op API endpoint used to configure the rate limiter
37	PingEndpoint = "ping"
38)
39
40var (
41	// ErrWorkspaceLocked is returned when trying to lock a
42	// locked workspace.
43	ErrWorkspaceLocked = errors.New("workspace already locked")
44	// ErrWorkspaceNotLocked is returned when trying to unlock
45	// a unlocked workspace.
46	ErrWorkspaceNotLocked = errors.New("workspace already unlocked")
47
48	// ErrUnauthorized is returned when a receiving a 401.
49	ErrUnauthorized = errors.New("unauthorized")
50	// ErrResourceNotFound is returned when a receiving a 404.
51	ErrResourceNotFound = errors.New("resource not found")
52)
53
54// RetryLogHook allows a function to run before each retry.
55type RetryLogHook func(attemptNum int, resp *http.Response)
56
57// Config provides configuration details to the API client.
58type Config struct {
59	// The address of the Terraform Enterprise API.
60	Address string
61
62	// The base path on which the API is served.
63	BasePath string
64
65	// API token used to access the Terraform Enterprise API.
66	Token string
67
68	// Headers that will be added to every request.
69	Headers http.Header
70
71	// A custom HTTP client to use.
72	HTTPClient *http.Client
73
74	// RetryLogHook is invoked each time a request is retried.
75	RetryLogHook RetryLogHook
76}
77
78// DefaultConfig returns a default config structure.
79func DefaultConfig() *Config {
80	config := &Config{
81		Address:    os.Getenv("TFE_ADDRESS"),
82		BasePath:   DefaultBasePath,
83		Token:      os.Getenv("TFE_TOKEN"),
84		Headers:    make(http.Header),
85		HTTPClient: cleanhttp.DefaultPooledClient(),
86	}
87
88	// Set the default address if none is given.
89	if config.Address == "" {
90		config.Address = DefaultAddress
91	}
92
93	// Set the default user agent.
94	config.Headers.Set("User-Agent", userAgent)
95
96	return config
97}
98
99// Client is the Terraform Enterprise API client. It provides the basic
100// connectivity and configuration for accessing the TFE API.
101type Client struct {
102	baseURL           *url.URL
103	token             string
104	headers           http.Header
105	http              *retryablehttp.Client
106	limiter           *rate.Limiter
107	retryLogHook      RetryLogHook
108	retryServerErrors bool
109	remoteAPIVersion  string
110
111	AgentPools                 AgentPools
112	AgentTokens                AgentTokens
113	Applies                    Applies
114	ConfigurationVersions      ConfigurationVersions
115	CostEstimates              CostEstimates
116	NotificationConfigurations NotificationConfigurations
117	OAuthClients               OAuthClients
118	OAuthTokens                OAuthTokens
119	Organizations              Organizations
120	OrganizationMemberships    OrganizationMemberships
121	OrganizationTokens         OrganizationTokens
122	Plans                      Plans
123	PlanExports                PlanExports
124	Policies                   Policies
125	PolicyChecks               PolicyChecks
126	PolicySetParameters        PolicySetParameters
127	PolicySets                 PolicySets
128	RegistryModules            RegistryModules
129	Runs                       Runs
130	RunTriggers                RunTriggers
131	SSHKeys                    SSHKeys
132	StateVersionOutputs        StateVersionOutputs
133	StateVersions              StateVersions
134	Teams                      Teams
135	TeamAccess                 TeamAccesses
136	TeamMembers                TeamMembers
137	TeamTokens                 TeamTokens
138	Users                      Users
139	UserTokens                 UserTokens
140	Variables                  Variables
141	Workspaces                 Workspaces
142
143	Meta Meta
144}
145
146// Meta contains any Terraform Cloud APIs which provide data about the API itself.
147type Meta struct {
148	IPRanges IPRanges
149}
150
151// NewClient creates a new Terraform Enterprise API client.
152func NewClient(cfg *Config) (*Client, error) {
153	config := DefaultConfig()
154
155	// Layer in the provided config for any non-blank values.
156	if cfg != nil {
157		if cfg.Address != "" {
158			config.Address = cfg.Address
159		}
160		if cfg.BasePath != "" {
161			config.BasePath = cfg.BasePath
162		}
163		if cfg.Token != "" {
164			config.Token = cfg.Token
165		}
166		for k, v := range cfg.Headers {
167			config.Headers[k] = v
168		}
169		if cfg.HTTPClient != nil {
170			config.HTTPClient = cfg.HTTPClient
171		}
172		if cfg.RetryLogHook != nil {
173			config.RetryLogHook = cfg.RetryLogHook
174		}
175	}
176
177	// Parse the address to make sure its a valid URL.
178	baseURL, err := url.Parse(config.Address)
179	if err != nil {
180		return nil, fmt.Errorf("invalid address: %v", err)
181	}
182
183	baseURL.Path = config.BasePath
184	if !strings.HasSuffix(baseURL.Path, "/") {
185		baseURL.Path += "/"
186	}
187
188	// This value must be provided by the user.
189	if config.Token == "" {
190		return nil, fmt.Errorf("missing API token")
191	}
192
193	// Create the client.
194	client := &Client{
195		baseURL:      baseURL,
196		token:        config.Token,
197		headers:      config.Headers,
198		retryLogHook: config.RetryLogHook,
199	}
200
201	client.http = &retryablehttp.Client{
202		Backoff:      client.retryHTTPBackoff,
203		CheckRetry:   client.retryHTTPCheck,
204		ErrorHandler: retryablehttp.PassthroughErrorHandler,
205		HTTPClient:   config.HTTPClient,
206		RetryWaitMin: 100 * time.Millisecond,
207		RetryWaitMax: 400 * time.Millisecond,
208		RetryMax:     30,
209	}
210
211	meta, err := client.getRawAPIMetadata()
212	if err != nil {
213		return nil, err
214	}
215
216	// Configure the rate limiter.
217	client.configureLimiter(meta.RateLimit)
218
219	// Save the API version so we can return it from the RemoteAPIVersion
220	// method later.
221	client.remoteAPIVersion = meta.APIVersion
222
223	// Create the services.
224	client.AgentPools = &agentPools{client: client}
225	client.AgentTokens = &agentTokens{client: client}
226	client.Applies = &applies{client: client}
227	client.ConfigurationVersions = &configurationVersions{client: client}
228	client.CostEstimates = &costEstimates{client: client}
229	client.NotificationConfigurations = &notificationConfigurations{client: client}
230	client.OAuthClients = &oAuthClients{client: client}
231	client.OAuthTokens = &oAuthTokens{client: client}
232	client.Organizations = &organizations{client: client}
233	client.OrganizationMemberships = &organizationMemberships{client: client}
234	client.OrganizationTokens = &organizationTokens{client: client}
235	client.Plans = &plans{client: client}
236	client.PlanExports = &planExports{client: client}
237	client.Policies = &policies{client: client}
238	client.PolicyChecks = &policyChecks{client: client}
239	client.PolicySetParameters = &policySetParameters{client: client}
240	client.PolicySets = &policySets{client: client}
241	client.RegistryModules = &registryModules{client: client}
242	client.Runs = &runs{client: client}
243	client.RunTriggers = &runTriggers{client: client}
244	client.SSHKeys = &sshKeys{client: client}
245	client.StateVersionOutputs = &stateVersionOutputs{client: client}
246	client.StateVersions = &stateVersions{client: client}
247	client.Teams = &teams{client: client}
248	client.TeamAccess = &teamAccesses{client: client}
249	client.TeamMembers = &teamMembers{client: client}
250	client.TeamTokens = &teamTokens{client: client}
251	client.Users = &users{client: client}
252	client.UserTokens = &userTokens{client: client}
253	client.Variables = &variables{client: client}
254	client.Workspaces = &workspaces{client: client}
255
256	client.Meta = Meta{
257		IPRanges: &ipRanges{client: client},
258	}
259
260	return client, nil
261}
262
263// RemoteAPIVersion returns the server's declared API version string.
264//
265// A Terraform Cloud or Enterprise API server returns its API version in an
266// HTTP header field in all responses. The NewClient function saves the
267// version number returned in its initial setup request and RemoteAPIVersion
268// returns that cached value.
269//
270// The API protocol calls for this string to be a dotted-decimal version number
271// like 2.3.0, where the first number indicates the API major version while the
272// second indicates a minor version which may have introduced some
273// backward-compatible additional features compared to its predecessor.
274//
275// Explicit API versioning was added to the Terraform Cloud and Enterprise
276// APIs as a later addition, so older servers will not return version
277// information. In that case, this function returns an empty string as the
278// version.
279func (c *Client) RemoteAPIVersion() string {
280	return c.remoteAPIVersion
281}
282
283// SetFakeRemoteAPIVersion allows setting a given string as the client's remoteAPIVersion,
284// overriding the value pulled from the API header during client initialization.
285//
286// This is intended for use in tests, when you may want to configure your TFE client to
287// return something different than the actual API version in order to test error handling.
288func (c *Client) SetFakeRemoteAPIVersion(fakeAPIVersion string) {
289	c.remoteAPIVersion = fakeAPIVersion
290}
291
292// RetryServerErrors configures the retry HTTP check to also retry
293// unexpected errors or requests that failed with a server error.
294func (c *Client) RetryServerErrors(retry bool) {
295	c.retryServerErrors = retry
296}
297
298// retryHTTPCheck provides a callback for Client.CheckRetry which
299// will retry both rate limit (429) and server (>= 500) errors.
300func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
301	if ctx.Err() != nil {
302		return false, ctx.Err()
303	}
304	if err != nil {
305		return c.retryServerErrors, err
306	}
307	if resp.StatusCode == 429 || (c.retryServerErrors && resp.StatusCode >= 500) {
308		return true, nil
309	}
310	return false, nil
311}
312
313// retryHTTPBackoff provides a generic callback for Client.Backoff which
314// will pass through all calls based on the status code of the response.
315func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
316	if c.retryLogHook != nil {
317		c.retryLogHook(attemptNum, resp)
318	}
319
320	// Use the rate limit backoff function when we are rate limited.
321	if resp != nil && resp.StatusCode == 429 {
322		return rateLimitBackoff(min, max, attemptNum, resp)
323	}
324
325	// Set custom duration's when we experience a service interruption.
326	min = 700 * time.Millisecond
327	max = 900 * time.Millisecond
328
329	return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
330}
331
332// rateLimitBackoff provides a callback for Client.Backoff which will use the
333// X-RateLimit_Reset header to determine the time to wait. We add some jitter
334// to prevent a thundering herd.
335//
336// min and max are mainly used for bounding the jitter that will be added to
337// the reset time retrieved from the headers. But if the final wait time is
338// less then min, min will be used instead.
339func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
340	// rnd is used to generate pseudo-random numbers.
341	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
342
343	// First create some jitter bounded by the min and max durations.
344	jitter := time.Duration(rnd.Float64() * float64(max-min))
345
346	if resp != nil {
347		if v := resp.Header.Get(headerRateReset); v != "" {
348			if reset, _ := strconv.ParseFloat(v, 64); reset > 0 {
349				// Only update min if the given time to wait is longer.
350				if wait := time.Duration(reset * 1e9); wait > min {
351					min = wait
352				}
353			}
354		}
355	}
356
357	return min + jitter
358}
359
360type rawAPIMetadata struct {
361	// APIVersion is the raw API version string reported by the server in the
362	// TFP-API-Version response header, or an empty string if that header
363	// field was not included in the response.
364	APIVersion string
365
366	// RateLimit is the raw API version string reported by the server in the
367	// X-RateLimit-Limit response header, or an empty string if that header
368	// field was not included in the response.
369	RateLimit string
370}
371
372func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) {
373	var meta rawAPIMetadata
374
375	// Create a new request.
376	u, err := c.baseURL.Parse(PingEndpoint)
377	if err != nil {
378		return meta, err
379	}
380	req, err := http.NewRequest("GET", u.String(), nil)
381	if err != nil {
382		return meta, err
383	}
384
385	// Attach the default headers.
386	for k, v := range c.headers {
387		req.Header[k] = v
388	}
389	req.Header.Set("Accept", "application/vnd.api+json")
390	req.Header.Set("Authorization", "Bearer "+c.token)
391
392	// Make a single request to retrieve the rate limit headers.
393	resp, err := c.http.HTTPClient.Do(req)
394	if err != nil {
395		return meta, err
396	}
397	resp.Body.Close()
398
399	meta.APIVersion = resp.Header.Get(headerAPIVersion)
400	meta.RateLimit = resp.Header.Get(headerRateLimit)
401
402	return meta, nil
403}
404
405// configureLimiter configures the rate limiter.
406func (c *Client) configureLimiter(rawLimit string) {
407
408	// Set default values for when rate limiting is disabled.
409	limit := rate.Inf
410	burst := 0
411
412	if v := rawLimit; v != "" {
413		if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
414			// Configure the limit and burst using a split of 2/3 for the limit and
415			// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
416			// calls before the limiter kicks in. The remaining calls will then be
417			// spread out evenly using intervals of time.Second / limit which should
418			// prevent hitting the rate limit.
419			limit = rate.Limit(rateLimit * 0.66)
420			burst = int(rateLimit * 0.33)
421		}
422	}
423
424	// Create a new limiter using the calculated values.
425	c.limiter = rate.NewLimiter(limit, burst)
426}
427
428// newRequest creates an API request. A relative URL path can be provided in
429// path, in which case it is resolved relative to the apiVersionPath of the
430// Client. Relative URL paths should always be specified without a preceding
431// slash.
432// If v is supplied, the value will be JSONAPI encoded and included as the
433// request body. If the method is GET, the value will be parsed and added as
434// query parameters.
435func (c *Client) newRequest(method, path string, v interface{}) (*retryablehttp.Request, error) {
436	u, err := c.baseURL.Parse(path)
437	if err != nil {
438		return nil, err
439	}
440
441	// Create a request specific headers map.
442	reqHeaders := make(http.Header)
443	reqHeaders.Set("Authorization", "Bearer "+c.token)
444
445	var body interface{}
446	switch method {
447	case "GET":
448		reqHeaders.Set("Accept", "application/vnd.api+json")
449
450		if v != nil {
451			q, err := query.Values(v)
452			if err != nil {
453				return nil, err
454			}
455			u.RawQuery = q.Encode()
456		}
457	case "DELETE", "PATCH", "POST":
458		reqHeaders.Set("Accept", "application/vnd.api+json")
459		reqHeaders.Set("Content-Type", "application/vnd.api+json")
460
461		if v != nil {
462			if body, err = serializeRequestBody(v); err != nil {
463				return nil, err
464			}
465		}
466	case "PUT":
467		reqHeaders.Set("Accept", "application/json")
468		reqHeaders.Set("Content-Type", "application/octet-stream")
469		body = v
470	}
471
472	req, err := retryablehttp.NewRequest(method, u.String(), body)
473	if err != nil {
474		return nil, err
475	}
476
477	// Set the default headers.
478	for k, v := range c.headers {
479		req.Header[k] = v
480	}
481
482	// Set the request specific headers.
483	for k, v := range reqHeaders {
484		req.Header[k] = v
485	}
486
487	return req, nil
488}
489
490// Helper method that serializes the given ptr or ptr slice into a JSON
491// request. It automatically uses jsonapi or json serialization, depending
492// on the body type's tags.
493func serializeRequestBody(v interface{}) (interface{}, error) {
494	// The body can be a slice of pointers or a pointer. In either
495	// case we want to choose the serialization type based on the
496	// individual record type. To determine that type, we need
497	// to either follow the pointer or examine the slice element type.
498	// There are other theoretical possiblities (e. g. maps,
499	// non-pointers) but they wouldn't work anyway because the
500	// json-api library doesn't support serializing other things.
501	var modelType reflect.Type
502	bodyType := reflect.TypeOf(v)
503	invalidBodyError := errors.New("go-tfe bug: DELETE/PATCH/POST body must be nil, ptr, or ptr slice")
504	switch bodyType.Kind() {
505	case reflect.Slice:
506		sliceElem := bodyType.Elem()
507		if sliceElem.Kind() != reflect.Ptr {
508			return nil, invalidBodyError
509		}
510		modelType = sliceElem.Elem()
511	case reflect.Ptr:
512		modelType = reflect.ValueOf(v).Elem().Type()
513	default:
514		return nil, invalidBodyError
515	}
516
517	// Infer whether the request uses jsonapi or regular json
518	// serialization based on how the fields are tagged.
519	jsonApiFields := 0
520	jsonFields := 0
521	for i := 0; i < modelType.NumField(); i++ {
522		structField := modelType.Field(i)
523		if structField.Tag.Get("jsonapi") != "" {
524			jsonApiFields++
525		}
526		if structField.Tag.Get("json") != "" {
527			jsonFields++
528		}
529	}
530	if jsonApiFields > 0 && jsonFields > 0 {
531		// Defining a struct with both json and jsonapi tags doesn't
532		// make sense, because a struct can only be serialized
533		// as one or another. If this does happen, it's a bug
534		// in the library that should be fixed at development time
535		return nil, errors.New("go-tfe bug: struct can't use both json and jsonapi attributes")
536	}
537
538	if jsonFields > 0 {
539		return json.Marshal(v)
540	} else {
541		buf := bytes.NewBuffer(nil)
542		if err := jsonapi.MarshalPayloadWithoutIncluded(buf, v); err != nil {
543			return nil, err
544		}
545		return buf, nil
546	}
547}
548
549// do sends an API request and returns the API response. The API response
550// is JSONAPI decoded and the document's primary data is stored in the value
551// pointed to by v, or returned as an error if an API error has occurred.
552
553// If v implements the io.Writer interface, the raw response body will be
554// written to v, without attempting to first decode it.
555//
556// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err()
557// will be returned.
558func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface{}) error {
559	// Wait will block until the limiter can obtain a new token
560	// or returns an error if the given context is canceled.
561	if err := c.limiter.Wait(ctx); err != nil {
562		return err
563	}
564
565	// Add the context to the request.
566	req = req.WithContext(ctx)
567
568	// Execute the request and check the response.
569	resp, err := c.http.Do(req)
570	if err != nil {
571		// If we got an error, and the context has been canceled,
572		// the context's error is probably more useful.
573		select {
574		case <-ctx.Done():
575			return ctx.Err()
576		default:
577			return err
578		}
579	}
580	defer resp.Body.Close()
581
582	// Basic response checking.
583	if err := checkResponseCode(resp); err != nil {
584		return err
585	}
586
587	// Return here if decoding the response isn't needed.
588	if v == nil {
589		return nil
590	}
591
592	// If v implements io.Writer, write the raw response body.
593	if w, ok := v.(io.Writer); ok {
594		_, err = io.Copy(w, resp.Body)
595		return err
596	}
597
598	// Get the value of v so we can test if it's a struct.
599	dst := reflect.Indirect(reflect.ValueOf(v))
600
601	// Return an error if v is not a struct or an io.Writer.
602	if dst.Kind() != reflect.Struct {
603		return fmt.Errorf("v must be a struct or an io.Writer")
604	}
605
606	// Try to get the Items and Pagination struct fields.
607	items := dst.FieldByName("Items")
608	pagination := dst.FieldByName("Pagination")
609
610	// Unmarshal a single value if v does not contain the
611	// Items and Pagination struct fields.
612	if !items.IsValid() || !pagination.IsValid() {
613		return jsonapi.UnmarshalPayload(resp.Body, v)
614	}
615
616	// Return an error if v.Items is not a slice.
617	if items.Type().Kind() != reflect.Slice {
618		return fmt.Errorf("v.Items must be a slice")
619	}
620
621	// Create a temporary buffer and copy all the read data into it.
622	body := bytes.NewBuffer(nil)
623	reader := io.TeeReader(resp.Body, body)
624
625	// Unmarshal as a list of values as v.Items is a slice.
626	raw, err := jsonapi.UnmarshalManyPayload(reader, items.Type().Elem())
627	if err != nil {
628		return err
629	}
630
631	// Make a new slice to hold the results.
632	sliceType := reflect.SliceOf(items.Type().Elem())
633	result := reflect.MakeSlice(sliceType, 0, len(raw))
634
635	// Add all of the results to the new slice.
636	for _, v := range raw {
637		result = reflect.Append(result, reflect.ValueOf(v))
638	}
639
640	// Pointer-swap the result.
641	items.Set(result)
642
643	// As we are getting a list of values, we need to decode
644	// the pagination details out of the response body.
645	p, err := parsePagination(body)
646	if err != nil {
647		return err
648	}
649
650	// Pointer-swap the decoded pagination details.
651	pagination.Set(reflect.ValueOf(p))
652
653	return nil
654}
655
656// ListOptions is used to specify pagination options when making API requests.
657// Pagination allows breaking up large result sets into chunks, or "pages".
658type ListOptions struct {
659	// The page number to request. The results vary based on the PageSize.
660	PageNumber int `url:"page[number],omitempty"`
661
662	// The number of elements returned in a single page.
663	PageSize int `url:"page[size],omitempty"`
664}
665
666// Pagination is used to return the pagination details of an API request.
667type Pagination struct {
668	CurrentPage  int `json:"current-page"`
669	PreviousPage int `json:"prev-page"`
670	NextPage     int `json:"next-page"`
671	TotalPages   int `json:"total-pages"`
672	TotalCount   int `json:"total-count"`
673}
674
675func parsePagination(body io.Reader) (*Pagination, error) {
676	var raw struct {
677		Meta struct {
678			Pagination Pagination `json:"pagination"`
679		} `json:"meta"`
680	}
681
682	// JSON decode the raw response.
683	if err := json.NewDecoder(body).Decode(&raw); err != nil {
684		return &Pagination{}, err
685	}
686
687	return &raw.Meta.Pagination, nil
688}
689
690// checkResponseCode can be used to check the status code of an HTTP request.
691func checkResponseCode(r *http.Response) error {
692	if r.StatusCode >= 200 && r.StatusCode <= 299 {
693		return nil
694	}
695
696	switch r.StatusCode {
697	case 401:
698		return ErrUnauthorized
699	case 404:
700		return ErrResourceNotFound
701	case 409:
702		switch {
703		case strings.HasSuffix(r.Request.URL.Path, "actions/lock"):
704			return ErrWorkspaceLocked
705		case strings.HasSuffix(r.Request.URL.Path, "actions/unlock"):
706			return ErrWorkspaceNotLocked
707		case strings.HasSuffix(r.Request.URL.Path, "actions/force-unlock"):
708			return ErrWorkspaceNotLocked
709		}
710	}
711
712	// Decode the error payload.
713	errPayload := &jsonapi.ErrorsPayload{}
714	err := json.NewDecoder(r.Body).Decode(errPayload)
715	if err != nil || len(errPayload.Errors) == 0 {
716		return fmt.Errorf(r.Status)
717	}
718
719	// Parse and format the errors.
720	var errs []string
721	for _, e := range errPayload.Errors {
722		if e.Detail == "" {
723			errs = append(errs, e.Title)
724		} else {
725			errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
726		}
727	}
728
729	return fmt.Errorf(strings.Join(errs, "\n"))
730}
731