Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1140 lines
34 KiB
1140 lines
34 KiB
// |
|
// Copyright 2017, Sander van Harmelen |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
// |
|
|
|
// Package gitlab implements a GitLab API client. |
|
package gitlab |
|
|
|
import ( |
|
"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. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/ |
|
type authType int |
|
|
|
// List of available authentication types. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/ |
|
const ( |
|
basicAuth authType = iota |
|
oAuthToken |
|
privateToken |
|
) |
|
|
|
// AccessLevelValue represents a permission level within GitLab. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html |
|
type AccessLevelValue int |
|
|
|
// List of available access levels |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html |
|
const ( |
|
NoPermissions AccessLevelValue = 0 |
|
GuestPermissions AccessLevelValue = 10 |
|
ReporterPermissions AccessLevelValue = 20 |
|
DeveloperPermissions AccessLevelValue = 30 |
|
MaintainerPermissions AccessLevelValue = 40 |
|
OwnerPermissions AccessLevelValue = 50 |
|
|
|
// These are deprecated and should be removed in a future version |
|
MasterPermissions AccessLevelValue = 40 |
|
OwnerPermission AccessLevelValue = 50 |
|
) |
|
|
|
// BuildStateValue represents a GitLab build state. |
|
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" |
|
Canceled BuildStateValue = "canceled" |
|
Skipped BuildStateValue = "skipped" |
|
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 |
|
|
|
// ISO 8601 date format |
|
const iso8601 = "2006-01-02" |
|
|
|
// MarshalJSON implements the json.Marshaler interface |
|
func (t ISOTime) MarshalJSON() ([]byte, error) { |
|
if y := time.Time(t).Year(); y < 0 || y >= 10000 { |
|
// ISO 8901 uses 4 digits for the years |
|
return nil, errors.New("json: ISOTime year outside of range [0,9999]") |
|
} |
|
|
|
b := make([]byte, 0, len(iso8601)+2) |
|
b = append(b, '"') |
|
b = time.Time(t).AppendFormat(b, iso8601) |
|
b = append(b, '"') |
|
|
|
return b, nil |
|
} |
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface |
|
func (t *ISOTime) UnmarshalJSON(data []byte) error { |
|
// Ignore null, like in the main JSON package |
|
if string(data) == "null" { |
|
return nil |
|
} |
|
|
|
isotime, err := time.Parse(`"`+iso8601+`"`, string(data)) |
|
*t = ISOTime(isotime) |
|
|
|
return err |
|
} |
|
|
|
// EncodeValues implements the query.Encoder interface |
|
func (t *ISOTime) EncodeValues(key string, v *url.Values) error { |
|
if t == nil || (time.Time(*t)).IsZero() { |
|
return nil |
|
} |
|
v.Add(key, t.String()) |
|
return nil |
|
} |
|
|
|
// String implements the Stringer interface |
|
func (t ISOTime) String() string { |
|
return time.Time(t).Format(iso8601) |
|
} |
|
|
|
// NotificationLevelValue represents a notification level. |
|
type NotificationLevelValue int |
|
|
|
// String implements the fmt.Stringer interface. |
|
func (l NotificationLevelValue) String() string { |
|
return notificationLevelNames[l] |
|
} |
|
|
|
// MarshalJSON implements the json.Marshaler interface. |
|
func (l NotificationLevelValue) MarshalJSON() ([]byte, error) { |
|
return json.Marshal(l.String()) |
|
} |
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface. |
|
func (l *NotificationLevelValue) UnmarshalJSON(data []byte) error { |
|
var raw interface{} |
|
if err := json.Unmarshal(data, &raw); err != nil { |
|
return err |
|
} |
|
|
|
switch raw := raw.(type) { |
|
case float64: |
|
*l = NotificationLevelValue(raw) |
|
case string: |
|
*l = notificationLevelTypes[raw] |
|
case nil: |
|
// No action needed. |
|
default: |
|
return fmt.Errorf("json: cannot unmarshal %T into Go value of type %T", raw, *l) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// List of valid notification levels. |
|
const ( |
|
DisabledNotificationLevel NotificationLevelValue = iota |
|
ParticipatingNotificationLevel |
|
WatchNotificationLevel |
|
GlobalNotificationLevel |
|
MentionNotificationLevel |
|
CustomNotificationLevel |
|
) |
|
|
|
var notificationLevelNames = [...]string{ |
|
"disabled", |
|
"participating", |
|
"watch", |
|
"global", |
|
"mention", |
|
"custom", |
|
} |
|
|
|
var notificationLevelTypes = map[string]NotificationLevelValue{ |
|
"disabled": DisabledNotificationLevel, |
|
"participating": ParticipatingNotificationLevel, |
|
"watch": WatchNotificationLevel, |
|
"global": GlobalNotificationLevel, |
|
"mention": MentionNotificationLevel, |
|
"custom": CustomNotificationLevel, |
|
} |
|
|
|
// VisibilityValue represents a visibility level within GitLab. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/ |
|
type VisibilityValue string |
|
|
|
// List of available visibility levels. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/ |
|
const ( |
|
PrivateVisibility VisibilityValue = "private" |
|
InternalVisibility VisibilityValue = "internal" |
|
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/ |
|
type VariableTypeValue string |
|
|
|
// List of available variable types. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/ |
|
const ( |
|
EnvVariableType VariableTypeValue = "env_var" |
|
FileVariableType VariableTypeValue = "file" |
|
) |
|
|
|
// MergeMethodValue represents a project merge type within GitLab. |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method |
|
type MergeMethodValue string |
|
|
|
// List of available merge type |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method |
|
const ( |
|
NoFastForwardMerge MergeMethodValue = "merge" |
|
FastForwardMerge MergeMethodValue = "ff" |
|
RebaseMerge MergeMethodValue = "rebase_merge" |
|
) |
|
|
|
// EventTypeValue represents actions type for contribution events |
|
type EventTypeValue string |
|
|
|
// List of available action type |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#action-types |
|
const ( |
|
CreatedEventType EventTypeValue = "created" |
|
UpdatedEventType EventTypeValue = "updated" |
|
ClosedEventType EventTypeValue = "closed" |
|
ReopenedEventType EventTypeValue = "reopened" |
|
PushedEventType EventTypeValue = "pushed" |
|
CommentedEventType EventTypeValue = "commented" |
|
MergedEventType EventTypeValue = "merged" |
|
JoinedEventType EventTypeValue = "joined" |
|
LeftEventType EventTypeValue = "left" |
|
DestroyedEventType EventTypeValue = "destroyed" |
|
ExpiredEventType EventTypeValue = "expired" |
|
) |
|
|
|
// EventTargetTypeValue represents actions type value for contribution events |
|
type EventTargetTypeValue string |
|
|
|
// List of available action type |
|
// |
|
// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#target-types |
|
const ( |
|
IssueEventTargetType EventTargetTypeValue = "issue" |
|
MilestoneEventTargetType EventTargetTypeValue = "milestone" |
|
MergeRequestEventTargetType EventTargetTypeValue = "merge_request" |
|
NoteEventTargetType EventTargetTypeValue = "note" |
|
ProjectEventTargetType EventTargetTypeValue = "project" |
|
SnippetEventTargetType EventTargetTypeValue = "snippet" |
|
UserEventTargetType EventTargetTypeValue = "user" |
|
) |
|
|
|
// A Client manages communication with the GitLab API. |
|
type Client struct { |
|
// HTTP client used to communicate with the API. |
|
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 |
|
|
|
// Username and password used for basix authentication. |
|
username, password string |
|
|
|
// Token used to make authenticated API calls. |
|
token string |
|
|
|
// User agent used when communicating with the GitLab API. |
|
UserAgent string |
|
|
|
// Services used for talking to different parts of the GitLab API. |
|
AccessRequests *AccessRequestsService |
|
Applications *ApplicationsService |
|
AwardEmoji *AwardEmojiService |
|
Boards *IssueBoardsService |
|
Branches *BranchesService |
|
BroadcastMessage *BroadcastMessagesService |
|
CIYMLTemplate *CIYMLTemplatesService |
|
Commits *CommitsService |
|
ContainerRegistry *ContainerRegistryService |
|
CustomAttribute *CustomAttributesService |
|
DeployKeys *DeployKeysService |
|
DeployTokens *DeployTokensService |
|
Deployments *DeploymentsService |
|
Discussions *DiscussionsService |
|
Environments *EnvironmentsService |
|
Epics *EpicsService |
|
Events *EventsService |
|
Features *FeaturesService |
|
GitIgnoreTemplates *GitIgnoreTemplatesService |
|
GroupBadges *GroupBadgesService |
|
GroupCluster *GroupClustersService |
|
GroupIssueBoards *GroupIssueBoardsService |
|
GroupLabels *GroupLabelsService |
|
GroupMembers *GroupMembersService |
|
GroupMilestones *GroupMilestonesService |
|
GroupVariables *GroupVariablesService |
|
Groups *GroupsService |
|
IssueLinks *IssueLinksService |
|
Issues *IssuesService |
|
Jobs *JobsService |
|
Keys *KeysService |
|
Labels *LabelsService |
|
License *LicenseService |
|
LicenseTemplates *LicenseTemplatesService |
|
MergeRequestApprovals *MergeRequestApprovalsService |
|
MergeRequests *MergeRequestsService |
|
Milestones *MilestonesService |
|
Namespaces *NamespacesService |
|
Notes *NotesService |
|
NotificationSettings *NotificationSettingsService |
|
PagesDomains *PagesDomainsService |
|
PipelineSchedules *PipelineSchedulesService |
|
PipelineTriggers *PipelineTriggersService |
|
Pipelines *PipelinesService |
|
ProjectBadges *ProjectBadgesService |
|
ProjectCluster *ProjectClustersService |
|
ProjectImportExport *ProjectImportExportService |
|
ProjectMembers *ProjectMembersService |
|
ProjectSnippets *ProjectSnippetsService |
|
ProjectVariables *ProjectVariablesService |
|
Projects *ProjectsService |
|
ProtectedBranches *ProtectedBranchesService |
|
ProtectedTags *ProtectedTagsService |
|
ReleaseLinks *ReleaseLinksService |
|
Releases *ReleasesService |
|
Repositories *RepositoriesService |
|
RepositoryFiles *RepositoryFilesService |
|
ResourceLabelEvents *ResourceLabelEventsService |
|
Runners *RunnersService |
|
Search *SearchService |
|
Services *ServicesService |
|
Settings *SettingsService |
|
Sidekiq *SidekiqService |
|
Snippets *SnippetsService |
|
SystemHooks *SystemHooksService |
|
Tags *TagsService |
|
Todos *TodosService |
|
Users *UsersService |
|
Validate *ValidateService |
|
Version *VersionService |
|
Wikis *WikisService |
|
} |
|
|
|
// ListOptions specifies the optional parameters to various List methods that |
|
// support pagination. |
|
type ListOptions struct { |
|
// For paginated result sets, page of results to retrieve. |
|
Page int `url:"page,omitempty" json:"page,omitempty"` |
|
|
|
// For paginated result sets, the number of results to include per page. |
|
PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"` |
|
} |
|
|
|
// NewClient returns a new GitLab API client. To use API methods which require |
|
// authentication, provide a valid private or personal token. |
|
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, nil |
|
} |
|
|
|
// 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 |
|
|
|
err = client.requestOAuthToken(context.Background()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
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{ |
|
AuthURL: fmt.Sprintf("%s://%s/oauth/authorize", c.BaseURL().Scheme, c.BaseURL().Host), |
|
TokenURL: fmt.Sprintf("%s://%s/oauth/token", c.BaseURL().Scheme, c.BaseURL().Host), |
|
}, |
|
} |
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.client) |
|
t, err := config.PasswordCredentialsToken(ctx, c.username, c.password) |
|
if err != nil { |
|
return err |
|
} |
|
c.token = t.AccessToken |
|
return nil |
|
} |
|
|
|
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, |
|
} |
|
|
|
// 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. |
|
timeStats := &timeStatsService{client: c} |
|
|
|
// 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} |
|
c.BroadcastMessage = &BroadcastMessagesService{client: c} |
|
c.CIYMLTemplate = &CIYMLTemplatesService{client: c} |
|
c.Commits = &CommitsService{client: c} |
|
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} |
|
c.Epics = &EpicsService{client: c} |
|
c.Events = &EventsService{client: c} |
|
c.Features = &FeaturesService{client: c} |
|
c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c} |
|
c.GroupBadges = &GroupBadgesService{client: c} |
|
c.GroupCluster = &GroupClustersService{client: c} |
|
c.GroupIssueBoards = &GroupIssueBoardsService{client: c} |
|
c.GroupLabels = &GroupLabelsService{client: c} |
|
c.GroupMembers = &GroupMembersService{client: c} |
|
c.GroupMilestones = &GroupMilestonesService{client: c} |
|
c.GroupVariables = &GroupVariablesService{client: c} |
|
c.Groups = &GroupsService{client: c} |
|
c.IssueLinks = &IssueLinksService{client: c} |
|
c.Issues = &IssuesService{client: c, timeStats: timeStats} |
|
c.Jobs = &JobsService{client: c} |
|
c.Keys = &KeysService{client: c} |
|
c.Labels = &LabelsService{client: c} |
|
c.License = &LicenseService{client: c} |
|
c.LicenseTemplates = &LicenseTemplatesService{client: c} |
|
c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c} |
|
c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats} |
|
c.Milestones = &MilestonesService{client: c} |
|
c.Namespaces = &NamespacesService{client: c} |
|
c.Notes = &NotesService{client: c} |
|
c.NotificationSettings = &NotificationSettingsService{client: c} |
|
c.PagesDomains = &PagesDomainsService{client: c} |
|
c.PipelineSchedules = &PipelineSchedulesService{client: c} |
|
c.PipelineTriggers = &PipelineTriggersService{client: c} |
|
c.Pipelines = &PipelinesService{client: c} |
|
c.ProjectBadges = &ProjectBadgesService{client: c} |
|
c.ProjectCluster = &ProjectClustersService{client: c} |
|
c.ProjectImportExport = &ProjectImportExportService{client: c} |
|
c.ProjectMembers = &ProjectMembersService{client: c} |
|
c.ProjectSnippets = &ProjectSnippetsService{client: c} |
|
c.ProjectVariables = &ProjectVariablesService{client: c} |
|
c.Projects = &ProjectsService{client: c} |
|
c.ProtectedBranches = &ProtectedBranchesService{client: c} |
|
c.ProtectedTags = &ProtectedTagsService{client: c} |
|
c.ReleaseLinks = &ReleaseLinksService{client: c} |
|
c.Releases = &ReleasesService{client: c} |
|
c.Repositories = &RepositoriesService{client: c} |
|
c.RepositoryFiles = &RepositoryFilesService{client: c} |
|
c.ResourceLabelEvents = &ResourceLabelEventsService{client: c} |
|
c.Runners = &RunnersService{client: c} |
|
c.Search = &SearchService{client: c} |
|
c.Services = &ServicesService{client: c} |
|
c.Settings = &SettingsService{client: c} |
|
c.Sidekiq = &SidekiqService{client: c} |
|
c.Snippets = &SnippetsService{client: c} |
|
c.SystemHooks = &SystemHooksService{client: c} |
|
c.Tags = &TagsService{client: c} |
|
c.Todos = &TodosService{client: c} |
|
c.Users = &UsersService{client: c} |
|
c.Validate = &ValidateService{client: c} |
|
c.Version = &VersionService{client: c} |
|
c.Wikis = &WikisService{client: 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. |
|
func (c *Client) BaseURL() *url.URL { |
|
u := *c.baseURL |
|
return &u |
|
} |
|
|
|
// 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 += "/" |
|
} |
|
|
|
baseURL, err := url.Parse(urlStr) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if !strings.HasSuffix(baseURL.Path, apiVersionPath) { |
|
baseURL.Path += apiVersionPath |
|
} |
|
|
|
// Update the base URL of the client. |
|
c.baseURL = baseURL |
|
|
|
return nil |
|
} |
|
|
|
// NewRequest creates an API request. A relative URL path can be provided in |
|
// 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 []RequestOptionFunc) (*retryablehttp.Request, error) { |
|
u := *c.baseURL |
|
unescaped, err := url.PathUnescape(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Set the encoded path data |
|
u.RawPath = c.baseURL.Path + path |
|
u.Path = c.baseURL.Path + unescaped |
|
|
|
// 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 |
|
} |
|
u.RawQuery = q.Encode() |
|
} |
|
|
|
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 |
|
} |
|
} |
|
|
|
// Set the request specific headers. |
|
for k, v := range reqHeaders { |
|
req.Header[k] = v |
|
} |
|
|
|
return req, nil |
|
} |
|
|
|
// Response is a GitLab API response. This wraps the standard http.Response |
|
// returned from GitLab and provides convenient access to things like |
|
// pagination links. |
|
type Response struct { |
|
*http.Response |
|
|
|
// These fields provide the page values for paginating through a set of |
|
// results. Any or all of these may be set to the zero value for |
|
// responses that are not part of a paginated set, or for which there |
|
// are no additional pages. |
|
TotalItems int |
|
TotalPages int |
|
ItemsPerPage int |
|
CurrentPage int |
|
NextPage int |
|
PreviousPage int |
|
} |
|
|
|
// newResponse creates a new Response for the provided http.Response. |
|
func newResponse(r *http.Response) *Response { |
|
response := &Response{Response: r} |
|
response.populatePageValues() |
|
return response |
|
} |
|
|
|
const ( |
|
xTotal = "X-Total" |
|
xTotalPages = "X-Total-Pages" |
|
xPerPage = "X-Per-Page" |
|
xPage = "X-Page" |
|
xNextPage = "X-Next-Page" |
|
xPrevPage = "X-Prev-Page" |
|
) |
|
|
|
// populatePageValues parses the HTTP Link response headers and populates the |
|
// various pagination link values in the Response. |
|
func (r *Response) populatePageValues() { |
|
if totalItems := r.Response.Header.Get(xTotal); totalItems != "" { |
|
r.TotalItems, _ = strconv.Atoi(totalItems) |
|
} |
|
if totalPages := r.Response.Header.Get(xTotalPages); totalPages != "" { |
|
r.TotalPages, _ = strconv.Atoi(totalPages) |
|
} |
|
if itemsPerPage := r.Response.Header.Get(xPerPage); itemsPerPage != "" { |
|
r.ItemsPerPage, _ = strconv.Atoi(itemsPerPage) |
|
} |
|
if currentPage := r.Response.Header.Get(xPage); currentPage != "" { |
|
r.CurrentPage, _ = strconv.Atoi(currentPage) |
|
} |
|
if nextPage := r.Response.Header.Get(xNextPage); nextPage != "" { |
|
r.NextPage, _ = strconv.Atoi(nextPage) |
|
} |
|
if previousPage := r.Response.Header.Get(xPrevPage); previousPage != "" { |
|
r.PreviousPage, _ = strconv.Atoi(previousPage) |
|
} |
|
} |
|
|
|
// Do sends an API request and returns the API response. The API response is |
|
// JSON decoded and stored in the value pointed to by v, or returned as an |
|
// 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 *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 |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode == http.StatusUnauthorized && c.authType == basicAuth { |
|
err = c.requestOAuthToken(req.Context()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return c.Do(req, v) |
|
} |
|
|
|
response := newResponse(resp) |
|
|
|
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. |
|
return response, err |
|
} |
|
|
|
if v != nil { |
|
if w, ok := v.(io.Writer); ok { |
|
_, err = io.Copy(w, resp.Body) |
|
} else { |
|
err = json.NewDecoder(resp.Body).Decode(v) |
|
} |
|
} |
|
|
|
return response, err |
|
} |
|
|
|
// Helper function to accept and format both the project ID or name as project |
|
// identifier for all API calls. |
|
func parseID(id interface{}) (string, error) { |
|
switch v := id.(type) { |
|
case int: |
|
return strconv.Itoa(v), nil |
|
case string: |
|
return v, nil |
|
default: |
|
return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id) |
|
} |
|
} |
|
|
|
// Helper function to escape a project identifier. |
|
func pathEscape(s string) string { |
|
return strings.Replace(url.PathEscape(s), ".", "%2E", -1) |
|
} |
|
|
|
// An ErrorResponse reports one or more errors caused by an API request. |
|
// |
|
// GitLab API docs: |
|
// https://docs.gitlab.com/ce/api/README.html#data-validation-and-error-reporting |
|
type ErrorResponse struct { |
|
Body []byte |
|
Response *http.Response |
|
Message string |
|
} |
|
|
|
func (e *ErrorResponse) Error() string { |
|
path, _ := url.QueryUnescape(e.Response.Request.URL.Path) |
|
u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path) |
|
return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message) |
|
} |
|
|
|
// CheckResponse checks the API response for errors, and returns them if present. |
|
func CheckResponse(r *http.Response) error { |
|
switch r.StatusCode { |
|
case 200, 201, 202, 204, 304: |
|
return nil |
|
} |
|
|
|
errorResponse := &ErrorResponse{Response: r} |
|
data, err := ioutil.ReadAll(r.Body) |
|
if err == nil && data != nil { |
|
errorResponse.Body = data |
|
|
|
var raw interface{} |
|
if err := json.Unmarshal(data, &raw); err != nil { |
|
errorResponse.Message = "failed to parse unknown error format" |
|
} else { |
|
errorResponse.Message = parseError(raw) |
|
} |
|
} |
|
|
|
return errorResponse |
|
} |
|
|
|
// Format: |
|
// { |
|
// "message": { |
|
// "<property-name>": [ |
|
// "<error-message>", |
|
// "<error-message>", |
|
// ... |
|
// ], |
|
// "<embed-entity>": { |
|
// "<property-name>": [ |
|
// "<error-message>", |
|
// "<error-message>", |
|
// ... |
|
// ], |
|
// } |
|
// }, |
|
// "error": "<error-message>" |
|
// } |
|
func parseError(raw interface{}) string { |
|
switch raw := raw.(type) { |
|
case string: |
|
return raw |
|
|
|
case []interface{}: |
|
var errs []string |
|
for _, v := range raw { |
|
errs = append(errs, parseError(v)) |
|
} |
|
return fmt.Sprintf("[%s]", strings.Join(errs, ", ")) |
|
|
|
case map[string]interface{}: |
|
var errs []string |
|
for k, v := range raw { |
|
errs = append(errs, fmt.Sprintf("{%s: %s}", k, parseError(v))) |
|
} |
|
sort.Strings(errs) |
|
return strings.Join(errs, ", ") |
|
|
|
default: |
|
return fmt.Sprintf("failed to parse unexpected error type: %T", raw) |
|
} |
|
} |
|
|
|
// 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 { |
|
p := new(bool) |
|
*p = v |
|
return p |
|
} |
|
|
|
// Int is a helper routine that allocates a new int32 value |
|
// to store v and returns a pointer to it, but unlike Int32 |
|
// its argument value is an int. |
|
func Int(v int) *int { |
|
p := new(int) |
|
*p = v |
|
return p |
|
} |
|
|
|
// String is a helper routine that allocates a new string value |
|
// to store v and returns a pointer to it. |
|
func String(v string) *string { |
|
p := new(string) |
|
*p = v |
|
return p |
|
} |
|
|
|
// Time is a helper routine that allocates a new time.Time value |
|
// to store v and returns a pointer to it. |
|
func Time(v time.Time) *time.Time { |
|
p := new(time.Time) |
|
*p = v |
|
return p |
|
} |
|
|
|
// AccessLevel is a helper routine that allocates a new AccessLevelValue |
|
// to store v and returns a pointer to it. |
|
func AccessLevel(v AccessLevelValue) *AccessLevelValue { |
|
p := new(AccessLevelValue) |
|
*p = v |
|
return p |
|
} |
|
|
|
// BuildState is a helper routine that allocates a new BuildStateValue |
|
// to store v and returns a pointer to it. |
|
func BuildState(v BuildStateValue) *BuildStateValue { |
|
p := new(BuildStateValue) |
|
*p = v |
|
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 { |
|
p := new(NotificationLevelValue) |
|
*p = v |
|
return p |
|
} |
|
|
|
// VariableType is a helper routine that allocates a new VariableTypeValue |
|
// to store v and returns a pointer to it. |
|
func VariableType(v VariableTypeValue) *VariableTypeValue { |
|
p := new(VariableTypeValue) |
|
*p = v |
|
return p |
|
} |
|
|
|
// Visibility is a helper routine that allocates a new VisibilityValue |
|
// to store v and returns a pointer to it. |
|
func Visibility(v VisibilityValue) *VisibilityValue { |
|
p := new(VisibilityValue) |
|
*p = v |
|
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 { |
|
p := new(MergeMethodValue) |
|
*p = v |
|
return p |
|
} |
|
|
|
// BoolValue is a boolean value with advanced json unmarshaling features. |
|
type BoolValue bool |
|
|
|
// UnmarshalJSON allows 1 and 0 to be considered as boolean values |
|
// Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/50122 |
|
func (t *BoolValue) UnmarshalJSON(b []byte) error { |
|
switch string(b) { |
|
case `"1"`: |
|
*t = true |
|
return nil |
|
case `"0"`: |
|
*t = false |
|
return nil |
|
default: |
|
var v bool |
|
err := json.Unmarshal(b, &v) |
|
*t = BoolValue(v) |
|
return err |
|
} |
|
}
|
|
|