summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/xanzy/go-gitlab/gitlab.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/xanzy/go-gitlab/gitlab.go')
-rw-r--r--vendor/github.com/xanzy/go-gitlab/gitlab.go391
1 files changed, 288 insertions, 103 deletions
diff --git a/vendor/github.com/xanzy/go-gitlab/gitlab.go b/vendor/github.com/xanzy/go-gitlab/gitlab.go
index b8c951c5dc..4fdca56351 100644
--- a/vendor/github.com/xanzy/go-gitlab/gitlab.go
+++ b/vendor/github.com/xanzy/go-gitlab/gitlab.go
@@ -18,28 +18,35 @@
package gitlab
import (
- "bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
+ "math/rand"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
+ "sync"
"time"
"github.com/google/go-querystring/query"
+ "github.com/hashicorp/go-cleanhttp"
+ retryablehttp "github.com/hashicorp/go-retryablehttp"
"golang.org/x/oauth2"
+ "golang.org/x/time/rate"
)
const (
defaultBaseURL = "https://gitlab.com/"
apiVersionPath = "api/v4/"
userAgent = "go-gitlab"
+
+ headerRateLimit = "RateLimit-Limit"
+ headerRateReset = "RateLimit-Reset"
)
// authType represents an authentication type within GitLab.
@@ -83,6 +90,7 @@ type BuildStateValue string
// These constants represent all valid build states.
const (
Pending BuildStateValue = "pending"
+ Created BuildStateValue = "created"
Running BuildStateValue = "running"
Success BuildStateValue = "success"
Failed BuildStateValue = "failed"
@@ -91,6 +99,18 @@ const (
Manual BuildStateValue = "manual"
)
+// DeploymentStatusValue represents a Gitlab deployment status.
+type DeploymentStatusValue string
+
+// These constants represent all valid deployment statuses.
+const (
+ DeploymentStatusCreated DeploymentStatusValue = "created"
+ DeploymentStatusRunning DeploymentStatusValue = "running"
+ DeploymentStatusSuccess DeploymentStatusValue = "success"
+ DeploymentStatusFailed DeploymentStatusValue = "failed"
+ DeploymentStatusCanceled DeploymentStatusValue = "canceled"
+)
+
// ISOTime represents an ISO 8601 formatted date
type ISOTime time.Time
@@ -215,6 +235,33 @@ const (
PublicVisibility VisibilityValue = "public"
)
+// ProjectCreationLevelValue represents a project creation level within GitLab.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/
+type ProjectCreationLevelValue string
+
+// List of available project creation levels.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/
+const (
+ NoOneProjectCreation ProjectCreationLevelValue = "noone"
+ MaintainerProjectCreation ProjectCreationLevelValue = "maintainer"
+ DeveloperProjectCreation ProjectCreationLevelValue = "developer"
+)
+
+// SubGroupCreationLevelValue represents a sub group creation level within GitLab.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/
+type SubGroupCreationLevelValue string
+
+// List of available sub group creation levels.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/
+const (
+ OwnerSubGroupCreationLevelValue SubGroupCreationLevelValue = "owner"
+ MaintainerSubGroupCreationLevelValue SubGroupCreationLevelValue = "maintainer"
+)
+
// VariableTypeValue represents a variable type within GitLab.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/
@@ -281,13 +328,23 @@ const (
// A Client manages communication with the GitLab API.
type Client struct {
// HTTP client used to communicate with the API.
- client *http.Client
+ client *retryablehttp.Client
// Base URL for API requests. Defaults to the public GitLab API, but can be
// set to a domain endpoint to use with a self hosted GitLab server. baseURL
// should always be specified with a trailing slash.
baseURL *url.URL
+ // disableRetries is used to disable the default retry logic.
+ disableRetries bool
+
+ // configLimiter is used to make sure the limiter is configured exactly
+ // once and block all other calls until the initial (one) call is done.
+ configureLimiterOnce sync.Once
+
+ // Limiter is used to limit API calls and prevent 429 responses.
+ limiter *rate.Limiter
+
// Token type used to make authenticated API calls.
authType authType
@@ -302,6 +359,7 @@ type Client struct {
// Services used for talking to different parts of the GitLab API.
AccessRequests *AccessRequestsService
+ Applications *ApplicationsService
AwardEmoji *AwardEmojiService
Boards *IssueBoardsService
Branches *BranchesService
@@ -311,6 +369,7 @@ type Client struct {
ContainerRegistry *ContainerRegistryService
CustomAttribute *CustomAttributesService
DeployKeys *DeployKeysService
+ DeployTokens *DeployTokensService
Deployments *DeploymentsService
Discussions *DiscussionsService
Environments *EnvironmentsService
@@ -382,27 +441,31 @@ type ListOptions struct {
PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"`
}
-// NewClient returns a new GitLab API client. If a nil httpClient is
-// provided, http.DefaultClient will be used. To use API methods which require
+// NewClient returns a new GitLab API client. To use API methods which require
// authentication, provide a valid private or personal token.
-func NewClient(httpClient *http.Client, token string) *Client {
- client := newClient(httpClient)
+func NewClient(token string, options ...ClientOptionFunc) (*Client, error) {
+ client, err := newClient(options...)
+ if err != nil {
+ return nil, err
+ }
client.authType = privateToken
client.token = token
- return client
+ return client, nil
}
-// NewBasicAuthClient returns a new GitLab API client. If a nil httpClient is
-// provided, http.DefaultClient will be used. To use API methods which require
-// authentication, provide a valid username and password.
-func NewBasicAuthClient(httpClient *http.Client, endpoint, username, password string) (*Client, error) {
- client := newClient(httpClient)
+// NewBasicAuthClient returns a new GitLab API client. To use API methods which
+// require authentication, provide a valid username and password.
+func NewBasicAuthClient(username, password string, options ...ClientOptionFunc) (*Client, error) {
+ client, err := newClient(options...)
+ if err != nil {
+ return nil, err
+ }
+
client.authType = basicAuth
client.username = username
client.password = password
- client.SetBaseURL(endpoint)
- err := client.requestOAuthToken(context.TODO())
+ err = client.requestOAuthToken(context.Background())
if err != nil {
return nil, err
}
@@ -410,6 +473,18 @@ func NewBasicAuthClient(httpClient *http.Client, endpoint, username, password st
return client, nil
}
+// NewOAuthClient returns a new GitLab API client. To use API methods which
+// require authentication, provide a valid oauth token.
+func NewOAuthClient(token string, options ...ClientOptionFunc) (*Client, error) {
+ client, err := newClient(options...)
+ if err != nil {
+ return nil, err
+ }
+ client.authType = oAuthToken
+ client.token = token
+ return client, nil
+}
+
func (c *Client) requestOAuthToken(ctx context.Context) error {
config := &oauth2.Config{
Endpoint: oauth2.Endpoint{
@@ -426,25 +501,31 @@ func (c *Client) requestOAuthToken(ctx context.Context) error {
return nil
}
-// NewOAuthClient returns a new GitLab API client. If a nil httpClient is
-// provided, http.DefaultClient will be used. To use API methods which require
-// authentication, provide a valid oauth token.
-func NewOAuthClient(httpClient *http.Client, token string) *Client {
- client := newClient(httpClient)
- client.authType = oAuthToken
- client.token = token
- return client
-}
-
-func newClient(httpClient *http.Client) *Client {
- if httpClient == nil {
- httpClient = http.DefaultClient
+func newClient(options ...ClientOptionFunc) (*Client, error) {
+ c := &Client{UserAgent: userAgent}
+
+ // Configure the HTTP client.
+ c.client = &retryablehttp.Client{
+ Backoff: c.retryHTTPBackoff,
+ CheckRetry: c.retryHTTPCheck,
+ ErrorHandler: retryablehttp.PassthroughErrorHandler,
+ HTTPClient: cleanhttp.DefaultPooledClient(),
+ RetryWaitMin: 100 * time.Millisecond,
+ RetryWaitMax: 400 * time.Millisecond,
+ RetryMax: 5,
}
- c := &Client{client: httpClient, UserAgent: userAgent}
- if err := c.SetBaseURL(defaultBaseURL); err != nil {
- // Should never happen since defaultBaseURL is our constant.
- panic(err)
+ // Set the default base URL.
+ c.setBaseURL(defaultBaseURL)
+
+ // Apply any given client options.
+ for _, fn := range options {
+ if fn == nil {
+ continue
+ }
+ if err := fn(c); err != nil {
+ return nil, err
+ }
}
// Create the internal timeStats service.
@@ -452,6 +533,7 @@ func newClient(httpClient *http.Client) *Client {
// Create all the public services.
c.AccessRequests = &AccessRequestsService{client: c}
+ c.Applications = &ApplicationsService{client: c}
c.AwardEmoji = &AwardEmojiService{client: c}
c.Boards = &IssueBoardsService{client: c}
c.Branches = &BranchesService{client: c}
@@ -461,6 +543,7 @@ func newClient(httpClient *http.Client) *Client {
c.ContainerRegistry = &ContainerRegistryService{client: c}
c.CustomAttribute = &CustomAttributesService{client: c}
c.DeployKeys = &DeployKeysService{client: c}
+ c.DeployTokens = &DeployTokensService{client: c}
c.Deployments = &DeploymentsService{client: c}
c.Discussions = &DiscussionsService{client: c}
c.Environments = &EnvironmentsService{client: c}
@@ -521,7 +604,107 @@ func newClient(httpClient *http.Client) *Client {
c.Version = &VersionService{client: c}
c.Wikis = &WikisService{client: c}
- return c
+ return c, nil
+}
+
+// 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()
+ }
+ if err != nil {
+ return false, err
+ }
+ if !c.disableRetries && (resp.StatusCode == 429 || 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 {
+ // Use the rate limit backoff function when we are rate limited.
+ if resp != nil && 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
+// RateLimit-Reset header to determine the time to wait. We add some jitter
+// to prevent a thundering herd.
+//
+// min and max are mainly used for bounding the jitter that will be added to
+// the reset time retrieved from the headers. But if the final wait time is
+// less then min, min will be used instead.
+func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+ // rnd is used to generate pseudo-random numbers.
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ // First create some jitter bounded by the min and max durations.
+ jitter := time.Duration(rnd.Float64() * float64(max-min))
+
+ if resp != nil {
+ if v := resp.Header.Get(headerRateReset); v != "" {
+ if reset, _ := strconv.ParseInt(v, 10, 64); reset > 0 {
+ // Only update min if the given time to wait is longer.
+ if wait := time.Until(time.Unix(reset, 0)); wait > min {
+ min = wait
+ }
+ }
+ }
+ }
+
+ return min + jitter
+}
+
+// configureLimiter configures the rate limiter.
+func (c *Client) configureLimiter() error {
+ // Set default values for when rate limiting is disabled.
+ limit := rate.Inf
+ burst := 0
+
+ defer func() {
+ // Create a new limiter using the calculated values.
+ c.limiter = rate.NewLimiter(limit, burst)
+ }()
+
+ // Create a new request.
+ req, err := http.NewRequest("GET", c.baseURL.String(), nil)
+ if err != nil {
+ return err
+ }
+
+ // Make a single request to retrieve the rate limit headers.
+ resp, err := c.client.HTTPClient.Do(req)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if v := resp.Header.Get(headerRateLimit); v != "" {
+ if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
+ // The rate limit is based on requests per minute, so for our limiter to
+ // work correctly we devide the limit by 60 to get the limit per second.
+ rateLimit /= 60
+ // 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
+ // calls before the limiter kicks in. The remaining calls will then be
+ // spread out evenly using intervals of time.Second / limit which should
+ // prevent hitting the rate limit.
+ limit = rate.Limit(rateLimit * 0.66)
+ burst = int(rateLimit * 0.33)
+ }
+ }
+
+ return nil
}
// BaseURL return a copy of the baseURL.
@@ -530,9 +713,8 @@ func (c *Client) BaseURL() *url.URL {
return &u
}
-// SetBaseURL sets the base URL for API requests to a custom endpoint. urlStr
-// should always be specified with a trailing slash.
-func (c *Client) SetBaseURL(urlStr string) error {
+// setBaseURL sets the base URL for API requests to a custom endpoint.
+func (c *Client) setBaseURL(urlStr string) error {
// Make sure the given URL end with a slash
if !strings.HasSuffix(urlStr, "/") {
urlStr += "/"
@@ -554,11 +736,11 @@ func (c *Client) SetBaseURL(urlStr string) error {
}
// NewRequest creates an API request. A relative URL path can be provided in
-// urlStr, in which case it is resolved relative to the base URL of the Client.
+// path, in which case it is resolved relative to the base URL of the Client.
// Relative URL paths should always be specified without a preceding slash. If
// specified, the value pointed to by body is JSON encoded and included as the
// request body.
-func (c *Client) NewRequest(method, path string, opt interface{}, options []OptionFunc) (*http.Request, error) {
+func (c *Client) NewRequest(method, path string, opt interface{}, options []RequestOptionFunc) (*retryablehttp.Request, error) {
u := *c.baseURL
unescaped, err := url.PathUnescape(path)
if err != nil {
@@ -569,7 +751,33 @@ func (c *Client) NewRequest(method, path string, opt interface{}, options []Opti
u.RawPath = c.baseURL.Path + path
u.Path = c.baseURL.Path + unescaped
- if opt != nil {
+ // Create a request specific headers map.
+ reqHeaders := make(http.Header)
+ reqHeaders.Set("Accept", "application/json")
+
+ switch c.authType {
+ case basicAuth, oAuthToken:
+ reqHeaders.Set("Authorization", "Bearer "+c.token)
+ case privateToken:
+ reqHeaders.Set("PRIVATE-TOKEN", c.token)
+ }
+
+ if c.UserAgent != "" {
+ reqHeaders.Set("User-Agent", c.UserAgent)
+ }
+
+ var body interface{}
+ switch {
+ case method == "POST" || method == "PUT":
+ reqHeaders.Set("Content-Type", "application/json")
+
+ if opt != nil {
+ body, err = json.Marshal(opt)
+ if err != nil {
+ return nil, err
+ }
+ }
+ case opt != nil:
q, err := query.Values(opt)
if err != nil {
return nil, err
@@ -577,53 +785,23 @@ func (c *Client) NewRequest(method, path string, opt interface{}, options []Opti
u.RawQuery = q.Encode()
}
- req := &http.Request{
- Method: method,
- URL: &u,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- Header: make(http.Header),
- Host: u.Host,
+ req, err := retryablehttp.NewRequest(method, u.String(), body)
+ if err != nil {
+ return nil, err
}
for _, fn := range options {
if fn == nil {
continue
}
-
if err := fn(req); err != nil {
return nil, err
}
}
- if method == "POST" || method == "PUT" {
- bodyBytes, err := json.Marshal(opt)
- if err != nil {
- return nil, err
- }
- bodyReader := bytes.NewReader(bodyBytes)
-
- u.RawQuery = ""
- req.Body = ioutil.NopCloser(bodyReader)
- req.GetBody = func() (io.ReadCloser, error) {
- return ioutil.NopCloser(bodyReader), nil
- }
- req.ContentLength = int64(bodyReader.Len())
- req.Header.Set("Content-Type", "application/json")
- }
-
- req.Header.Set("Accept", "application/json")
-
- switch c.authType {
- case basicAuth, oAuthToken:
- req.Header.Set("Authorization", "Bearer "+c.token)
- case privateToken:
- req.Header.Set("PRIVATE-TOKEN", c.token)
- }
-
- if c.UserAgent != "" {
- req.Header.Set("User-Agent", c.UserAgent)
+ // Set the request specific headers.
+ for k, v := range reqHeaders {
+ req.Header[k] = v
}
return req, nil
@@ -691,7 +869,16 @@ func (r *Response) populatePageValues() {
// error if an API error has occurred. If v implements the io.Writer
// interface, the raw response body will be written to v, without attempting to
// first decode it.
-func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
+func (c *Client) Do(req *retryablehttp.Request, v interface{}) (*Response, error) {
+ // If not yet configured, try to configure the rate limiter. Fail
+ // silently as the limiter will be disabled in case of an error.
+ c.configureLimiterOnce.Do(func() { c.configureLimiter() })
+
+ // Wait will block until the limiter can obtain a new token.
+ if err := c.limiter.Wait(req.Context()); err != nil {
+ return nil, err
+ }
+
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -710,8 +897,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
err = CheckResponse(resp)
if err != nil {
- // even though there was an error, we still return the response
- // in case the caller wants to inspect it further
+ // Even though there was an error, we still return the response
+ // in case the caller wants to inspect it further.
return response, err
}
@@ -826,32 +1013,6 @@ func parseError(raw interface{}) string {
}
}
-// OptionFunc can be passed to all API requests to make the API call as if you were
-// another user, provided your private token is from an administrator account.
-//
-// GitLab docs: https://docs.gitlab.com/ce/api/README.html#sudo
-type OptionFunc func(*http.Request) error
-
-// WithSudo takes either a username or user ID and sets the SUDO request header
-func WithSudo(uid interface{}) OptionFunc {
- return func(req *http.Request) error {
- user, err := parseID(uid)
- if err != nil {
- return err
- }
- req.Header.Set("SUDO", user)
- return nil
- }
-}
-
-// WithContext runs the request with the provided context
-func WithContext(ctx context.Context) OptionFunc {
- return func(req *http.Request) error {
- *req = *req.WithContext(ctx)
- return nil
- }
-}
-
// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
@@ -901,6 +1062,14 @@ func BuildState(v BuildStateValue) *BuildStateValue {
return p
}
+// DeploymentStatus is a helper routine that allocates a new
+// DeploymentStatusValue to store v and returns a pointer to it.
+func DeploymentStatus(v DeploymentStatusValue) *DeploymentStatusValue {
+ p := new(DeploymentStatusValue)
+ *p = v
+ return p
+}
+
// NotificationLevel is a helper routine that allocates a new NotificationLevelValue
// to store v and returns a pointer to it.
func NotificationLevel(v NotificationLevelValue) *NotificationLevelValue {
@@ -925,6 +1094,22 @@ func Visibility(v VisibilityValue) *VisibilityValue {
return p
}
+// ProjectCreationLevel is a helper routine that allocates a new ProjectCreationLevelValue
+// to store v and returns a pointer to it.
+func ProjectCreationLevel(v ProjectCreationLevelValue) *ProjectCreationLevelValue {
+ p := new(ProjectCreationLevelValue)
+ *p = v
+ return p
+}
+
+// SubGroupCreationLevel is a helper routine that allocates a new SubGroupCreationLevelValue
+// to store v and returns a pointer to it.
+func SubGroupCreationLevel(v SubGroupCreationLevelValue) *SubGroupCreationLevelValue {
+ p := new(SubGroupCreationLevelValue)
+ *p = v
+ return p
+}
+
// MergeMethod is a helper routine that allocates a new MergeMethod
// to sotre v and returns a pointer to it.
func MergeMethod(v MergeMethodValue) *MergeMethodValue {