github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/service/adhoc.go (about)

     1  package service
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/pyroscope-io/pyroscope/pkg/adhoc/writer"
    16  	"github.com/pyroscope-io/pyroscope/pkg/model"
    17  	"github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer"
    18  	"github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer/convert"
    19  )
    20  
    21  type AdhocService struct {
    22  	adhocWriter *writer.AdhocDataDirWriter
    23  	dataDir     string
    24  	maxNodes    int
    25  
    26  	m        sync.RWMutex
    27  	profiles map[string]model.AdhocProfile
    28  }
    29  
    30  func NewAdhocService(maxNodes int, dataDir string) *AdhocService {
    31  	return &AdhocService{
    32  		adhocWriter: writer.NewAdhocDataDirWriter(dataDir),
    33  		dataDir:     dataDir,
    34  		maxNodes:    maxNodes,
    35  		profiles:    make(map[string]model.AdhocProfile),
    36  	}
    37  }
    38  
    39  func (svc *AdhocService) GetProfileByID(_ context.Context, id string) (*flamebearer.FlamebearerProfile, error) {
    40  	svc.m.RLock()
    41  	p, ok := svc.profiles[id]
    42  	svc.m.RUnlock()
    43  	if !ok {
    44  		return nil, model.ErrAdhocProfileNotFound
    45  	}
    46  	fb, err := svc.loadProfile(p)
    47  	if err != nil {
    48  		return nil, fmt.Errorf("unable to process profile: %w", err)
    49  	}
    50  	return fb, nil
    51  }
    52  
    53  // GetAllProfiles retrieves the list of profiles for the local pyroscope data directory.
    54  // The profiles are assigned a unique ID (hash based) which is then used for retrieval.
    55  // This requires a bit of extra work to setup the IDs but prevents
    56  // the clients from accesing the filesystem directly, removing that whole attack vector.
    57  //
    58  // The profiles are retrieved every time the endpoint is requested,
    59  // which should be good enough as massive access to this auth endpoint is not expected.
    60  func (svc *AdhocService) GetAllProfiles(_ context.Context) ([]model.AdhocProfile, error) {
    61  	if err := os.MkdirAll(svc.dataDir, os.ModeDir|os.ModePerm); err != nil {
    62  		return nil, fmt.Errorf("unable to create data directory: %w", err)
    63  	}
    64  	profiles := make(map[string]model.AdhocProfile, 0)
    65  	err := filepath.WalkDir(svc.dataDir, func(path string, e fs.DirEntry, err error) error {
    66  		if err != nil {
    67  			return err
    68  		}
    69  		if e.IsDir() && path != svc.dataDir {
    70  			return fs.SkipDir
    71  		}
    72  		if e.Type().IsRegular() {
    73  			id := svc.generateHash(e.Name())
    74  			if p, ok := profiles[id]; ok {
    75  				return fmt.Errorf("a hash collision detected between %s and %s, please report it", e.Name(), p.Name)
    76  			}
    77  			info, err := e.Info()
    78  			if err != nil {
    79  				return fmt.Errorf("unable to retrieve entry information: %w", err)
    80  			}
    81  			profiles[id] = model.AdhocProfile{ID: id, Name: e.Name(), UpdatedAt: info.ModTime()}
    82  		}
    83  		return nil
    84  	})
    85  	if err != nil {
    86  		return nil, fmt.Errorf("retrieving the profile list: %w", err)
    87  	}
    88  	profilesCopy := make([]model.AdhocProfile, 0, len(svc.profiles))
    89  	for _, p := range profiles {
    90  		profilesCopy = append(profilesCopy, p)
    91  	}
    92  	svc.m.Lock()
    93  	svc.profiles = profiles
    94  	svc.m.Unlock()
    95  	return profilesCopy, nil
    96  }
    97  
    98  func (svc *AdhocService) GetProfileDiffByID(_ context.Context, params model.GetAdhocProfileDiffByIDParams) (*flamebearer.FlamebearerProfile, error) {
    99  	svc.m.RLock()
   100  	bp, bok := svc.profiles[params.BaseID]
   101  	dp, dok := svc.profiles[params.DiffID]
   102  	svc.m.RUnlock()
   103  	if !bok || !dok {
   104  		return nil, model.ErrAdhocProfileNotFound
   105  	}
   106  	var err error
   107  	bfb, err := svc.loadProfile(bp)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("unable to process left profile: %w", err)
   110  	}
   111  	dfb, err := svc.loadProfile(dp)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("unable to process right profile: %w", err)
   114  	}
   115  	// Try to get a name for the profile.
   116  	var name string
   117  	for _, n := range []string{bfb.Metadata.Name, dfb.Metadata.Name, bp.Name, dp.Name} {
   118  		if n != "" {
   119  			name = n
   120  			break
   121  		}
   122  	}
   123  	fb, err := flamebearer.Diff(name, bfb, dfb, svc.maxNodes)
   124  	if err != nil {
   125  		return nil, fmt.Errorf("unable to generate a diff profile: %w", err)
   126  	}
   127  	return &fb, nil
   128  }
   129  
   130  func (svc *AdhocService) UploadProfile(_ context.Context, params model.UploadAdhocProfileParams) (*flamebearer.FlamebearerProfile, string, error) {
   131  	fb, err := convert.FlamebearerFromFile(params.Profile, svc.maxNodes)
   132  	if err != nil {
   133  		return nil, "", model.ValidationError{Err: err}
   134  	}
   135  	err = svc.adhocWriter.EnsureExists()
   136  	if err != nil {
   137  		return nil, "", fmt.Errorf("unable to create data directory: %w", err)
   138  	}
   139  	now := time.Now()
   140  	// Remove extension, since we will store a json file
   141  	filename := strings.TrimSuffix(params.Profile.Name, filepath.Ext(params.Profile.Name))
   142  	// TODO(eh-am): maybe we should use whatever the user has sent us?
   143  	// TODO(kolesnikovae): I agree that we should store the original
   144  	//   user input, however, it's pretty problematic to change without
   145  	//   violation of the backward compatibility.
   146  	filename = fmt.Sprintf("%s-%s.json", filename, now.Format("2006-01-02-15-04-05"))
   147  	if _, err = svc.adhocWriter.Write(filename, *fb); err != nil {
   148  		return nil, "", fmt.Errorf("unable to write profile to the data directory: %w", err)
   149  	}
   150  	return fb, svc.generateHash(filename), nil
   151  }
   152  
   153  func (svc *AdhocService) loadProfile(p model.AdhocProfile) (*flamebearer.FlamebearerProfile, error) {
   154  	fileName := filepath.Join(svc.dataDir, p.Name)
   155  	f, err := os.Open(fileName)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("unable to open profile: %w", err)
   158  	}
   159  	defer f.Close()
   160  	b, err := io.ReadAll(f)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("unable to read profile: %w", err)
   163  	}
   164  	return convert.FlamebearerFromFile(convert.ProfileFile{Name: fileName, Data: b}, svc.maxNodes)
   165  }
   166  
   167  func (*AdhocService) generateHash(name string) string {
   168  	return fmt.Sprintf("%x", sha256.Sum256([]byte(name)))
   169  }