github.com/grafana/pyroscope@v1.18.0/pkg/adhocprofiles/adhocprofiles_test.go (about) 1 package adhocprofiles 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "encoding/json" 8 "os" 9 "strings" 10 "testing" 11 12 "connectrpc.com/connect" 13 "github.com/stretchr/testify/require" 14 thanosobjstore "github.com/thanos-io/objstore" 15 16 v1 "github.com/grafana/pyroscope/api/gen/proto/go/adhocprofiles/v1" 17 phlareobjstore "github.com/grafana/pyroscope/pkg/objstore" 18 "github.com/grafana/pyroscope/pkg/tenant" 19 "github.com/grafana/pyroscope/pkg/util" 20 "github.com/grafana/pyroscope/pkg/validation" 21 ) 22 23 func TestAdHocProfiles_Get(t *testing.T) { 24 bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket()) 25 rawProfile, err := os.ReadFile("testdata/cpu.pprof") 26 require.NoError(t, err) 27 encodedProfile := base64.StdEncoding.EncodeToString(rawProfile) 28 ahp := &AdHocProfile{ 29 Name: "cpu.pprof", 30 Data: encodedProfile, 31 } 32 jsonProfile, _ := json.Marshal(ahp) 33 _ = bucket.Upload(context.Background(), "tenant/adhoc/existing-invalid-json", bytes.NewReader([]byte{1, 2, 3})) 34 _ = bucket.Upload(context.Background(), "tenant/adhoc/existing-valid-profile", bytes.NewReader(jsonProfile)) 35 type args struct { 36 ctx context.Context 37 c *connect.Request[v1.AdHocProfilesGetRequest] 38 } 39 tests := []struct { 40 name string 41 args args 42 wantErr bool 43 }{ 44 { 45 name: "reject requests with missing tenant id", 46 args: args{ 47 ctx: context.Background(), 48 c: nil, 49 }, 50 wantErr: true, 51 }, 52 { 53 name: "return error when getting a non existing profile", 54 args: args{ 55 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 56 c: connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "non-existing-id"}), 57 }, 58 wantErr: true, 59 }, 60 { 61 name: "return error when getting an existing invalid profile", 62 args: args{ 63 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 64 c: connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "existing-invalid-profile"}), 65 }, 66 wantErr: true, 67 }, 68 { 69 name: "return data when getting an existing valid profile", 70 args: args{ 71 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 72 c: connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "existing-valid-profile"}), 73 }, 74 wantErr: false, 75 }, 76 } 77 for _, tt := range tests { 78 t.Run(tt.name, func(t *testing.T) { 79 a := &AdHocProfiles{ 80 logger: util.Logger, 81 limits: validation.MockLimits{MaxFlameGraphNodesDefaultValue: 8192}, 82 bucket: bucket, 83 } 84 _, err := a.Get(tt.args.ctx, tt.args.c) 85 if (err != nil) != tt.wantErr { 86 t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) 87 return 88 } 89 }) 90 } 91 } 92 93 func TestAdHocProfiles_List(t *testing.T) { 94 bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket()) 95 _ = bucket.Upload(context.Background(), "tenant/adhoc/bad-id-should-be-ignored", bytes.NewReader([]byte{1})) 96 _ = bucket.Upload(context.Background(), "tenant/adhoc/01HMXV8BF4EH71NBYZNPPVGJ2X-cpu.pprof", bytes.NewReader([]byte{1})) 97 _ = bucket.Upload(context.Background(), "tenant/adhoc/01HMXRV02963FK36GGRE9N6MPH-heap.pprof", bytes.NewReader([]byte{1})) 98 a := &AdHocProfiles{ 99 logger: util.Logger, 100 bucket: bucket, 101 } 102 response, err := a.List(tenant.InjectTenantID(context.Background(), "tenant"), connect.NewRequest(&v1.AdHocProfilesListRequest{})) 103 require.NoError(t, err) 104 expected := []*v1.AdHocProfilesProfileMetadata{ 105 { 106 Id: "01HMXV8BF4EH71NBYZNPPVGJ2X-cpu.pprof", 107 Name: "cpu.pprof", 108 UploadedAt: 1706103680484, 109 }, 110 { 111 Id: "01HMXRV02963FK36GGRE9N6MPH-heap.pprof", 112 Name: "heap.pprof", 113 UploadedAt: 1706101145673, 114 }, 115 } 116 require.Equal(t, connect.NewResponse(&v1.AdHocProfilesListResponse{Profiles: expected}), response) 117 } 118 119 func TestAdHocProfiles_Upload(t *testing.T) { 120 overrides := validation.MockOverrides(func(defaults *validation.Limits, tenantLimits map[string]*validation.Limits) { 121 defaults.MaxFlameGraphNodesDefault = 8192 122 123 l := validation.MockDefaultLimits() 124 l.MaxProfileSizeBytes = 16 125 tenantLimits["tenant-16-bytes-limit"] = l 126 127 l = validation.MockDefaultLimits() 128 l.MaxProfileSizeBytes = 1600 129 tenantLimits["tenant-1600-bytes-limit"] = l 130 }) 131 132 bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket()) 133 rawProfile, err := os.ReadFile("testdata/cpu.pprof") 134 require.NoError(t, err) 135 encodedProfile := base64.StdEncoding.EncodeToString(rawProfile) 136 type args struct { 137 ctx context.Context 138 c *connect.Request[v1.AdHocProfilesUploadRequest] 139 } 140 tests := []struct { 141 name string 142 args args 143 wantErr string 144 expectedSuffix string 145 }{ 146 { 147 name: "reject requests with missing tenant id", 148 args: args{ 149 ctx: context.Background(), 150 c: nil, 151 }, 152 wantErr: "no org id", 153 }, 154 { 155 name: "should reject an invalid profile", 156 args: args{ 157 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 158 c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{ 159 Name: "test", 160 Profile: "123", 161 }), 162 }, 163 wantErr: "failed to parse profile", 164 }, 165 { 166 name: "should store a valid profile", 167 args: args{ 168 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 169 c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{ 170 Name: "test.cpu.pb.gz", 171 Profile: encodedProfile, 172 }), 173 }, 174 expectedSuffix: "-test.cpu.pb.gz", 175 }, 176 { 177 name: "should limit profile names to particular character set", 178 args: args{ 179 ctx: tenant.InjectTenantID(context.Background(), "tenant"), 180 c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{ 181 Name: "test/../../../etc/passwd", 182 Profile: encodedProfile, 183 }), 184 }, 185 expectedSuffix: "-test_.._.._.._etc_passwd", 186 }, 187 { 188 name: "should enforce profile size", 189 args: args{ 190 ctx: tenant.InjectTenantID(context.Background(), "tenant-16-bytes-limit"), 191 c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{ 192 Name: "compressed-too-big", 193 Profile: encodedProfile, 194 }), 195 }, 196 wantErr: "invalid_argument: profile payload size exceeds limit of 16 B", 197 }, 198 { 199 name: "should enforce profile size limit after decompression", 200 args: args{ 201 // 1580 is the profile size compressed 202 ctx: tenant.InjectTenantID(context.Background(), "tenant-1600-bytes-limit"), 203 c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{ 204 Name: "decompressed-too-big", 205 Profile: encodedProfile, 206 }), 207 }, 208 wantErr: "invalid_argument: uncompressed profile payload size exceeds limit of 1.6 kB", 209 }, 210 } 211 for _, tt := range tests { 212 t.Run(tt.name, func(t *testing.T) { 213 a := &AdHocProfiles{ 214 logger: util.Logger, 215 limits: overrides, 216 bucket: bucket, 217 } 218 _, err := a.Upload(tt.args.ctx, tt.args.c) 219 if tt.wantErr == "" { 220 require.NoError(t, err) 221 } else { 222 require.ErrorContains(t, err, tt.wantErr) 223 } 224 225 if tt.expectedSuffix != "" { 226 found := false 227 err := bucket.Iter(tt.args.ctx, "tenant/adhoc", func(name string) error { 228 if strings.HasSuffix(name, tt.expectedSuffix) { 229 found = true 230 } 231 return nil 232 }) 233 require.NoError(t, err) 234 require.True(t, found) 235 } 236 }) 237 } 238 }