github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/source/find.go (about) 1 package source 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "io" 8 "net/http" 9 "path/filepath" 10 "strings" 11 12 "connectrpc.com/connect" 13 "github.com/go-kit/log" 14 "github.com/go-kit/log/level" 15 giturl "github.com/kubescape/go-git-url" 16 "github.com/opentracing/opentracing-go" 17 18 vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1" 19 "github.com/grafana/pyroscope/pkg/frontend/vcs/client" 20 "github.com/grafana/pyroscope/pkg/frontend/vcs/config" 21 ) 22 23 type VCSClient interface { 24 GetFile(ctx context.Context, req client.FileRequest) (client.File, error) 25 } 26 27 // FileFinder finds a file in a vcs repository. 28 type FileFinder struct { 29 file config.FileSpec 30 ref, rootPath string 31 repo giturl.IGitURL 32 33 config *config.PyroscopeConfig 34 client VCSClient 35 httpClient *http.Client 36 logger log.Logger 37 } 38 39 // NewFileFinder returns a new FileFinder. 40 func NewFileFinder(client VCSClient, repo giturl.IGitURL, file config.FileSpec, rootPath, ref string, httpClient *http.Client, logger log.Logger) *FileFinder { 41 if ref == "" { 42 ref = "HEAD" 43 } 44 return &FileFinder{ 45 client: client, 46 logger: logger, 47 repo: repo, 48 file: file, 49 rootPath: rootPath, 50 ref: ref, 51 httpClient: httpClient, 52 } 53 } 54 55 // Find returns the file content and URL. 56 func (ff *FileFinder) Find(ctx context.Context) (*vcsv1.GetFileResponse, error) { 57 // first try to gather the config 58 ff.loadConfig(ctx) 59 60 // without config we are done here 61 if ff.config == nil { 62 return ff.findFallback(ctx) 63 } 64 65 // find matching mappings 66 mapping := ff.config.FindMapping(ff.file) 67 if mapping == nil { 68 return ff.findFallback(ctx) 69 } 70 71 switch config.Language(mapping.Language) { 72 case config.LanguageGo: 73 return ff.findGoFile(ctx, mapping) 74 case config.LanguageJava: 75 return ff.findJavaFile(ctx, mapping) 76 case config.LanguagePython: 77 return ff.findPythonFile(ctx, mapping) 78 // todo: add more languages support 79 default: 80 return ff.findFallback(ctx) 81 } 82 } 83 84 func (ff FileFinder) findFallback(ctx context.Context) (*vcsv1.GetFileResponse, error) { 85 switch filepath.Ext(ff.file.Path) { 86 case ExtGo: 87 return ff.findGoFile(ctx) 88 case ExtPython: 89 return ff.findPythonFile(ctx) 90 case ExtAsm: // Note: When adding wider language support this needs to be revisited 91 return ff.findGoFile(ctx) 92 // todo: add more languages support 93 default: 94 // by default we return the file content at the given path without any processing. 95 return ff.fetchRepoFile(ctx, ff.file.Path, ff.ref) 96 } 97 } 98 99 // loadConfig attempts to load .pyroscope.yaml from the repository root 100 func (ff *FileFinder) loadConfig(ctx context.Context) { 101 sp, ctx := opentracing.StartSpanFromContext(ctx, "FileFinder.loadConfig") 102 defer sp.Finish() 103 104 configPath := config.PyroscopeConfigPath 105 if ff.rootPath != "" { 106 configPath = filepath.Join(ff.rootPath, config.PyroscopeConfigPath) 107 } 108 109 file, err := ff.client.GetFile(ctx, client.FileRequest{ 110 Owner: ff.repo.GetOwnerName(), 111 Repo: ff.repo.GetRepoName(), 112 Path: configPath, 113 Ref: ff.ref, 114 }) 115 if err != nil { 116 // Config is optional, so just log and continue 117 level.Debug(ff.logger).Log("msg", "no .pyroscope.yaml found", "path", configPath) 118 return 119 } 120 sp.SetTag("config.url", file.URL) 121 sp.SetTag("config", file.Content) 122 123 cfg, err := config.ParsePyroscopeConfig([]byte(file.Content)) 124 if err != nil { 125 level.Warn(ff.logger).Log("msg", "failed to parse .pyroscope.yaml", "err", err) 126 return 127 } 128 129 ff.config = cfg 130 level.Debug(ff.logger).Log("msg", "loaded .pyroscope.yaml", "url", file.URL, "mappings", len(cfg.SourceCode.Mappings)) 131 sp.SetTag("config.source_code.mappings_count", len(cfg.SourceCode.Mappings)) 132 133 } 134 135 // fetchRepoFile fetches the file content from the configured repository. 136 func (arg FileFinder) fetchRepoFile(ctx context.Context, path, ref string) (*vcsv1.GetFileResponse, error) { 137 if arg.rootPath != "" { 138 path = filepath.Join(arg.rootPath, path) 139 } 140 content, err := arg.client.GetFile(ctx, client.FileRequest{ 141 Owner: arg.repo.GetOwnerName(), 142 Repo: arg.repo.GetRepoName(), 143 Path: strings.TrimLeft(path, "/"), 144 Ref: ref, 145 }) 146 if err != nil { 147 return nil, err 148 } 149 return newFileResponse(content.Content, content.URL) 150 } 151 152 // fetchURL fetches the file content from the given URL. 153 func (ff FileFinder) fetchURL(ctx context.Context, url string, decodeBase64 bool) (*vcsv1.GetFileResponse, error) { 154 req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 155 if err != nil { 156 return nil, err 157 } 158 resp, err := ff.httpClient.Do(req) // todo: use a custom client with timeout 159 if err != nil { 160 return nil, err 161 } 162 defer resp.Body.Close() 163 if resp.StatusCode != http.StatusOK { 164 return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("failed to fetch %s: %s", url, resp.Status)) 165 } 166 content, err := io.ReadAll(resp.Body) 167 if err != nil { 168 return nil, err 169 } 170 if !decodeBase64 { 171 return newFileResponse(string(content), url) 172 } 173 decoded, err := base64.StdEncoding.DecodeString(string(content)) 174 if err != nil { 175 return nil, err 176 } 177 return newFileResponse(string(decoded), url) 178 } 179 180 func newFileResponse(content, url string) (*vcsv1.GetFileResponse, error) { 181 return &vcsv1.GetFileResponse{ 182 Content: base64.StdEncoding.EncodeToString([]byte(content)), 183 URL: url, 184 }, nil 185 } 186 187 func (ff FileFinder) fetchMappingFile(ctx context.Context, m *config.MappingConfig, path string) (*vcsv1.GetFileResponse, error) { 188 if s := m.Source.Local; s != nil { 189 if s.Path != "" { 190 path = filepath.Join(s.Path, path) 191 } 192 return ff.fetchRepoFile(ctx, path, ff.ref) 193 } 194 if s := m.Source.GitHub; s != nil { 195 if s.Path != "" { 196 path = filepath.Join(s.Path, path) 197 } 198 content, err := ff.client.GetFile(ctx, client.FileRequest{ 199 Owner: s.Owner, 200 Repo: s.Repo, 201 Ref: s.Ref, 202 Path: path, 203 }) 204 if err != nil { 205 return nil, err 206 } 207 return newFileResponse(content.Content, content.URL) 208 } 209 return nil, fmt.Errorf("no supported source provided, file not resolvable") 210 }