1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
54
55
56 type AuthType int
57
58
59
60
61 const (
62 BasicAuth AuthType = iota
63 JobToken
64 OAuthToken
65 PrivateToken
66 )
67
68
69 type Client struct {
70
71 client *retryablehttp.Client
72
73
74
75
76 baseURL *url.URL
77
78
79 disableRetries bool
80
81
82
83 configureLimiterOnce sync.Once
84
85
86 limiter RateLimiter
87
88
89 authType AuthType
90
91
92 username, password string
93
94
95 token string
96
97
98 tokenLock sync.RWMutex
99
100
101 defaultRequestOptions []RequestOptionFunc
102
103
104 UserAgent string
105
106
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
231
232 type ListOptions struct {
233
234 Page int `url:"page,omitempty" json:"page,omitempty"`
235
236 PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"`
237
238
239 OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"`
240
241 Pagination string `url:"pagination,omitempty" json:"pagination,omitempty"`
242
243 Sort string `url:"sort,omitempty" json:"sort,omitempty"`
244 }
245
246
247 type RateLimiter interface {
248 Wait(context.Context) error
249 }
250
251
252
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
264
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
279
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
291
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
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
317 c.setBaseURL(defaultBaseURL)
318
319
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
330
331
332
333 if c.limiter == nil {
334 c.limiter = rate.NewLimiter(rate.Inf, 0)
335 }
336
337
338 timeStats := &timeStatsService{client: c}
339
340
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
467
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
482
483 func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
484
485 if resp != nil && resp.StatusCode == 429 {
486 return rateLimitBackoff(min, max, attemptNum, resp)
487 }
488
489
490 min = 700 * time.Millisecond
491 max = 900 * time.Millisecond
492
493 return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp)
494 }
495
496
497
498
499
500
501
502
503 func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
504
505 rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
506
507
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
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
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
529
530 rateLimit /= 60
531
532
533
534
535
536
537 limit := rate.Limit(rateLimit * 0.66)
538 burst := int(rateLimit * 0.33)
539
540
541 if burst == 0 {
542 burst = 1
543 }
544
545
546 c.limiter = rate.NewLimiter(limit, burst)
547
548
549
550 c.limiter.Wait(ctx)
551 }
552 }
553 }
554
555
556 func (c *Client) BaseURL() *url.URL {
557 u := *c.baseURL
558 return &u
559 }
560
561
562 func (c *Client) setBaseURL(urlStr string) error {
563
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
578 c.baseURL = baseURL
579
580 return nil
581 }
582
583
584
585
586
587
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
596 u.RawPath = c.baseURL.Path + path
597 u.Path = c.baseURL.Path + unescaped
598
599
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
641 for k, v := range reqHeaders {
642 req.Header[k] = v
643 }
644
645 return req, nil
646 }
647
648
649
650
651
652
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
661 u.RawPath = c.baseURL.Path + path
662 u.Path = c.baseURL.Path + unescaped
663
664
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
717 for k, v := range reqHeaders {
718 req.Header[k] = v
719 }
720
721 return req, nil
722 }
723
724
725
726
727 type Response struct {
728 *http.Response
729
730
731 TotalItems int
732 TotalPages int
733 ItemsPerPage int
734 CurrentPage int
735 NextPage int
736 PreviousPage int
737
738
739 PreviousLink string
740 NextLink string
741 FirstLink string
742 LastLink string
743 }
744
745
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
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
763 linkPrev = "prev"
764 linkNext = "next"
765 linkFirst = "first"
766 linkLast = "last"
767 )
768
769
770
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
818
819
820
821
822 func (c *Client) Do(req *retryablehttp.Request, v interface{}) (*Response, error) {
823
824 err := c.limiter.Wait(req.Context())
825 if err != nil {
826 return nil, err
827 }
828
829
830
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
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
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
876
877
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
885
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
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
927
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
940 func PathEscape(s string) string {
941 return strings.ReplaceAll(url.PathEscape(s), ".", "%2E")
942 }
943
944
945
946
947
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
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
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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