Provisioning: Move repository package to provisioning app (#110228)
* Move repository package to apps
* Move operators to grafana/grafana
* Go mod tidy
* Own package by git sync team for now
* Merged
* Do not use settings in local extra
* Remove dependency on webhook extra
* Hack to work around issue with secure contracts
* Sync Go modules
* Revert "Move operators to grafana/grafana"
This reverts commit 9f19b30a2e.
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
)
|
||||
|
||||
type githubClient struct {
|
||||
gh *github.Client
|
||||
}
|
||||
|
||||
func NewClient(client *github.Client) Client {
|
||||
return &githubClient{client}
|
||||
}
|
||||
|
||||
const (
|
||||
maxCommits = 1000 // Maximum number of commits to fetch
|
||||
maxWebhooks = 100 // Maximum number of webhooks allowed per repository
|
||||
maxPRFiles = 1000 // Maximum number of files allowed in a pull request
|
||||
)
|
||||
|
||||
// Commits returns a list of commits for a given repository and branch.
|
||||
func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) {
|
||||
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) {
|
||||
return r.gh.Repositories.ListCommits(ctx, owner, repository, &github.CommitsListOptions{
|
||||
Path: path,
|
||||
SHA: branch,
|
||||
ListOptions: *opts,
|
||||
})
|
||||
}
|
||||
|
||||
commits, err := paginatedList(
|
||||
ctx,
|
||||
listFn,
|
||||
defaultListOptions(maxCommits),
|
||||
)
|
||||
if errors.Is(err, ErrTooManyItems) {
|
||||
return nil, fmt.Errorf("too many commits to fetch (more than %d)", maxCommits)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]Commit, 0, len(commits))
|
||||
for _, c := range commits {
|
||||
// FIXME: This code is a mess. I am pretty sure that we have issue in
|
||||
// some situations
|
||||
var createdAt time.Time
|
||||
var author *CommitAuthor
|
||||
if c.GetCommit().GetAuthor() != nil {
|
||||
author = &CommitAuthor{
|
||||
Name: c.GetCommit().GetAuthor().GetName(),
|
||||
Username: c.GetAuthor().GetLogin(),
|
||||
AvatarURL: c.GetAuthor().GetAvatarURL(),
|
||||
}
|
||||
|
||||
createdAt = c.GetCommit().GetAuthor().GetDate().Time
|
||||
}
|
||||
|
||||
var committer *CommitAuthor
|
||||
if c.GetCommitter() != nil {
|
||||
committer = &CommitAuthor{
|
||||
Name: c.GetCommit().GetCommitter().GetName(),
|
||||
Username: c.GetCommitter().GetLogin(),
|
||||
AvatarURL: c.GetCommitter().GetAvatarURL(),
|
||||
}
|
||||
}
|
||||
|
||||
ret = append(ret, Commit{
|
||||
Ref: c.GetSHA(),
|
||||
Message: c.GetCommit().GetMessage(),
|
||||
Author: author,
|
||||
Committer: committer,
|
||||
CreatedAt: createdAt,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) {
|
||||
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
|
||||
return r.gh.Repositories.ListHooks(ctx, owner, repository, opts)
|
||||
}
|
||||
|
||||
hooks, err := paginatedList(
|
||||
ctx,
|
||||
listFn,
|
||||
defaultListOptions(maxWebhooks),
|
||||
)
|
||||
if errors.Is(err, ErrTooManyItems) {
|
||||
return nil, fmt.Errorf("too many webhooks configured (more than %d)", maxWebhooks)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pre-allocate the result slice
|
||||
ret := make([]WebhookConfig, 0, len(hooks))
|
||||
for _, h := range hooks {
|
||||
contentType := h.GetConfig().GetContentType()
|
||||
if contentType == "" {
|
||||
contentType = "form"
|
||||
}
|
||||
|
||||
ret = append(ret, WebhookConfig{
|
||||
ID: h.GetID(),
|
||||
Events: h.Events,
|
||||
Active: h.GetActive(),
|
||||
URL: h.GetConfig().GetURL(),
|
||||
ContentType: contentType,
|
||||
// Intentionally not setting Secret.
|
||||
})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) {
|
||||
if cfg.ContentType == "" {
|
||||
cfg.ContentType = "form"
|
||||
}
|
||||
|
||||
hook := &github.Hook{
|
||||
URL: &cfg.URL,
|
||||
Events: cfg.Events,
|
||||
Active: &cfg.Active,
|
||||
Config: &github.HookConfig{
|
||||
ContentType: &cfg.ContentType,
|
||||
Secret: &cfg.Secret,
|
||||
URL: &cfg.URL,
|
||||
},
|
||||
}
|
||||
|
||||
createdHook, _, err := r.gh.Repositories.CreateHook(ctx, owner, repository, hook)
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return WebhookConfig{}, ErrServiceUnavailable
|
||||
}
|
||||
if err != nil {
|
||||
return WebhookConfig{}, err
|
||||
}
|
||||
|
||||
return WebhookConfig{
|
||||
ID: createdHook.GetID(),
|
||||
// events is not returned by GitHub.
|
||||
Events: cfg.Events,
|
||||
Active: createdHook.GetActive(),
|
||||
URL: createdHook.GetConfig().GetURL(),
|
||||
ContentType: createdHook.GetConfig().GetContentType(),
|
||||
// Secret is not returned by GitHub.
|
||||
Secret: cfg.Secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) {
|
||||
hook, _, err := r.gh.Repositories.GetHook(ctx, owner, repository, webhookID)
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return WebhookConfig{}, ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return WebhookConfig{}, ErrResourceNotFound
|
||||
}
|
||||
return WebhookConfig{}, err
|
||||
}
|
||||
|
||||
contentType := hook.GetConfig().GetContentType()
|
||||
if contentType == "" {
|
||||
// FIXME: Not sure about the value of the contentType
|
||||
// we default to form in the other ones but to JSON here
|
||||
contentType = "json"
|
||||
}
|
||||
|
||||
return WebhookConfig{
|
||||
ID: hook.GetID(),
|
||||
Events: hook.Events,
|
||||
Active: hook.GetActive(),
|
||||
URL: hook.GetConfig().GetURL(),
|
||||
ContentType: contentType,
|
||||
// Intentionally not setting Secret.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error {
|
||||
_, err := r.gh.Repositories.DeleteHook(ctx, owner, repository, webhookID)
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *githubClient) EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error {
|
||||
if cfg.ContentType == "" {
|
||||
cfg.ContentType = "form"
|
||||
}
|
||||
|
||||
hook := &github.Hook{
|
||||
URL: &cfg.URL,
|
||||
Events: cfg.Events,
|
||||
Active: &cfg.Active,
|
||||
Config: &github.HookConfig{
|
||||
ContentType: &cfg.ContentType,
|
||||
Secret: &cfg.Secret,
|
||||
URL: &cfg.URL,
|
||||
},
|
||||
}
|
||||
_, _, err := r.gh.Repositories.EditHook(ctx, owner, repository, cfg.ID, hook)
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *githubClient) ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) {
|
||||
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) {
|
||||
return r.gh.PullRequests.ListFiles(ctx, owner, repository, number, opts)
|
||||
}
|
||||
|
||||
files, err := paginatedList(
|
||||
ctx,
|
||||
listFn,
|
||||
defaultListOptions(maxPRFiles),
|
||||
)
|
||||
if errors.Is(err, ErrTooManyItems) {
|
||||
return nil, fmt.Errorf("pull request contains too many files (more than %d)", maxPRFiles)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to the interface type
|
||||
ret := make([]CommitFile, 0, len(files))
|
||||
for _, f := range files {
|
||||
ret = append(ret, f)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error {
|
||||
comment := &github.IssueComment{
|
||||
Body: &body,
|
||||
}
|
||||
|
||||
if _, _, err := r.gh.Issues.CreateComment(ctx, owner, repository, number, comment); err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listOptions represents pagination parameters for list operations
|
||||
type listOptions struct {
|
||||
github.ListOptions
|
||||
MaxItems int
|
||||
}
|
||||
|
||||
// defaultListOptions returns a ListOptions with sensible defaults
|
||||
func defaultListOptions(maxItems int) listOptions {
|
||||
return listOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: 1,
|
||||
PerPage: 100,
|
||||
},
|
||||
MaxItems: maxItems,
|
||||
}
|
||||
}
|
||||
|
||||
// paginatedList is a generic function to handle GitHub API pagination
|
||||
func paginatedList[T any](
|
||||
ctx context.Context,
|
||||
listFn func(context.Context, *github.ListOptions) ([]T, *github.Response, error),
|
||||
opts listOptions,
|
||||
) ([]T, error) {
|
||||
var allItems []T
|
||||
|
||||
for {
|
||||
items, resp, err := listFn(ctx, &opts.ListOptions)
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return nil, err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return nil, ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pre-allocate the slice if this is the first page
|
||||
if allItems == nil {
|
||||
allItems = make([]T, 0, len(items)*2) // Estimate double the first page size
|
||||
}
|
||||
|
||||
allItems = append(allItems, items...)
|
||||
|
||||
// Check if we've exceeded the maximum allowed items
|
||||
if len(allItems) > opts.MaxItems {
|
||||
return nil, ErrTooManyItems
|
||||
}
|
||||
|
||||
// If there are no more pages, break
|
||||
if resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Set up next page
|
||||
opts.Page = resp.NextPage
|
||||
}
|
||||
|
||||
return allItems, nil
|
||||
}
|
||||
Reference in New Issue
Block a user