github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/client/github.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"time"
     9  
    10  	"connectrpc.com/connect"
    11  	"github.com/google/go-github/v58/github"
    12  	"github.com/opentracing/opentracing-go"
    13  	"golang.org/x/oauth2"
    14  
    15  	vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1"
    16  	"github.com/grafana/pyroscope/pkg/util/connectgrpc"
    17  )
    18  
    19  // GithubClient returns a github client.
    20  func GithubClient(ctx context.Context, token *oauth2.Token, client *http.Client) (*githubClient, error) {
    21  	return &githubClient{
    22  		repoService: github.NewClient(client).WithAuthToken(token.AccessToken).Repositories,
    23  	}, nil
    24  }
    25  
    26  type repositoryService interface {
    27  	GetCommit(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.RepositoryCommit, *github.Response, error)
    28  	GetContents(ctx context.Context, owner, repo, path string, opts *github.RepositoryContentGetOptions) (*github.RepositoryContent, []*github.RepositoryContent, *github.Response, error)
    29  }
    30  
    31  type githubClient struct {
    32  	repoService repositoryService
    33  }
    34  
    35  func (gh *githubClient) GetCommit(ctx context.Context, owner, repo, ref string) (*vcsv1.CommitInfo, error) {
    36  	sp, ctx := opentracing.StartSpanFromContext(ctx, "githubClient.GetCommit")
    37  	defer sp.Finish()
    38  	sp.SetTag("owner", owner)
    39  	sp.SetTag("repo", repo)
    40  	sp.SetTag("ref", ref)
    41  
    42  	commit, _, err := gh.repoService.GetCommit(ctx, owner, repo, ref, nil)
    43  	if err != nil {
    44  		var githubErr *github.ErrorResponse
    45  		if errors.As(err, &githubErr) {
    46  			code := connectgrpc.HTTPToCode(int32(githubErr.Response.StatusCode))
    47  			sp.SetTag("error", true)
    48  			sp.SetTag("error.message", err.Error())
    49  			sp.SetTag("http.status_code", githubErr.Response.StatusCode)
    50  			return nil, connect.NewError(code, err)
    51  		}
    52  		sp.SetTag("error", true)
    53  		sp.SetTag("error.message", err.Error())
    54  		return nil, err
    55  	}
    56  	// error if message is nil
    57  	if commit.Commit == nil || commit.Commit.Message == nil {
    58  		err := connect.NewError(connect.CodeInternal, errors.New("commit contains no message"))
    59  		sp.SetTag("error", true)
    60  		sp.SetTag("error.message", err.Error())
    61  		return nil, err
    62  	}
    63  	if commit.Commit == nil || commit.Commit.Author == nil || commit.Commit.Author.Date == nil {
    64  		err := connect.NewError(connect.CodeInternal, errors.New("commit contains no date"))
    65  		sp.SetTag("error", true)
    66  		sp.SetTag("error.message", err.Error())
    67  		return nil, err
    68  	}
    69  
    70  	commitInfo := &vcsv1.CommitInfo{
    71  		Sha:     toString(commit.SHA),
    72  		Message: toString(commit.Commit.Message),
    73  		Date:    commit.Commit.Author.Date.Format(time.RFC3339),
    74  	}
    75  
    76  	// add author if it exists
    77  	if commit.Author != nil && commit.Author.Login != nil && commit.Author.AvatarURL != nil {
    78  		commitInfo.Author = &vcsv1.CommitAuthor{
    79  			Login:     toString(commit.Author.Login),
    80  			AvatarURL: toString(commit.Author.AvatarURL),
    81  		}
    82  	}
    83  
    84  	return commitInfo, nil
    85  }
    86  
    87  func (gh *githubClient) GetFile(ctx context.Context, req FileRequest) (File, error) {
    88  	sp, ctx := opentracing.StartSpanFromContext(ctx, "githubClient.GetFile")
    89  	defer sp.Finish()
    90  	sp.SetTag("owner", req.Owner)
    91  	sp.SetTag("repo", req.Repo)
    92  	sp.SetTag("path", req.Path)
    93  	sp.SetTag("ref", req.Ref)
    94  
    95  	// We could abstract away git provider using git protocol
    96  	// git clone https://x-access-token:<token>@github.com/owner/repo.git
    97  	// For now we use the github client.
    98  
    99  	file, _, _, err := gh.repoService.GetContents(ctx, req.Owner, req.Repo, req.Path, &github.RepositoryContentGetOptions{Ref: req.Ref})
   100  	if err != nil {
   101  		var githubErr *github.ErrorResponse
   102  		if errors.As(err, &githubErr) && githubErr.Response.StatusCode == http.StatusNotFound {
   103  			err := fmt.Errorf("%w: %s", ErrNotFound, err)
   104  			sp.SetTag("error", true)
   105  			sp.SetTag("error.message", err.Error())
   106  			sp.SetTag("http.status_code", http.StatusNotFound)
   107  			return File{}, err
   108  		}
   109  		sp.SetTag("error", true)
   110  		sp.SetTag("error.message", err.Error())
   111  		return File{}, err
   112  	}
   113  
   114  	if file == nil {
   115  		sp.SetTag("error", true)
   116  		sp.SetTag("error.message", ErrNotFound.Error())
   117  		return File{}, ErrNotFound
   118  	}
   119  
   120  	// We only support files retrieval.
   121  	if file.Type != nil && *file.Type != "file" {
   122  		err := connect.NewError(connect.CodeInvalidArgument, errors.New("path is not a file"))
   123  		sp.SetTag("error", true)
   124  		sp.SetTag("error.message", err.Error())
   125  		return File{}, err
   126  	}
   127  
   128  	content, err := file.GetContent()
   129  	if err != nil {
   130  		sp.SetTag("error", true)
   131  		sp.SetTag("error.message", err.Error())
   132  		return File{}, err
   133  	}
   134  
   135  	return File{
   136  		Content: content,
   137  		URL:     toString(file.HTMLURL),
   138  	}, nil
   139  }
   140  
   141  func toString(s *string) string {
   142  	if s == nil {
   143  		return ""
   144  	}
   145  	return *s
   146  }