Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ee/agent-smith/pkg/detector/proc_test.go
2501 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package detector
6
7
import (
8
"bytes"
9
"fmt"
10
"sort"
11
"sync"
12
"testing"
13
14
"github.com/gitpod-io/gitpod/agent-smith/pkg/common"
15
"github.com/google/go-cmp/cmp"
16
lru "github.com/hashicorp/golang-lru"
17
"github.com/prometheus/client_golang/prometheus"
18
"github.com/prometheus/procfs"
19
)
20
21
type memoryProcEntry struct {
22
P *process
23
Env []string
24
}
25
26
type memoryProc map[int]memoryProcEntry
27
28
func (p memoryProc) Discover() map[int]*process {
29
res := make(map[int]*process, len(p))
30
for k, v := range p {
31
res[k] = v.P
32
}
33
return res
34
}
35
36
func (p memoryProc) Environ(pid int) ([]string, error) {
37
proc, ok := p[pid]
38
if !ok {
39
return nil, fmt.Errorf("process does not exist")
40
}
41
return proc.Env, nil
42
}
43
44
var ws = &common.Workspace{WorkspaceID: "foobar", InstanceID: "baz", PID: 3}
45
46
func TestFindWorkspaces(t *testing.T) {
47
ws5 := &common.Workspace{WorkspaceID: "bla", InstanceID: "blabla", PID: 5}
48
ws7 := &common.Workspace{WorkspaceID: "second-ws", InstanceID: "second-ws", PID: 7}
49
50
type WorkspaceAndDepth struct {
51
W *common.Workspace
52
K ProcessKind
53
C string
54
D int
55
PID int
56
}
57
tests := []struct {
58
Name string
59
Proc memoryProc
60
Expectation []WorkspaceAndDepth
61
}{
62
{
63
Name: "happy path",
64
Proc: (func() memoryProc {
65
res := make(map[int]memoryProcEntry)
66
res[1] = memoryProcEntry{P: &process{PID: 1}}
67
res[2] = memoryProcEntry{
68
P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
69
Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},
70
}
71
res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}
72
res[1].P.Children = []*process{res[2].P}
73
res[2].P.Children = []*process{res[3].P}
74
return res
75
})(),
76
Expectation: []WorkspaceAndDepth{
77
{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},
78
{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},
79
},
80
},
81
{
82
Name: "multiple workspacekit children",
83
Proc: (func() memoryProc {
84
res := make(map[int]memoryProcEntry)
85
res[1] = memoryProcEntry{P: &process{PID: 1}}
86
res[2] = memoryProcEntry{
87
P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
88
Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},
89
}
90
res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}
91
res[1].P.Children = []*process{res[2].P}
92
res[2].P.Children = []*process{res[3].P}
93
return res
94
})(),
95
Expectation: []WorkspaceAndDepth{
96
{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},
97
{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},
98
},
99
},
100
{
101
Name: "mixed depths",
102
Proc: (func() memoryProc {
103
res := make(map[int]memoryProcEntry)
104
res[1] = memoryProcEntry{P: &process{PID: 1}}
105
res[2] = memoryProcEntry{
106
P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
107
Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},
108
}
109
res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}
110
res[1].P.Children = []*process{res[2].P}
111
res[2].P.Children = []*process{res[3].P}
112
113
res[4] = memoryProcEntry{
114
P: &process{PID: 4, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
115
Env: []string{"GITPOD_WORKSPACE_ID=bla", "GITPOD_INSTANCE_ID=blabla"},
116
}
117
res[5] = memoryProcEntry{P: &process{PID: 5, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}
118
res[3].P.Children = []*process{res[4].P}
119
res[4].P.Children = []*process{res[5].P}
120
121
return res
122
})(),
123
Expectation: []WorkspaceAndDepth{
124
{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},
125
{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},
126
{PID: 4, D: 3, K: ProcessUserWorkload, C: "/proc/self/exe", W: ws},
127
{PID: 5, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws},
128
},
129
},
130
{
131
Name: "depper workspace",
132
Proc: (func() memoryProc {
133
res := make(map[int]memoryProcEntry)
134
res[1] = memoryProcEntry{P: &process{PID: 1}}
135
res[2] = memoryProcEntry{
136
P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"not-a-workspace"}},
137
}
138
res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"still", "not"}}}
139
res[1].P.Children = []*process{res[2].P}
140
res[2].P.Children = []*process{res[3].P}
141
142
res[4] = memoryProcEntry{
143
P: &process{PID: 4, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
144
Env: []string{"GITPOD_WORKSPACE_ID=bla", "GITPOD_INSTANCE_ID=blabla"},
145
}
146
res[5] = memoryProcEntry{P: &process{PID: 5, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}
147
res[3].P.Children = []*process{res[4].P}
148
res[4].P.Children = []*process{res[5].P}
149
150
res[6] = memoryProcEntry{
151
P: &process{PID: 6, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
152
Env: []string{"GITPOD_WORKSPACE_ID=second-ws", "GITPOD_INSTANCE_ID=second-ws"},
153
}
154
res[7] = memoryProcEntry{P: &process{PID: 7, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}
155
res[3].P.Children = []*process{res[4].P, res[6].P}
156
res[6].P.Children = []*process{res[7].P}
157
158
return res
159
})(),
160
Expectation: []WorkspaceAndDepth{
161
{PID: 4, D: 3, K: ProcessSandbox, C: "/proc/self/exe", W: ws5},
162
{PID: 5, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws5},
163
{PID: 6, D: 3, K: ProcessSandbox, C: "/proc/self/exe", W: ws7},
164
{PID: 7, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws7},
165
},
166
},
167
}
168
169
for _, test := range tests {
170
t.Run(test.Name, func(t *testing.T) {
171
idx := test.Proc.Discover()
172
root, ok := idx[1]
173
if !ok {
174
t.Fatal("test has no PID 1")
175
}
176
177
findWorkspaces(test.Proc, root, 0, nil)
178
179
var act []WorkspaceAndDepth
180
for _, p := range idx {
181
if p.Workspace != nil {
182
act = append(act, WorkspaceAndDepth{
183
W: p.Workspace,
184
D: p.Depth,
185
K: p.Kind,
186
C: p.Cmdline[0],
187
PID: p.PID,
188
})
189
}
190
}
191
sort.Slice(act, func(i, j int) bool {
192
return act[i].PID < act[j].PID
193
})
194
195
if diff := cmp.Diff(test.Expectation, act); diff != "" {
196
t.Errorf("unexpected findWorkspaces (-want +got):\n%s", diff)
197
}
198
})
199
}
200
}
201
202
func TestRunDetector(t *testing.T) {
203
tests := []struct {
204
Name string
205
Proc []memoryProc
206
Expectation []Process
207
}{
208
{
209
Name: "happy path",
210
Proc: []memoryProc{
211
(func() memoryProc {
212
res := make(map[int]memoryProcEntry)
213
res[1] = memoryProcEntry{P: &process{Hash: 1, PID: 1}}
214
res[2] = memoryProcEntry{
215
P: &process{Hash: 2, PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
216
Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},
217
}
218
res[3] = memoryProcEntry{P: &process{Hash: 3, PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}
219
res[4] = memoryProcEntry{P: &process{Hash: 4, PID: 4, Parent: res[3].P, Cmdline: []string{"bad-actor", "has", "args"}}}
220
res[1].P.Children = []*process{res[2].P}
221
res[2].P.Children = []*process{res[3].P}
222
res[3].P.Children = []*process{res[4].P}
223
return res
224
})(),
225
(func() memoryProc {
226
res := make(map[int]memoryProcEntry)
227
res[1] = memoryProcEntry{P: &process{Hash: 1, PID: 1}}
228
res[2] = memoryProcEntry{
229
P: &process{Hash: 2, PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},
230
Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},
231
}
232
res[3] = memoryProcEntry{P: &process{Hash: 3, PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}
233
res[4] = memoryProcEntry{P: &process{Hash: 4, PID: 4, Parent: res[3].P, Cmdline: []string{"bad-actor", "has", "args"}}}
234
res[5] = memoryProcEntry{P: &process{Hash: 5, PID: 5, Parent: res[3].P, Cmdline: []string{"another-bad-actor", "has", "args"}}}
235
res[1].P.Children = []*process{res[2].P}
236
res[2].P.Children = []*process{res[3].P}
237
res[3].P.Children = []*process{res[4].P, res[5].P}
238
return res
239
})(),
240
},
241
Expectation: []Process{
242
{Path: "", CommandLine: []string{"bad-actor", "has", "args"}, Kind: ProcessUserWorkload, Workspace: ws},
243
{Path: "", CommandLine: []string{"another-bad-actor", "has", "args"}, Kind: ProcessUserWorkload, Workspace: ws},
244
},
245
},
246
}
247
248
for _, test := range tests {
249
t.Run(test.Name, func(t *testing.T) {
250
cache, _ := lru.New(10)
251
ps := make(chan Process)
252
det := ProcfsDetector{
253
indexSizeGuage: prometheus.NewGauge(prometheus.GaugeOpts{Name: "dont"}),
254
cacheUseCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"use"}),
255
workspaceGauge: prometheus.NewGauge(prometheus.GaugeOpts{Name: "dont"}),
256
cache: cache,
257
}
258
259
var wg sync.WaitGroup
260
var res []Process
261
wg.Add(1)
262
go func() {
263
defer wg.Done()
264
for p := range ps {
265
res = append(res, p)
266
}
267
}()
268
269
for _, proc := range test.Proc {
270
det.proc = proc
271
det.run(ps)
272
}
273
close(ps)
274
wg.Wait()
275
276
sort.Slice(res, func(i, j int) bool {
277
return res[i].Kind < res[j].Kind
278
})
279
280
if diff := cmp.Diff(test.Expectation, res); diff != "" {
281
t.Errorf("unexpected run (-want +got):\n%s", diff)
282
}
283
})
284
}
285
}
286
287
func TestDiscovery(t *testing.T) {
288
p, err := procfs.NewFS("/proc")
289
if err != nil {
290
t.Fatal(err)
291
}
292
293
proc := realProcfs(p)
294
res := proc.Discover()
295
296
if len(res) == 0 {
297
t.Fatal("did not discover any process")
298
}
299
}
300
301
func TestParseGitpodEnviron(t *testing.T) {
302
tests := []struct {
303
Name string
304
Content string
305
Expectation []string
306
}{
307
{
308
Name: "empty set",
309
Expectation: []string{},
310
},
311
{
312
Name: "happy path",
313
Content: "GITPOD_INSTANCE_ID=foobar\000GITPOD_SOMETHING=blabla\000SOMETHING_ELSE\000",
314
Expectation: []string{
315
"GITPOD_INSTANCE_ID=foobar",
316
"GITPOD_SOMETHING=blabla",
317
},
318
},
319
{
320
Name: "exceed token size",
321
Content: func() string {
322
r := "12345678"
323
for i := 0; i < 7; i++ {
324
r += r
325
}
326
return "SOME_ENV_VAR=" + r + "\000GITPOD_FOOBAR=bar"
327
}(),
328
Expectation: []string{
329
"GITPOD_FOOBAR=bar",
330
},
331
},
332
}
333
for _, test := range tests {
334
t.Run(test.Name, func(t *testing.T) {
335
act, err := parseGitpodEnviron(bytes.NewReader([]byte(test.Content)))
336
if err != nil {
337
t.Fatal(err)
338
}
339
340
if diff := cmp.Diff(test.Expectation, act); diff != "" {
341
t.Errorf("unexpected parseGitpodEnviron (-want +got):\n%s", diff)
342
}
343
})
344
}
345
}
346
347
func benchmarkParseGitpodEnviron(content string, b *testing.B) {
348
b.ReportAllocs()
349
b.ResetTimer()
350
for n := 0; n < b.N; n++ {
351
parseGitpodEnviron(bytes.NewReader([]byte(content)))
352
}
353
}
354
355
func BenchmarkParseGitpodEnvironP0(b *testing.B) { benchmarkParseGitpodEnviron("", b) }
356
func BenchmarkParseGitpodEnvironP1(b *testing.B) {
357
benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000", b)
358
}
359
func BenchmarkParseGitpodEnvironP2(b *testing.B) {
360
benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000", b)
361
}
362
func BenchmarkParseGitPodEnvironP4(b *testing.B) {
363
benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000", b)
364
}
365
func BenchmarkParseGitpodEnvironP8(b *testing.B) {
366
benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000", b)
367
}
368
func BenchmarkParseGitpodEnvironN1(b *testing.B) { benchmarkParseGitpodEnviron("NOT_ME\000", b) }
369
func BenchmarkParseGitpodEnvironN2(b *testing.B) {
370
benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000", b)
371
}
372
func BenchmarkParseGitpodEnvironN4(b *testing.B) {
373
benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000", b)
374
}
375
func BenchmarkParseGitpodEnvironN8(b *testing.B) {
376
benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000", b)
377
}
378
379
func TestParseStat(t *testing.T) {
380
type Expectation struct {
381
S *stat
382
Err string
383
}
384
tests := []struct {
385
Name string
386
Content string
387
Expectation Expectation
388
}{
389
{
390
Name: "empty set",
391
Expectation: Expectation{Err: "cannot parse stat"},
392
},
393
{
394
Name: "happy path",
395
Content: "80275 (cat) R 717 80275 717 34817 80275 4194304 85 0 0 0 0 0 0 0 26 6 1 0 4733826 5771264 135 18446744073709551615 94070799228928 94070799254577 140722983793472 0 0 0 0 0 0 0 0 0 17 14 0 0 0 0 0 94070799272592 94070799274176 94070803738624 140722983801930 140722983801950 140722983801950 140722983821291 0",
396
Expectation: Expectation{S: &stat{PPID: 717, Starttime: 4733826}},
397
},
398
{
399
Name: "pid 1",
400
Content: "1 (systemd) S 0 1 1 0 -1 4194560 62769 924461 98 1590 388 255 2488 1097 20 0 1 0 63 175169536 3435 18446744073709551615 94093530578944 94093531561125 140726309452800 0 0 0 671173123 4096 1260 1 0 0 17 3 0 0 32 0 0 94093531915152 94093532201000 94093562523648 140726309453736 140726309453747 140726309453747 140726309453805 0",
401
Expectation: Expectation{S: &stat{Starttime: 63}},
402
},
403
{
404
Name: "kthreadd",
405
Content: "2 (kthreadd) S 0 0 0 0 -1 2129984 0 0 0 0 3 0 0 0 20 0 1 0 63 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0",
406
Expectation: Expectation{S: &stat{Starttime: 63}},
407
},
408
}
409
for _, test := range tests {
410
t.Run(test.Name, func(t *testing.T) {
411
var (
412
act Expectation
413
err error
414
)
415
act.S, err = parseStat(bytes.NewReader([]byte(test.Content)))
416
if err != nil {
417
act.Err = err.Error()
418
}
419
420
if diff := cmp.Diff(test.Expectation, act); diff != "" {
421
t.Errorf("unexpected parseStat (-want +got):\n%s", diff)
422
}
423
})
424
}
425
}
426
427
func BenchmarkParseStat(b *testing.B) {
428
r := bytes.NewReader([]byte("80275 (cat) R 717 80275 717 34817 80275 4194304 85 0 0 0 0 0 0 0 26 6 1 0 4733826 5771264 135 18446744073709551615 94070799228928 94070799254577 140722983793472 0 0 0 0 0 0 0 0 0 17 14 0 0 0 0 0 94070799272592 94070799274176 94070803738624 140722983801930 140722983801950 140722983801950 140722983821291 0"))
429
430
b.ReportAllocs()
431
b.ResetTimer()
432
for n := 0; n < b.N; n++ {
433
parseStat(r)
434
}
435
}
436
437