Provisioning: Use Nanogit for basic git operations in Github repository type (#107889)
This commit is contained in:
@@ -8,10 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
)
|
||||
|
||||
type githubClient struct {
|
||||
@@ -22,268 +18,12 @@ func NewClient(client *github.Client) Client {
|
||||
return &githubClient{client}
|
||||
}
|
||||
|
||||
func (r *githubClient) IsAuthenticated(ctx context.Context) error {
|
||||
if _, _, err := r.gh.Users.Get(ctx, ""); err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) {
|
||||
switch ghErr.Response.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return apierrors.NewUnauthorized("token is invalid or expired")
|
||||
case http.StatusForbidden:
|
||||
return &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusUnauthorized,
|
||||
Reason: metav1.StatusReasonUnauthorized,
|
||||
Message: "token is revoked or has insufficient permissions",
|
||||
},
|
||||
}
|
||||
case http.StatusServiceUnavailable:
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) {
|
||||
_, resp, err := r.gh.Repositories.Get(ctx, owner, repository)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
const (
|
||||
maxDirectoryItems = 1000 // Maximum number of items allowed in a directory
|
||||
maxTreeItems = 10000 // Maximum number of items allowed in a tree
|
||||
maxCommits = 1000 // Maximum number of commits to fetch
|
||||
maxCompareFiles = 1000 // Maximum number of files to compare between commits
|
||||
maxWebhooks = 100 // Maximum number of webhooks allowed per repository
|
||||
maxPRFiles = 1000 // Maximum number of files allowed in a pull request
|
||||
maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request
|
||||
maxFileSize = 10 * 1024 * 1024 // 10MB in bytes
|
||||
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
|
||||
)
|
||||
|
||||
func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) {
|
||||
// First try to get repository contents
|
||||
opts := &github.RepositoryContentGetOptions{
|
||||
Ref: ref,
|
||||
}
|
||||
|
||||
fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts)
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return nil, nil, ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return nil, nil, ErrResourceNotFound
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if fc != nil {
|
||||
// Check file size before returning content
|
||||
if fc.GetSize() > maxFileSize {
|
||||
return nil, nil, ErrFileTooLarge
|
||||
}
|
||||
return realRepositoryContent{fc}, nil, nil
|
||||
}
|
||||
|
||||
// For directories, check size limits
|
||||
if len(dc) > maxDirectoryItems {
|
||||
return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems)
|
||||
}
|
||||
|
||||
// Convert directory contents
|
||||
allContents := make([]RepositoryContent, 0, len(dc))
|
||||
for _, original := range dc {
|
||||
allContents = append(allContents, realRepositoryContent{original})
|
||||
}
|
||||
|
||||
return nil, allContents, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) {
|
||||
var tree *github.Tree
|
||||
var err error
|
||||
|
||||
subPaths := safepath.Split(basePath)
|
||||
currentRef := ref
|
||||
|
||||
for {
|
||||
// If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children.
|
||||
recursive := recursive && len(subPaths) == 0
|
||||
|
||||
tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive)
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return nil, false, err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return nil, false, ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
if currentRef != ref {
|
||||
// We're operating with a subpath which doesn't exist yet.
|
||||
// Pretend as if there is simply no files.
|
||||
// FIXME: why should we pretend this?
|
||||
return nil, false, nil
|
||||
}
|
||||
// currentRef == ref
|
||||
// This indicates the repository or commitish reference doesn't exist. This should always return an error.
|
||||
return nil, false, ErrResourceNotFound
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Check if we've exceeded the maximum allowed items
|
||||
if len(tree.Entries) > maxTreeItems {
|
||||
return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems)
|
||||
}
|
||||
|
||||
// Prep for next iteration.
|
||||
if len(subPaths) == 0 {
|
||||
// We're done: we've discovered the tree we want.
|
||||
break
|
||||
}
|
||||
|
||||
// the ref must be equal the SHA of the entry corresponding to subPaths[0]
|
||||
currentRef = ""
|
||||
for _, e := range tree.Entries {
|
||||
if e.GetPath() == subPaths[0] {
|
||||
currentRef = e.GetSHA()
|
||||
break
|
||||
}
|
||||
}
|
||||
subPaths = subPaths[1:]
|
||||
if currentRef == "" {
|
||||
// We couldn't find the folder in the tree...
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If the tree is truncated and we're in recursive mode, return an error
|
||||
if tree.GetTruncated() && recursive {
|
||||
return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems)
|
||||
}
|
||||
|
||||
entries := make([]RepositoryContent, 0, len(tree.Entries))
|
||||
for _, te := range tree.Entries {
|
||||
rrc := &realRepositoryContent{
|
||||
real: &github.RepositoryContent{
|
||||
Path: te.Path,
|
||||
Size: te.Size,
|
||||
SHA: te.SHA,
|
||||
},
|
||||
}
|
||||
if te.GetType() == "tree" {
|
||||
rrc.real.Type = github.Ptr("dir")
|
||||
} else {
|
||||
rrc.real.Type = te.Type
|
||||
}
|
||||
entries = append(entries, rrc)
|
||||
}
|
||||
return entries, tree.GetTruncated(), nil
|
||||
}
|
||||
|
||||
func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("Create %s", path)
|
||||
}
|
||||
|
||||
_, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
||||
Branch: &branch,
|
||||
Message: &message,
|
||||
Content: content,
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusUnprocessableEntity {
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("Update %s", path)
|
||||
}
|
||||
|
||||
_, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
||||
Branch: &branch,
|
||||
Message: &message,
|
||||
Content: content,
|
||||
SHA: &hash,
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusConflict {
|
||||
return ErrMismatchedHash
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("Delete %s", path)
|
||||
}
|
||||
|
||||
_, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
||||
Branch: &branch,
|
||||
Message: &message,
|
||||
SHA: &hash,
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusConflict {
|
||||
return ErrMismatchedHash
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -343,105 +83,6 @@ func (r *githubClient) Commits(ctx context.Context, owner, repository, path, bra
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) {
|
||||
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) {
|
||||
compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return compare.Files, resp, nil
|
||||
}
|
||||
|
||||
files, err := paginatedList(
|
||||
ctx,
|
||||
listFn,
|
||||
defaultListOptions(maxCompareFiles),
|
||||
)
|
||||
if errors.Is(err, ErrTooManyItems) {
|
||||
return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles)
|
||||
}
|
||||
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) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) {
|
||||
branch, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
|
||||
if err != nil {
|
||||
// For some reason, GitHub client handles this case differently by failing with a wrapped error
|
||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
return Branch{}, ErrResourceNotFound
|
||||
}
|
||||
|
||||
if resp != nil && resp.StatusCode == http.StatusServiceUnavailable {
|
||||
return Branch{}, ErrServiceUnavailable
|
||||
}
|
||||
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return Branch{}, err
|
||||
}
|
||||
// Leaving these just in case
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return Branch{}, ErrServiceUnavailable
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return Branch{}, ErrResourceNotFound
|
||||
}
|
||||
return Branch{}, err
|
||||
}
|
||||
|
||||
return Branch{
|
||||
Name: branch.GetName(),
|
||||
Sha: branch.GetCommit().GetSHA(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error {
|
||||
// Fail if the branch already exists
|
||||
if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil {
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
|
||||
// Branch out based on the repository branch
|
||||
baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get base branch: %w", err)
|
||||
}
|
||||
|
||||
if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{
|
||||
Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)),
|
||||
Object: &github.GitObject{
|
||||
SHA: baseRef.Commit.SHA,
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create branch ref: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) {
|
||||
_, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -626,44 +267,6 @@ func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repo
|
||||
return nil
|
||||
}
|
||||
|
||||
type realRepositoryContent struct {
|
||||
real *github.RepositoryContent
|
||||
}
|
||||
|
||||
var _ RepositoryContent = realRepositoryContent{}
|
||||
|
||||
func (c realRepositoryContent) IsDirectory() bool {
|
||||
return c.real.GetType() == "dir"
|
||||
}
|
||||
|
||||
func (c realRepositoryContent) GetFileContent() (string, error) {
|
||||
return c.real.GetContent()
|
||||
}
|
||||
|
||||
func (c realRepositoryContent) IsSymlink() bool {
|
||||
return c.real.Target != nil
|
||||
}
|
||||
|
||||
func (c realRepositoryContent) GetPath() string {
|
||||
return c.real.GetPath()
|
||||
}
|
||||
|
||||
func (c realRepositoryContent) GetSHA() string {
|
||||
return c.real.GetSHA()
|
||||
}
|
||||
|
||||
func (c realRepositoryContent) GetSize() int64 {
|
||||
if c.real.Size != nil {
|
||||
return int64(*c.real.Size)
|
||||
}
|
||||
if c.real.Content != nil {
|
||||
if c, err := c.real.GetContent(); err == nil {
|
||||
return int64(len(c))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// listOptions represents pagination parameters for list operations
|
||||
type listOptions struct {
|
||||
github.ListOptions
|
||||
|
||||
Reference in New Issue
Block a user