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

     1  package source
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/go-kit/log/level"
    13  	"github.com/opentracing/opentracing-go"
    14  	"golang.org/x/mod/modfile"
    15  	"golang.org/x/mod/module"
    16  
    17  	vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1"
    18  	"github.com/grafana/pyroscope/pkg/frontend/vcs/client"
    19  	"github.com/grafana/pyroscope/pkg/frontend/vcs/config"
    20  	"github.com/grafana/pyroscope/pkg/frontend/vcs/source/golang"
    21  )
    22  
    23  const (
    24  	ExtGo  = ".go"
    25  	ExtAsm = ".s" // Assembler files in go
    26  )
    27  
    28  // findGoFile finds a go file in a vcs repository.
    29  func (ff FileFinder) findGoFile(ctx context.Context, mappings ...*config.MappingConfig) (*vcsv1.GetFileResponse, error) {
    30  	sp, ctx := opentracing.StartSpanFromContext(ctx, "findGoFile")
    31  	defer sp.Finish()
    32  	sp.SetTag("file.path", ff.file.Path)
    33  	sp.SetTag("file.function_name", ff.file.FunctionName)
    34  
    35  	// if we have mappings try those first
    36  	for _, m := range mappings {
    37  		pos := m.Match(ff.file)
    38  		if pos < 0 || pos > len(ff.file.Path) {
    39  			level.Warn(ff.logger).Log("msg", "mapping cut off out of bounds", "pos", pos, "file_path", ff.file.Path)
    40  			continue
    41  		}
    42  		resp, err := ff.fetchMappingFile(ctx, m, strings.TrimLeft(ff.file.Path[pos:], "/"))
    43  		if err != nil {
    44  			if errors.Is(err, client.ErrNotFound) {
    45  				continue
    46  			}
    47  			level.Warn(ff.logger).Log("msg", "failed to fetch mapping file", "err", err)
    48  			continue
    49  		}
    50  		return resp, nil
    51  	}
    52  
    53  	if path, version, ok := golang.IsStandardLibraryPath(ff.file.Path); ok {
    54  		return ff.fetchGoStdlib(ctx, path, version)
    55  	}
    56  
    57  	if relativePath, ok := golang.VendorRelativePath(ff.file.Path); ok {
    58  		return ff.fetchRepoFile(ctx, relativePath, ff.ref)
    59  	}
    60  
    61  	modFile, ok := golang.ParseModuleFromPath(ff.file.Path)
    62  	if ok {
    63  		mainModule := module.Version{
    64  			Path:    path.Join(ff.repo.GetHostName(), ff.repo.GetOwnerName(), ff.repo.GetRepoName()),
    65  			Version: module.PseudoVersion("", "", time.Time{}, ff.ref),
    66  		}
    67  		modf, err := ff.fetchGoMod(ctx)
    68  		if err != nil {
    69  			level.Warn(ff.logger).Log("msg", "failed to fetch go.mod file", "err", err)
    70  		}
    71  		if err := modFile.Resolve(ctx, mainModule, modf, ff.httpClient); err != nil {
    72  			return nil, err
    73  		}
    74  		return ff.fetchGoDependencyFile(ctx, modFile)
    75  	}
    76  	return ff.tryFindGoFile(ctx, 30)
    77  }
    78  
    79  func (ff FileFinder) fetchGoStdlib(ctx context.Context, path string, version string) (*vcsv1.GetFileResponse, error) {
    80  	sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoStdlib")
    81  	defer sp.Finish()
    82  
    83  	// if there is no version detected, use the one from .pyroscope.yaml
    84  	if version == "" && ff.config != nil {
    85  		mapping := ff.config.FindMapping(config.FileSpec{Path: "$GOROOT/src"})
    86  		if mapping != nil {
    87  			return ff.fetchMappingFile(ctx, mapping, path)
    88  		}
    89  	}
    90  
    91  	// use master branch as fallback
    92  	ref := "master"
    93  	if version != "" {
    94  		ref = "go" + version
    95  	}
    96  
    97  	content, err := ff.client.GetFile(ctx, client.FileRequest{
    98  		Owner: "golang",
    99  		Repo:  "go",
   100  		Path:  filepath.Join("src", path),
   101  		Ref:   ref,
   102  	})
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return newFileResponse(content.Content, content.URL)
   107  }
   108  
   109  func (ff FileFinder) fetchGoMod(ctx context.Context) (*modfile.File, error) {
   110  	sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoMod")
   111  	defer sp.Finish()
   112  	sp.SetTag("owner", ff.repo.GetOwnerName())
   113  	sp.SetTag("repo", ff.repo.GetRepoName())
   114  	sp.SetTag("ref", ff.ref)
   115  
   116  	content, err := ff.client.GetFile(ctx, client.FileRequest{
   117  		Owner: ff.repo.GetOwnerName(),
   118  		Repo:  ff.repo.GetRepoName(),
   119  		Path:  golang.GoMod,
   120  		Ref:   ff.ref,
   121  	})
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	return modfile.Parse(golang.GoMod, []byte(content.Content), nil)
   126  }
   127  
   128  func (ff FileFinder) fetchGoDependencyFile(ctx context.Context, module golang.Module) (*vcsv1.GetFileResponse, error) {
   129  	sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoDependencyFile")
   130  	defer sp.Finish()
   131  	sp.SetTag("module_path", module.Path)
   132  
   133  	switch {
   134  	case module.IsGitHub():
   135  		return ff.fetchGithubModuleFile(ctx, module)
   136  	case module.IsGoogleSource():
   137  		return ff.fetchGoogleSourceDependencyFile(ctx, module)
   138  	}
   139  	return nil, fmt.Errorf("unsupported module path: %s", module.Path)
   140  }
   141  
   142  func (ff FileFinder) fetchGithubModuleFile(ctx context.Context, mod golang.Module) (*vcsv1.GetFileResponse, error) {
   143  	sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGithubModuleFile")
   144  	defer sp.Finish()
   145  	sp.SetTag("module_path", mod.Path)
   146  
   147  	// todo: what if this is not a github repo?
   148  	// 		VSClient should support querying multiple repo providers.
   149  	githubFile, err := mod.GithubFile()
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	sp.SetTag("owner", githubFile.Owner)
   154  	sp.SetTag("repo", githubFile.Repo)
   155  	sp.SetTag("path", githubFile.Path)
   156  	sp.SetTag("ref", githubFile.Ref)
   157  
   158  	content, err := ff.client.GetFile(ctx, client.FileRequest{
   159  		Owner: githubFile.Owner,
   160  		Repo:  githubFile.Repo,
   161  		Path:  githubFile.Path,
   162  		Ref:   githubFile.Ref,
   163  	})
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	return newFileResponse(content.Content, content.URL)
   168  }
   169  
   170  func (ff FileFinder) fetchGoogleSourceDependencyFile(ctx context.Context, mod golang.Module) (*vcsv1.GetFileResponse, error) {
   171  	sp, ctx := opentracing.StartSpanFromContext(ctx, "fetchGoogleSourceDependencyFile")
   172  	defer sp.Finish()
   173  	sp.SetTag("module_path", mod.Path)
   174  
   175  	url, err := mod.GoogleSourceURL()
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	sp.SetTag("url", url)
   180  	return ff.fetchURL(ctx, url, true)
   181  }
   182  
   183  // tryFindGoFile tries to find the go file in the repo, under the rootPath.
   184  // It tries to find the file in the rootPath inside the repo by removing path segment after path segment.
   185  // maxAttempts is the maximum number of attempts to try to find the file in case the file path is very long.
   186  // For example, if the path is "github.com/grafana/grafana/pkg/infra/log/log.go" and rootPath is "path/to/module1", it
   187  // will try to find the file at:
   188  // - "path/to/module1/pkg/infra/log/log.go"
   189  // - "path/to/module1/infra/log/log.go"
   190  // - "path/to/module1/log/log.go"
   191  // - "path/to/module1/log.go"
   192  func (ff FileFinder) tryFindGoFile(ctx context.Context, maxAttempts int) (*vcsv1.GetFileResponse, error) {
   193  	if maxAttempts <= 0 {
   194  		return nil, errors.New("invalid max attempts")
   195  	}
   196  
   197  	// trim repo path (e.g. "github.com/grafana/pyroscope/") in path
   198  	path := ff.file.Path
   199  	repoPath := strings.Join([]string{ff.repo.GetHostName(), ff.repo.GetOwnerName(), ff.repo.GetRepoName(), ""}, "/")
   200  	if pos := strings.Index(path, repoPath); pos != -1 {
   201  		path = path[len(repoPath)+pos:]
   202  	}
   203  
   204  	// now try to find file in repo
   205  	path = strings.TrimLeft(path, "/")
   206  	attempts := 0
   207  	for {
   208  		reqPath := path
   209  		if ff.rootPath != "" {
   210  			reqPath = strings.Join([]string{ff.rootPath, path}, "/")
   211  		}
   212  		content, err := ff.client.GetFile(ctx, client.FileRequest{
   213  			Owner: ff.repo.GetOwnerName(),
   214  			Repo:  ff.repo.GetRepoName(),
   215  			Path:  reqPath,
   216  			Ref:   ff.ref,
   217  		})
   218  		attempts++
   219  		if err != nil && errors.Is(err, client.ErrNotFound) && attempts < maxAttempts {
   220  			i := strings.Index(path, "/")
   221  			if i < 0 {
   222  				return nil, err
   223  			}
   224  			// remove the first path segment
   225  			path = path[i+1:]
   226  			continue
   227  		}
   228  		if err != nil {
   229  			return nil, err
   230  		}
   231  		return newFileResponse(content.Content, content.URL)
   232  	}
   233  }