...

Source file src/github.com/xanzy/go-gitlab/gitlab.go

Documentation: github.com/xanzy/go-gitlab

     1  //
     2  // Copyright 2021, Sander van Harmelen
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  //
    16  
    17  // Package gitlab implements a GitLab API client.
    18  package gitlab
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"math/rand"
    27  	"mime/multipart"
    28  	"net/http"
    29  	"net/url"
    30  	"sort"
    31  	"strconv"
    32  	"strings"
    33  	"sync"
    34  	"time"
    35  
    36  	"github.com/hashicorp/go-cleanhttp"
    37  
    38  	"github.com/google/go-querystring/query"
    39  	retryablehttp "github.com/hashicorp/go-retryablehttp"
    40  	"golang.org/x/oauth2"
    41  	"golang.org/x/time/rate"
    42  )
    43  
    44  const (
    45  	defaultBaseURL = "https://gitlab.com/"
    46  	apiVersionPath = "api/v4/"
    47  	userAgent      = "go-gitlab"
    48  
    49  	headerRateLimit = "RateLimit-Limit"
    50  	headerRateReset = "RateLimit-Reset"
    51  )
    52  
    53  // AuthType represents an authentication type within GitLab.
    54  //
    55  // GitLab API docs: https://docs.gitlab.com/ee/api/
    56  type AuthType int
    57  
    58  // List of available authentication types.
    59  //
    60  // GitLab API docs: https://docs.gitlab.com/ee/api/
    61  const (
    62  	BasicAuth AuthType = iota
    63  	JobToken
    64  	OAuthToken
    65  	PrivateToken
    66  )
    67  
    68  // A Client manages communication with the GitLab API.
    69  type Client struct {
    70  	// HTTP client used to communicate with the API.
    71  	client *retryablehttp.Client
    72  
    73  	// Base URL for API requests. Defaults to the public GitLab API, but can be
    74  	// set to a domain endpoint to use with a self hosted GitLab server. baseURL
    75  	// should always be specified with a trailing slash.
    76  	baseURL *url.URL
    77  
    78  	// disableRetries is used to disable the default retry logic.
    79  	disableRetries bool
    80  
    81  	// configureLimiterOnce is used to make sure the limiter is configured exactly
    82  	// once and block all other calls until the initial (one) call is done.
    83  	configureLimiterOnce sync.Once
    84  
    85  	// Limiter is used to limit API calls and prevent 429 responses.
    86  	limiter RateLimiter
    87  
    88  	// Token type used to make authenticated API calls.
    89  	authType AuthType
    90  
    91  	// Username and password used for basic authentication.
    92  	username, password string
    93  
    94  	// Token used to make authenticated API calls.
    95  	token string
    96  
    97  	// Protects the token field from concurrent read/write accesses.
    98  	tokenLock sync.RWMutex
    99  
   100  	// Default request options applied to every request.
   101  	defaultRequestOptions []RequestOptionFunc
   102  
   103  	// User agent used when communicating with the GitLab API.
   104  	UserAgent string
   105  
   106  	// Services used for talking to different parts of the GitLab API.
   107  	AccessRequests               *AccessRequestsService
   108  	Appearance                   *AppearanceService
   109  	Applications                 *ApplicationsService
   110  	AuditEvents                  *AuditEventsService
   111  	Avatar                       *AvatarRequestsService
   112  	AwardEmoji                   *AwardEmojiService
   113  	Boards                       *IssueBoardsService
   114  	Branches                     *BranchesService
   115  	BroadcastMessage             *BroadcastMessagesService
   116  	CIYMLTemplate                *CIYMLTemplatesService
   117  	ClusterAgents                *ClusterAgentsService
   118  	Commits                      *CommitsService
   119  	ContainerRegistry            *ContainerRegistryService
   120  	CustomAttribute              *CustomAttributesService
   121  	DeployKeys                   *DeployKeysService
   122  	DeployTokens                 *DeployTokensService
   123  	DeploymentMergeRequests      *DeploymentMergeRequestsService
   124  	Deployments                  *DeploymentsService
   125  	Discussions                  *DiscussionsService
   126  	DockerfileTemplate           *DockerfileTemplatesService
   127  	DraftNotes                   *DraftNotesService
   128  	Environments                 *EnvironmentsService
   129  	EpicIssues                   *EpicIssuesService
   130  	Epics                        *EpicsService
   131  	ErrorTracking                *ErrorTrackingService
   132  	Events                       *EventsService
   133  	ExternalStatusChecks         *ExternalStatusChecksService
   134  	Features                     *FeaturesService
   135  	FreezePeriods                *FreezePeriodsService
   136  	GenericPackages              *GenericPackagesService
   137  	GeoNodes                     *GeoNodesService
   138  	GitIgnoreTemplates           *GitIgnoreTemplatesService
   139  	GroupAccessTokens            *GroupAccessTokensService
   140  	GroupBadges                  *GroupBadgesService
   141  	GroupCluster                 *GroupClustersService
   142  	GroupEpicBoards              *GroupEpicBoardsService
   143  	GroupImportExport            *GroupImportExportService
   144  	GroupIssueBoards             *GroupIssueBoardsService
   145  	GroupIterations              *GroupIterationsService
   146  	GroupLabels                  *GroupLabelsService
   147  	GroupMembers                 *GroupMembersService
   148  	GroupMilestones              *GroupMilestonesService
   149  	GroupProtectedEnvironments   *GroupProtectedEnvironmentsService
   150  	GroupRepositoryStorageMove   *GroupRepositoryStorageMoveService
   151  	GroupSSHCertificates         *GroupSSHCertificatesService
   152  	GroupVariables               *GroupVariablesService
   153  	GroupWikis                   *GroupWikisService
   154  	Groups                       *GroupsService
   155  	InstanceCluster              *InstanceClustersService
   156  	InstanceVariables            *InstanceVariablesService
   157  	Invites                      *InvitesService
   158  	IssueLinks                   *IssueLinksService
   159  	Issues                       *IssuesService
   160  	IssuesStatistics             *IssuesStatisticsService
   161  	Jobs                         *JobsService
   162  	JobTokenScope                *JobTokenScopeService
   163  	Keys                         *KeysService
   164  	Labels                       *LabelsService
   165  	License                      *LicenseService
   166  	LicenseTemplates             *LicenseTemplatesService
   167  	ManagedLicenses              *ManagedLicensesService
   168  	Markdown                     *MarkdownService
   169  	MemberRolesService           *MemberRolesService
   170  	MergeRequestApprovals        *MergeRequestApprovalsService
   171  	MergeRequests                *MergeRequestsService
   172  	MergeTrains                  *MergeTrainsService
   173  	Metadata                     *MetadataService
   174  	Milestones                   *MilestonesService
   175  	Namespaces                   *NamespacesService
   176  	Notes                        *NotesService
   177  	NotificationSettings         *NotificationSettingsService
   178  	Packages                     *PackagesService
   179  	Pages                        *PagesService
   180  	PagesDomains                 *PagesDomainsService
   181  	PersonalAccessTokens         *PersonalAccessTokensService
   182  	PipelineSchedules            *PipelineSchedulesService
   183  	PipelineTriggers             *PipelineTriggersService
   184  	Pipelines                    *PipelinesService
   185  	PlanLimits                   *PlanLimitsService
   186  	ProjectAccessTokens          *ProjectAccessTokensService
   187  	ProjectBadges                *ProjectBadgesService
   188  	ProjectCluster               *ProjectClustersService
   189  	ProjectFeatureFlags          *ProjectFeatureFlagService
   190  	ProjectImportExport          *ProjectImportExportService
   191  	ProjectIterations            *ProjectIterationsService
   192  	ProjectMembers               *ProjectMembersService
   193  	ProjectMirrors               *ProjectMirrorService
   194  	ProjectRepositoryStorageMove *ProjectRepositoryStorageMoveService
   195  	ProjectSnippets              *ProjectSnippetsService
   196  	ProjectTemplates             *ProjectTemplatesService
   197  	ProjectVariables             *ProjectVariablesService
   198  	ProjectVulnerabilities       *ProjectVulnerabilitiesService
   199  	Projects                     *ProjectsService
   200  	ProtectedBranches            *ProtectedBranchesService
   201  	ProtectedEnvironments        *ProtectedEnvironmentsService
   202  	ProtectedTags                *ProtectedTagsService
   203  	ReleaseLinks                 *ReleaseLinksService
   204  	Releases                     *ReleasesService
   205  	Repositories                 *RepositoriesService
   206  	RepositoryFiles              *RepositoryFilesService
   207  	RepositorySubmodules         *RepositorySubmodulesService
   208  	ResourceIterationEvents      *ResourceIterationEventsService
   209  	ResourceLabelEvents          *ResourceLabelEventsService
   210  	ResourceMilestoneEvents      *ResourceMilestoneEventsService
   211  	ResourceStateEvents          *ResourceStateEventsService
   212  	ResourceWeightEvents         *ResourceWeightEventsService
   213  	Runners                      *RunnersService
   214  	Search                       *SearchService
   215  	Services                     *ServicesService
   216  	Settings                     *SettingsService
   217  	Sidekiq                      *SidekiqService
   218  	SnippetRepositoryStorageMove *SnippetRepositoryStorageMoveService
   219  	Snippets                     *SnippetsService
   220  	SystemHooks                  *SystemHooksService
   221  	Tags                         *TagsService
   222  	Todos                        *TodosService
   223  	Topics                       *TopicsService
   224  	Users                        *UsersService
   225  	Validate                     *ValidateService
   226  	Version                      *VersionService
   227  	Wikis                        *WikisService
   228  }
   229  
   230  // ListOptions specifies the optional parameters to various List methods that
   231  // support pagination.
   232  type ListOptions struct {
   233  	// For offset-based paginated result sets, page of results to retrieve.
   234  	Page int `url:"page,omitempty" json:"page,omitempty"`
   235  	// For offset-based and keyset-based paginated result sets, the number of results to include per page.
   236  	PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"`
   237  
   238  	// For keyset-based paginated result sets, name of the column by which to order
   239  	OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"`
   240  	// For keyset-based paginated result sets, the value must be `"keyset"`
   241  	Pagination string `url:"pagination,omitempty" json:"pagination,omitempty"`
   242  	// For keyset-based paginated result sets, sort order (`"asc"`` or `"desc"`)
   243  	Sort string `url:"sort,omitempty" json:"sort,omitempty"`
   244  }
   245  
   246  // RateLimiter describes the interface that all (custom) rate limiters must implement.
   247  type RateLimiter interface {
   248  	Wait(context.Context) error
   249  }
   250  
   251  // NewClient returns a new GitLab API client. To use API methods which require
   252  // authentication, provide a valid private or personal token.
   253  func NewClient(token string, options ...ClientOptionFunc) (*Client, error) {
   254  	client, err := newClient(options...)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  	client.authType = PrivateToken
   259  	client.token = token
   260  	return client, nil
   261  }
   262  
   263  // NewBasicAuthClient returns a new GitLab API client. To use API methods which
   264  // require authentication, provide a valid username and password.
   265  func NewBasicAuthClient(username, password string, options ...ClientOptionFunc) (*Client, error) {
   266  	client, err := newClient(options...)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	client.authType = BasicAuth
   272  	client.username = username
   273  	client.password = password
   274  
   275  	return client, nil
   276  }
   277  
   278  // NewJobClient returns a new GitLab API client. To use API methods which require
   279  // authentication, provide a valid job token.
   280  func NewJobClient(token string, options ...ClientOptionFunc) (*Client, error) {
   281  	client, err := newClient(options...)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	client.authType = JobToken
   286  	client.token = token
   287  	return client, nil
   288  }
   289  
   290  // NewOAuthClient returns a new GitLab API client. To use API methods which
   291  // require authentication, provide a valid oauth token.
   292  func NewOAuthClient(token string, options ...ClientOptionFunc) (*Client, error) {
   293  	client, err := newClient(options...)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	client.authType = OAuthToken
   298  	client.token = token
   299  	return client, nil
   300  }
   301  
   302  func newClient(options ...ClientOptionFunc) (*Client, error) {
   303  	c := &Client{UserAgent: userAgent}
   304  
   305  	// Configure the HTTP client.
   306  	c.client = &retryablehttp.Client{
   307  		Backoff:      c.retryHTTPBackoff,
   308  		CheckRetry:   c.retryHTTPCheck,
   309  		ErrorHandler: retryablehttp.PassthroughErrorHandler,
   310  		HTTPClient:   cleanhttp.DefaultPooledClient(),
   311  		RetryWaitMin: 100 * time.Millisecond,
   312  		RetryWaitMax: 400 * time.Millisecond,
   313  		RetryMax:     5,
   314  	}
   315  
   316  	// Set the default base URL.
   317  	c.setBaseURL(defaultBaseURL)
   318  
   319  	// Apply any given client options.
   320  	for _, fn := range options {
   321  		if fn == nil {
   322  			continue
   323  		}
   324  		if err := fn(c); err != nil {
   325  			return nil, err
   326  		}
   327  	}
   328  
   329  	// If no custom limiter was set using a client option, configure
   330  	// the default rate limiter with values that implicitly disable
   331  	// rate limiting until an initial HTTP call is done and we can
   332  	// use the headers to try and properly configure the limiter.
   333  	if c.limiter == nil {
   334  		c.limiter = rate.NewLimiter(rate.Inf, 0)
   335  	}
   336  
   337  	// Create the internal timeStats service.
   338  	timeStats := &timeStatsService{client: c}
   339  
   340  	// Create all the public services.
   341  	c.AccessRequests = &AccessRequestsService{client: c}
   342  	c.Appearance = &AppearanceService{client: c}
   343  	c.Applications = &ApplicationsService{client: c}
   344  	c.AuditEvents = &AuditEventsService{client: c}
   345  	c.Avatar = &AvatarRequestsService{client: c}
   346  	c.AwardEmoji = &AwardEmojiService{client: c}
   347  	c.Boards = &IssueBoardsService{client: c}
   348  	c.Branches = &BranchesService{client: c}
   349  	c.BroadcastMessage = &BroadcastMessagesService{client: c}
   350  	c.CIYMLTemplate = &CIYMLTemplatesService{client: c}
   351  	c.ClusterAgents = &ClusterAgentsService{client: c}
   352  	c.Commits = &CommitsService{client: c}
   353  	c.ContainerRegistry = &ContainerRegistryService{client: c}
   354  	c.CustomAttribute = &CustomAttributesService{client: c}
   355  	c.DeployKeys = &DeployKeysService{client: c}
   356  	c.DeployTokens = &DeployTokensService{client: c}
   357  	c.DeploymentMergeRequests = &DeploymentMergeRequestsService{client: c}
   358  	c.Deployments = &DeploymentsService{client: c}
   359  	c.Discussions = &DiscussionsService{client: c}
   360  	c.DockerfileTemplate = &DockerfileTemplatesService{client: c}
   361  	c.DraftNotes = &DraftNotesService{client: c}
   362  	c.Environments = &EnvironmentsService{client: c}
   363  	c.EpicIssues = &EpicIssuesService{client: c}
   364  	c.Epics = &EpicsService{client: c}
   365  	c.ErrorTracking = &ErrorTrackingService{client: c}
   366  	c.Events = &EventsService{client: c}
   367  	c.ExternalStatusChecks = &ExternalStatusChecksService{client: c}
   368  	c.Features = &FeaturesService{client: c}
   369  	c.FreezePeriods = &FreezePeriodsService{client: c}
   370  	c.GenericPackages = &GenericPackagesService{client: c}
   371  	c.GeoNodes = &GeoNodesService{client: c}
   372  	c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c}
   373  	c.GroupAccessTokens = &GroupAccessTokensService{client: c}
   374  	c.GroupBadges = &GroupBadgesService{client: c}
   375  	c.GroupCluster = &GroupClustersService{client: c}
   376  	c.GroupEpicBoards = &GroupEpicBoardsService{client: c}
   377  	c.GroupImportExport = &GroupImportExportService{client: c}
   378  	c.GroupIssueBoards = &GroupIssueBoardsService{client: c}
   379  	c.GroupIterations = &GroupIterationsService{client: c}
   380  	c.GroupLabels = &GroupLabelsService{client: c}
   381  	c.GroupMembers = &GroupMembersService{client: c}
   382  	c.GroupMilestones = &GroupMilestonesService{client: c}
   383  	c.GroupProtectedEnvironments = &GroupProtectedEnvironmentsService{client: c}
   384  	c.GroupRepositoryStorageMove = &GroupRepositoryStorageMoveService{client: c}
   385  	c.GroupSSHCertificates = &GroupSSHCertificatesService{client: c}
   386  	c.GroupVariables = &GroupVariablesService{client: c}
   387  	c.GroupWikis = &GroupWikisService{client: c}
   388  	c.Groups = &GroupsService{client: c}
   389  	c.InstanceCluster = &InstanceClustersService{client: c}
   390  	c.InstanceVariables = &InstanceVariablesService{client: c}
   391  	c.Invites = &InvitesService{client: c}
   392  	c.IssueLinks = &IssueLinksService{client: c}
   393  	c.Issues = &IssuesService{client: c, timeStats: timeStats}
   394  	c.IssuesStatistics = &IssuesStatisticsService{client: c}
   395  	c.Jobs = &JobsService{client: c}
   396  	c.JobTokenScope = &JobTokenScopeService{client: c}
   397  	c.Keys = &KeysService{client: c}
   398  	c.Labels = &LabelsService{client: c}
   399  	c.License = &LicenseService{client: c}
   400  	c.LicenseTemplates = &LicenseTemplatesService{client: c}
   401  	c.ManagedLicenses = &ManagedLicensesService{client: c}
   402  	c.Markdown = &MarkdownService{client: c}
   403  	c.MemberRolesService = &MemberRolesService{client: c}
   404  	c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c}
   405  	c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats}
   406  	c.MergeTrains = &MergeTrainsService{client: c}
   407  	c.Metadata = &MetadataService{client: c}
   408  	c.Milestones = &MilestonesService{client: c}
   409  	c.Namespaces = &NamespacesService{client: c}
   410  	c.Notes = &NotesService{client: c}
   411  	c.NotificationSettings = &NotificationSettingsService{client: c}
   412  	c.Packages = &PackagesService{client: c}
   413  	c.Pages = &PagesService{client: c}
   414  	c.PagesDomains = &PagesDomainsService{client: c}
   415  	c.PersonalAccessTokens = &PersonalAccessTokensService{client: c}
   416  	c.PipelineSchedules = &PipelineSchedulesService{client: c}
   417  	c.PipelineTriggers = &PipelineTriggersService{client: c}
   418  	c.Pipelines = &PipelinesService{client: c}
   419  	c.PlanLimits = &PlanLimitsService{client: c}
   420  	c.ProjectAccessTokens = &ProjectAccessTokensService{client: c}
   421  	c.ProjectBadges = &ProjectBadgesService{client: c}
   422  	c.ProjectCluster = &ProjectClustersService{client: c}
   423  	c.ProjectFeatureFlags = &ProjectFeatureFlagService{client: c}
   424  	c.ProjectImportExport = &ProjectImportExportService{client: c}
   425  	c.ProjectIterations = &ProjectIterationsService{client: c}
   426  	c.ProjectMembers = &ProjectMembersService{client: c}
   427  	c.ProjectMirrors = &ProjectMirrorService{client: c}
   428  	c.ProjectRepositoryStorageMove = &ProjectRepositoryStorageMoveService{client: c}
   429  	c.ProjectSnippets = &ProjectSnippetsService{client: c}
   430  	c.ProjectTemplates = &ProjectTemplatesService{client: c}
   431  	c.ProjectVariables = &ProjectVariablesService{client: c}
   432  	c.ProjectVulnerabilities = &ProjectVulnerabilitiesService{client: c}
   433  	c.Projects = &ProjectsService{client: c}
   434  	c.ProtectedBranches = &ProtectedBranchesService{client: c}
   435  	c.ProtectedEnvironments = &ProtectedEnvironmentsService{client: c}
   436  	c.ProtectedTags = &ProtectedTagsService{client: c}
   437  	c.ReleaseLinks = &ReleaseLinksService{client: c}
   438  	c.Releases = &ReleasesService{client: c}
   439  	c.Repositories = &RepositoriesService{client: c}
   440  	c.RepositoryFiles = &RepositoryFilesService{client: c}
   441  	c.RepositorySubmodules = &RepositorySubmodulesService{client: c}
   442  	c.ResourceIterationEvents = &ResourceIterationEventsService{client: c}
   443  	c.ResourceLabelEvents = &ResourceLabelEventsService{client: c}
   444  	c.ResourceMilestoneEvents = &ResourceMilestoneEventsService{client: c}
   445  	c.ResourceStateEvents = &ResourceStateEventsService{client: c}
   446  	c.ResourceWeightEvents = &ResourceWeightEventsService{client: c}
   447  	c.Runners = &RunnersService{client: c}
   448  	c.Search = &SearchService{client: c}
   449  	c.Services = &ServicesService{client: c}
   450  	c.Settings = &SettingsService{client: c}
   451  	c.Sidekiq = &SidekiqService{client: c}
   452  	c.Snippets = &SnippetsService{client: c}
   453  	c.SnippetRepositoryStorageMove = &SnippetRepositoryStorageMoveService{client: c}
   454  	c.SystemHooks = &SystemHooksService{client: c}
   455  	c.Tags = &TagsService{client: c}
   456  	c.Todos = &TodosService{client: c}
   457  	c.Topics = &TopicsService{client: c}
   458  	c.Users = &UsersService{client: c}
   459  	c.Validate = &ValidateService{client: c}
   460  	c.Version = &VersionService{client: c}
   461  	c.Wikis = &WikisService{client: c}
   462  
   463  	return c, nil
   464  }
   465  
   466  // retryHTTPCheck provides a callback for Client.CheckRetry which
   467  // will retry both rate limit (429) and server (>= 500) errors.
   468  func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
   469  	if ctx.Err() != nil {
   470  		return false, ctx.Err()
   471  	}
   472  	if err != nil {
   473  		return false, err
   474  	}
   475  	if !c.disableRetries && (resp.StatusCode == 429 || resp.StatusCode >= 500) {
   476  		return true, nil
   477  	}
   478  	return false, nil
   479  }
   480  
   481  // retryHTTPBackoff provides a generic callback for Client.Backoff which
   482  // will pass through all calls based on the status code of the response.
   483  func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
   484  	// Use the rate limit backoff function when we are rate limited.
   485  	if resp != nil && resp.StatusCode == 429 {
   486  		return rateLimitBackoff(min, max, attemptNum, resp)
   487  	}
   488  
   489  	// Set custom duration's when we experience a service interruption.
   490  	min = 700 * time.Millisecond
   491  	max = 900 * time.Millisecond
   492  
   493  	return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
   494  }
   495  
   496  // rateLimitBackoff provides a callback for Client.Backoff which will use the
   497  // RateLimit-Reset header to determine the time to wait. We add some jitter
   498  // to prevent a thundering herd.
   499  //
   500  // min and max are mainly used for bounding the jitter that will be added to
   501  // the reset time retrieved from the headers. But if the final wait time is
   502  // less then min, min will be used instead.
   503  func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
   504  	// rnd is used to generate pseudo-random numbers.
   505  	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
   506  
   507  	// First create some jitter bounded by the min and max durations.
   508  	jitter := time.Duration(rnd.Float64() * float64(max-min))
   509  
   510  	if resp != nil {
   511  		if v := resp.Header.Get(headerRateReset); v != "" {
   512  			if reset, _ := strconv.ParseInt(v, 10, 64); reset > 0 {
   513  				// Only update min if the given time to wait is longer.
   514  				if wait := time.Until(time.Unix(reset, 0)); wait > min {
   515  					min = wait
   516  				}
   517  			}
   518  		}
   519  	}
   520  
   521  	return min + jitter
   522  }
   523  
   524  // configureLimiter configures the rate limiter.
   525  func (c *Client) configureLimiter(ctx context.Context, headers http.Header) {
   526  	if v := headers.Get(headerRateLimit); v != "" {
   527  		if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
   528  			// The rate limit is based on requests per minute, so for our limiter to
   529  			// work correctly we divide the limit by 60 to get the limit per second.
   530  			rateLimit /= 60
   531  
   532  			// Configure the limit and burst using a split of 2/3 for the limit and
   533  			// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
   534  			// calls before the limiter kicks in. The remaining calls will then be
   535  			// spread out evenly using intervals of time.Second / limit which should
   536  			// prevent hitting the rate limit.
   537  			limit := rate.Limit(rateLimit * 0.66)
   538  			burst := int(rateLimit * 0.33)
   539  
   540  			// Need at least one allowed to burst or x/time will throw an error
   541  			if burst == 0 {
   542  				burst = 1
   543  			}
   544  
   545  			// Create a new limiter using the calculated values.
   546  			c.limiter = rate.NewLimiter(limit, burst)
   547  
   548  			// Call the limiter once as we have already made a request
   549  			// to get the headers and the limiter is not aware of this.
   550  			c.limiter.Wait(ctx)
   551  		}
   552  	}
   553  }
   554  
   555  // BaseURL return a copy of the baseURL.
   556  func (c *Client) BaseURL() *url.URL {
   557  	u := *c.baseURL
   558  	return &u
   559  }
   560  
   561  // setBaseURL sets the base URL for API requests to a custom endpoint.
   562  func (c *Client) setBaseURL(urlStr string) error {
   563  	// Make sure the given URL end with a slash
   564  	if !strings.HasSuffix(urlStr, "/") {
   565  		urlStr += "/"
   566  	}
   567  
   568  	baseURL, err := url.Parse(urlStr)
   569  	if err != nil {
   570  		return err
   571  	}
   572  
   573  	if !strings.HasSuffix(baseURL.Path, apiVersionPath) {
   574  		baseURL.Path += apiVersionPath
   575  	}
   576  
   577  	// Update the base URL of the client.
   578  	c.baseURL = baseURL
   579  
   580  	return nil
   581  }
   582  
   583  // NewRequest creates a new API request. The method expects a relative URL
   584  // path that will be resolved relative to the base URL of the Client.
   585  // Relative URL paths should always be specified without a preceding slash.
   586  // If specified, the value pointed to by body is JSON encoded and included
   587  // as the request body.
   588  func (c *Client) NewRequest(method, path string, opt interface{}, options []RequestOptionFunc) (*retryablehttp.Request, error) {
   589  	u := *c.baseURL
   590  	unescaped, err := url.PathUnescape(path)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  
   595  	// Set the encoded path data
   596  	u.RawPath = c.baseURL.Path + path
   597  	u.Path = c.baseURL.Path + unescaped
   598  
   599  	// Create a request specific headers map.
   600  	reqHeaders := make(http.Header)
   601  	reqHeaders.Set("Accept", "application/json")
   602  
   603  	if c.UserAgent != "" {
   604  		reqHeaders.Set("User-Agent", c.UserAgent)
   605  	}
   606  
   607  	var body interface{}
   608  	switch {
   609  	case method == http.MethodPatch || method == http.MethodPost || method == http.MethodPut:
   610  		reqHeaders.Set("Content-Type", "application/json")
   611  
   612  		if opt != nil {
   613  			body, err = json.Marshal(opt)
   614  			if err != nil {
   615  				return nil, err
   616  			}
   617  		}
   618  	case opt != nil:
   619  		q, err := query.Values(opt)
   620  		if err != nil {
   621  			return nil, err
   622  		}
   623  		u.RawQuery = q.Encode()
   624  	}
   625  
   626  	req, err := retryablehttp.NewRequest(method, u.String(), body)
   627  	if err != nil {
   628  		return nil, err
   629  	}
   630  
   631  	for _, fn := range append(c.defaultRequestOptions, options...) {
   632  		if fn == nil {
   633  			continue
   634  		}
   635  		if err := fn(req); err != nil {
   636  			return nil, err
   637  		}
   638  	}
   639  
   640  	// Set the request specific headers.
   641  	for k, v := range reqHeaders {
   642  		req.Header[k] = v
   643  	}
   644  
   645  	return req, nil
   646  }
   647  
   648  // UploadRequest creates an API request for uploading a file. The method
   649  // expects a relative URL path that will be resolved relative to the base
   650  // URL of the Client. Relative URL paths should always be specified without
   651  // a preceding slash. If specified, the value pointed to by body is JSON
   652  // encoded and included as the request body.
   653  func (c *Client) UploadRequest(method, path string, content io.Reader, filename string, uploadType UploadType, opt interface{}, options []RequestOptionFunc) (*retryablehttp.Request, error) {
   654  	u := *c.baseURL
   655  	unescaped, err := url.PathUnescape(path)
   656  	if err != nil {
   657  		return nil, err
   658  	}
   659  
   660  	// Set the encoded path data
   661  	u.RawPath = c.baseURL.Path + path
   662  	u.Path = c.baseURL.Path + unescaped
   663  
   664  	// Create a request specific headers map.
   665  	reqHeaders := make(http.Header)
   666  	reqHeaders.Set("Accept", "application/json")
   667  
   668  	if c.UserAgent != "" {
   669  		reqHeaders.Set("User-Agent", c.UserAgent)
   670  	}
   671  
   672  	b := new(bytes.Buffer)
   673  	w := multipart.NewWriter(b)
   674  
   675  	fw, err := w.CreateFormFile(string(uploadType), filename)
   676  	if err != nil {
   677  		return nil, err
   678  	}
   679  
   680  	if _, err := io.Copy(fw, content); err != nil {
   681  		return nil, err
   682  	}
   683  
   684  	if opt != nil {
   685  		fields, err := query.Values(opt)
   686  		if err != nil {
   687  			return nil, err
   688  		}
   689  		for name := range fields {
   690  			if err = w.WriteField(name, fmt.Sprintf("%v", fields.Get(name))); err != nil {
   691  				return nil, err
   692  			}
   693  		}
   694  	}
   695  
   696  	if err = w.Close(); err != nil {
   697  		return nil, err
   698  	}
   699  
   700  	reqHeaders.Set("Content-Type", w.FormDataContentType())
   701  
   702  	req, err := retryablehttp.NewRequest(method, u.String(), b)
   703  	if err != nil {
   704  		return nil, err
   705  	}
   706  
   707  	for _, fn := range append(c.defaultRequestOptions, options...) {
   708  		if fn == nil {
   709  			continue
   710  		}
   711  		if err := fn(req); err != nil {
   712  			return nil, err
   713  		}
   714  	}
   715  
   716  	// Set the request specific headers.
   717  	for k, v := range reqHeaders {
   718  		req.Header[k] = v
   719  	}
   720  
   721  	return req, nil
   722  }
   723  
   724  // Response is a GitLab API response. This wraps the standard http.Response
   725  // returned from GitLab and provides convenient access to things like
   726  // pagination links.
   727  type Response struct {
   728  	*http.Response
   729  
   730  	// Fields used for offset-based pagination.
   731  	TotalItems   int
   732  	TotalPages   int
   733  	ItemsPerPage int
   734  	CurrentPage  int
   735  	NextPage     int
   736  	PreviousPage int
   737  
   738  	// Fields used for keyset-based pagination.
   739  	PreviousLink string
   740  	NextLink     string
   741  	FirstLink    string
   742  	LastLink     string
   743  }
   744  
   745  // newResponse creates a new Response for the provided http.Response.
   746  func newResponse(r *http.Response) *Response {
   747  	response := &Response{Response: r}
   748  	response.populatePageValues()
   749  	response.populateLinkValues()
   750  	return response
   751  }
   752  
   753  const (
   754  	// Headers used for offset-based pagination.
   755  	xTotal      = "X-Total"
   756  	xTotalPages = "X-Total-Pages"
   757  	xPerPage    = "X-Per-Page"
   758  	xPage       = "X-Page"
   759  	xNextPage   = "X-Next-Page"
   760  	xPrevPage   = "X-Prev-Page"
   761  
   762  	// Headers used for keyset-based pagination.
   763  	linkPrev  = "prev"
   764  	linkNext  = "next"
   765  	linkFirst = "first"
   766  	linkLast  = "last"
   767  )
   768  
   769  // populatePageValues parses the HTTP Link response headers and populates the
   770  // various pagination link values in the Response.
   771  func (r *Response) populatePageValues() {
   772  	if totalItems := r.Header.Get(xTotal); totalItems != "" {
   773  		r.TotalItems, _ = strconv.Atoi(totalItems)
   774  	}
   775  	if totalPages := r.Header.Get(xTotalPages); totalPages != "" {
   776  		r.TotalPages, _ = strconv.Atoi(totalPages)
   777  	}
   778  	if itemsPerPage := r.Header.Get(xPerPage); itemsPerPage != "" {
   779  		r.ItemsPerPage, _ = strconv.Atoi(itemsPerPage)
   780  	}
   781  	if currentPage := r.Header.Get(xPage); currentPage != "" {
   782  		r.CurrentPage, _ = strconv.Atoi(currentPage)
   783  	}
   784  	if nextPage := r.Header.Get(xNextPage); nextPage != "" {
   785  		r.NextPage, _ = strconv.Atoi(nextPage)
   786  	}
   787  	if previousPage := r.Header.Get(xPrevPage); previousPage != "" {
   788  		r.PreviousPage, _ = strconv.Atoi(previousPage)
   789  	}
   790  }
   791  
   792  func (r *Response) populateLinkValues() {
   793  	if link := r.Header.Get("Link"); link != "" {
   794  		for _, link := range strings.Split(link, ",") {
   795  			parts := strings.Split(link, ";")
   796  			if len(parts) < 2 {
   797  				continue
   798  			}
   799  
   800  			linkType := strings.Trim(strings.Split(parts[1], "=")[1], "\"")
   801  			linkValue := strings.Trim(parts[0], "< >")
   802  
   803  			switch linkType {
   804  			case linkPrev:
   805  				r.PreviousLink = linkValue
   806  			case linkNext:
   807  				r.NextLink = linkValue
   808  			case linkFirst:
   809  				r.FirstLink = linkValue
   810  			case linkLast:
   811  				r.LastLink = linkValue
   812  			}
   813  		}
   814  	}
   815  }
   816  
   817  // Do sends an API request and returns the API response. The API response is
   818  // JSON decoded and stored in the value pointed to by v, or returned as an
   819  // error if an API error has occurred. If v implements the io.Writer
   820  // interface, the raw response body will be written to v, without attempting to
   821  // first decode it.
   822  func (c *Client) Do(req *retryablehttp.Request, v interface{}) (*Response, error) {
   823  	// Wait will block until the limiter can obtain a new token.
   824  	err := c.limiter.Wait(req.Context())
   825  	if err != nil {
   826  		return nil, err
   827  	}
   828  
   829  	// Set the correct authentication header. If using basic auth, then check
   830  	// if we already have a token and if not first authenticate and get one.
   831  	var basicAuthToken string
   832  	switch c.authType {
   833  	case BasicAuth:
   834  		c.tokenLock.RLock()
   835  		basicAuthToken = c.token
   836  		c.tokenLock.RUnlock()
   837  		if basicAuthToken == "" {
   838  			// If we don't have a token yet, we first need to request one.
   839  			basicAuthToken, err = c.requestOAuthToken(req.Context(), basicAuthToken)
   840  			if err != nil {
   841  				return nil, err
   842  			}
   843  		}
   844  		req.Header.Set("Authorization", "Bearer "+basicAuthToken)
   845  	case JobToken:
   846  		if values := req.Header.Values("JOB-TOKEN"); len(values) == 0 {
   847  			req.Header.Set("JOB-TOKEN", c.token)
   848  		}
   849  	case OAuthToken:
   850  		if values := req.Header.Values("Authorization"); len(values) == 0 {
   851  			req.Header.Set("Authorization", "Bearer "+c.token)
   852  		}
   853  	case PrivateToken:
   854  		if values := req.Header.Values("PRIVATE-TOKEN"); len(values) == 0 {
   855  			req.Header.Set("PRIVATE-TOKEN", c.token)
   856  		}
   857  	}
   858  
   859  	resp, err := c.client.Do(req)
   860  	if err != nil {
   861  		return nil, err
   862  	}
   863  
   864  	if resp.StatusCode == http.StatusUnauthorized && c.authType == BasicAuth {
   865  		resp.Body.Close()
   866  		// The token most likely expired, so we need to request a new one and try again.
   867  		if _, err := c.requestOAuthToken(req.Context(), basicAuthToken); err != nil {
   868  			return nil, err
   869  		}
   870  		return c.Do(req, v)
   871  	}
   872  	defer resp.Body.Close()
   873  	defer io.Copy(io.Discard, resp.Body)
   874  
   875  	// If not yet configured, try to configure the rate limiter
   876  	// using the response headers we just received. Fail silently
   877  	// so the limiter will remain disabled in case of an error.
   878  	c.configureLimiterOnce.Do(func() { c.configureLimiter(req.Context(), resp.Header) })
   879  
   880  	response := newResponse(resp)
   881  
   882  	err = CheckResponse(resp)
   883  	if err != nil {
   884  		// Even though there was an error, we still return the response
   885  		// in case the caller wants to inspect it further.
   886  		return response, err
   887  	}
   888  
   889  	if v != nil {
   890  		if w, ok := v.(io.Writer); ok {
   891  			_, err = io.Copy(w, resp.Body)
   892  		} else {
   893  			err = json.NewDecoder(resp.Body).Decode(v)
   894  		}
   895  	}
   896  
   897  	return response, err
   898  }
   899  
   900  func (c *Client) requestOAuthToken(ctx context.Context, token string) (string, error) {
   901  	c.tokenLock.Lock()
   902  	defer c.tokenLock.Unlock()
   903  
   904  	// Return early if the token was updated while waiting for the lock.
   905  	if c.token != token {
   906  		return c.token, nil
   907  	}
   908  
   909  	config := &oauth2.Config{
   910  		Endpoint: oauth2.Endpoint{
   911  			AuthURL:  strings.TrimSuffix(c.baseURL.String(), apiVersionPath) + "oauth/authorize",
   912  			TokenURL: strings.TrimSuffix(c.baseURL.String(), apiVersionPath) + "oauth/token",
   913  		},
   914  	}
   915  
   916  	ctx = context.WithValue(ctx, oauth2.HTTPClient, c.client.HTTPClient)
   917  	t, err := config.PasswordCredentialsToken(ctx, c.username, c.password)
   918  	if err != nil {
   919  		return "", err
   920  	}
   921  	c.token = t.AccessToken
   922  
   923  	return c.token, nil
   924  }
   925  
   926  // Helper function to accept and format both the project ID or name as project
   927  // identifier for all API calls.
   928  func parseID(id interface{}) (string, error) {
   929  	switch v := id.(type) {
   930  	case int:
   931  		return strconv.Itoa(v), nil
   932  	case string:
   933  		return v, nil
   934  	default:
   935  		return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id)
   936  	}
   937  }
   938  
   939  // Helper function to escape a project identifier.
   940  func PathEscape(s string) string {
   941  	return strings.ReplaceAll(url.PathEscape(s), ".", "%2E")
   942  }
   943  
   944  // An ErrorResponse reports one or more errors caused by an API request.
   945  //
   946  // GitLab API docs:
   947  // https://docs.gitlab.com/ee/api/index.html#data-validation-and-error-reporting
   948  type ErrorResponse struct {
   949  	Body     []byte
   950  	Response *http.Response
   951  	Message  string
   952  }
   953  
   954  func (e *ErrorResponse) Error() string {
   955  	path, _ := url.QueryUnescape(e.Response.Request.URL.Path)
   956  	u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path)
   957  	return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message)
   958  }
   959  
   960  // CheckResponse checks the API response for errors, and returns them if present.
   961  func CheckResponse(r *http.Response) error {
   962  	switch r.StatusCode {
   963  	case 200, 201, 202, 204, 304:
   964  		return nil
   965  	}
   966  
   967  	errorResponse := &ErrorResponse{Response: r}
   968  	data, err := io.ReadAll(r.Body)
   969  	if err == nil && data != nil {
   970  		errorResponse.Body = data
   971  
   972  		var raw interface{}
   973  		if err := json.Unmarshal(data, &raw); err != nil {
   974  			errorResponse.Message = fmt.Sprintf("failed to parse unknown error format: %s", data)
   975  		} else {
   976  			errorResponse.Message = parseError(raw)
   977  		}
   978  	}
   979  
   980  	return errorResponse
   981  }
   982  
   983  // Format:
   984  //
   985  //	{
   986  //	    "message": {
   987  //	        "<property-name>": [
   988  //	            "<error-message>",
   989  //	            "<error-message>",
   990  //	            ...
   991  //	        ],
   992  //	        "<embed-entity>": {
   993  //	            "<property-name>": [
   994  //	                "<error-message>",
   995  //	                "<error-message>",
   996  //	                ...
   997  //	            ],
   998  //	        }
   999  //	    },
  1000  //	    "error": "<error-message>"
  1001  //	}
  1002  func parseError(raw interface{}) string {
  1003  	switch raw := raw.(type) {
  1004  	case string:
  1005  		return raw
  1006  
  1007  	case []interface{}:
  1008  		var errs []string
  1009  		for _, v := range raw {
  1010  			errs = append(errs, parseError(v))
  1011  		}
  1012  		return fmt.Sprintf("[%s]", strings.Join(errs, ", "))
  1013  
  1014  	case map[string]interface{}:
  1015  		var errs []string
  1016  		for k, v := range raw {
  1017  			errs = append(errs, fmt.Sprintf("{%s: %s}", k, parseError(v)))
  1018  		}
  1019  		sort.Strings(errs)
  1020  		return strings.Join(errs, ", ")
  1021  
  1022  	default:
  1023  		return fmt.Sprintf("failed to parse unexpected error type: %T", raw)
  1024  	}
  1025  }
  1026  

View as plain text