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 }