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 }