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 = ¬ificationConfigurations{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 = ®istryModules{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