The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
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.
 
 
 
 
 
 
grafana/pkg/registry/apis/provisioning/repository/github/impl.go

333 lines
8.8 KiB

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
}