Path: blob/main/components/ee/agent-smith/pkg/detector/proc_test.go
2501 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package detector56import (7"bytes"8"fmt"9"sort"10"sync"11"testing"1213"github.com/gitpod-io/gitpod/agent-smith/pkg/common"14"github.com/google/go-cmp/cmp"15lru "github.com/hashicorp/golang-lru"16"github.com/prometheus/client_golang/prometheus"17"github.com/prometheus/procfs"18)1920type memoryProcEntry struct {21P *process22Env []string23}2425type memoryProc map[int]memoryProcEntry2627func (p memoryProc) Discover() map[int]*process {28res := make(map[int]*process, len(p))29for k, v := range p {30res[k] = v.P31}32return res33}3435func (p memoryProc) Environ(pid int) ([]string, error) {36proc, ok := p[pid]37if !ok {38return nil, fmt.Errorf("process does not exist")39}40return proc.Env, nil41}4243var ws = &common.Workspace{WorkspaceID: "foobar", InstanceID: "baz", PID: 3}4445func TestFindWorkspaces(t *testing.T) {46ws5 := &common.Workspace{WorkspaceID: "bla", InstanceID: "blabla", PID: 5}47ws7 := &common.Workspace{WorkspaceID: "second-ws", InstanceID: "second-ws", PID: 7}4849type WorkspaceAndDepth struct {50W *common.Workspace51K ProcessKind52C string53D int54PID int55}56tests := []struct {57Name string58Proc memoryProc59Expectation []WorkspaceAndDepth60}{61{62Name: "happy path",63Proc: (func() memoryProc {64res := make(map[int]memoryProcEntry)65res[1] = memoryProcEntry{P: &process{PID: 1}}66res[2] = memoryProcEntry{67P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},68Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},69}70res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}71res[1].P.Children = []*process{res[2].P}72res[2].P.Children = []*process{res[3].P}73return res74})(),75Expectation: []WorkspaceAndDepth{76{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},77{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},78},79},80{81Name: "multiple workspacekit children",82Proc: (func() memoryProc {83res := make(map[int]memoryProcEntry)84res[1] = memoryProcEntry{P: &process{PID: 1}}85res[2] = memoryProcEntry{86P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},87Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},88}89res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}90res[1].P.Children = []*process{res[2].P}91res[2].P.Children = []*process{res[3].P}92return res93})(),94Expectation: []WorkspaceAndDepth{95{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},96{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},97},98},99{100Name: "mixed depths",101Proc: (func() memoryProc {102res := make(map[int]memoryProcEntry)103res[1] = memoryProcEntry{P: &process{PID: 1}}104res[2] = memoryProcEntry{105P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},106Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},107}108res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}109res[1].P.Children = []*process{res[2].P}110res[2].P.Children = []*process{res[3].P}111112res[4] = memoryProcEntry{113P: &process{PID: 4, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},114Env: []string{"GITPOD_WORKSPACE_ID=bla", "GITPOD_INSTANCE_ID=blabla"},115}116res[5] = memoryProcEntry{P: &process{PID: 5, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}117res[3].P.Children = []*process{res[4].P}118res[4].P.Children = []*process{res[5].P}119120return res121})(),122Expectation: []WorkspaceAndDepth{123{PID: 2, D: 1, K: ProcessSandbox, C: "/proc/self/exe", W: ws},124{PID: 3, D: 2, K: ProcessSupervisor, C: "supervisor", W: ws},125{PID: 4, D: 3, K: ProcessUserWorkload, C: "/proc/self/exe", W: ws},126{PID: 5, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws},127},128},129{130Name: "depper workspace",131Proc: (func() memoryProc {132res := make(map[int]memoryProcEntry)133res[1] = memoryProcEntry{P: &process{PID: 1}}134res[2] = memoryProcEntry{135P: &process{PID: 2, Parent: res[1].P, Cmdline: []string{"not-a-workspace"}},136}137res[3] = memoryProcEntry{P: &process{PID: 3, Parent: res[2].P, Cmdline: []string{"still", "not"}}}138res[1].P.Children = []*process{res[2].P}139res[2].P.Children = []*process{res[3].P}140141res[4] = memoryProcEntry{142P: &process{PID: 4, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},143Env: []string{"GITPOD_WORKSPACE_ID=bla", "GITPOD_INSTANCE_ID=blabla"},144}145res[5] = memoryProcEntry{P: &process{PID: 5, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}146res[3].P.Children = []*process{res[4].P}147res[4].P.Children = []*process{res[5].P}148149res[6] = memoryProcEntry{150P: &process{PID: 6, Parent: res[3].P, Cmdline: []string{"/proc/self/exe", "ring1"}},151Env: []string{"GITPOD_WORKSPACE_ID=second-ws", "GITPOD_INSTANCE_ID=second-ws"},152}153res[7] = memoryProcEntry{P: &process{PID: 7, Parent: res[4].P, Cmdline: []string{"supervisor", "init"}}}154res[3].P.Children = []*process{res[4].P, res[6].P}155res[6].P.Children = []*process{res[7].P}156157return res158})(),159Expectation: []WorkspaceAndDepth{160{PID: 4, D: 3, K: ProcessSandbox, C: "/proc/self/exe", W: ws5},161{PID: 5, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws5},162{PID: 6, D: 3, K: ProcessSandbox, C: "/proc/self/exe", W: ws7},163{PID: 7, D: 4, K: ProcessSupervisor, C: "supervisor", W: ws7},164},165},166}167168for _, test := range tests {169t.Run(test.Name, func(t *testing.T) {170idx := test.Proc.Discover()171root, ok := idx[1]172if !ok {173t.Fatal("test has no PID 1")174}175176findWorkspaces(test.Proc, root, 0, nil)177178var act []WorkspaceAndDepth179for _, p := range idx {180if p.Workspace != nil {181act = append(act, WorkspaceAndDepth{182W: p.Workspace,183D: p.Depth,184K: p.Kind,185C: p.Cmdline[0],186PID: p.PID,187})188}189}190sort.Slice(act, func(i, j int) bool {191return act[i].PID < act[j].PID192})193194if diff := cmp.Diff(test.Expectation, act); diff != "" {195t.Errorf("unexpected findWorkspaces (-want +got):\n%s", diff)196}197})198}199}200201func TestRunDetector(t *testing.T) {202tests := []struct {203Name string204Proc []memoryProc205Expectation []Process206}{207{208Name: "happy path",209Proc: []memoryProc{210(func() memoryProc {211res := make(map[int]memoryProcEntry)212res[1] = memoryProcEntry{P: &process{Hash: 1, PID: 1}}213res[2] = memoryProcEntry{214P: &process{Hash: 2, PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},215Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},216}217res[3] = memoryProcEntry{P: &process{Hash: 3, PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}218res[4] = memoryProcEntry{P: &process{Hash: 4, PID: 4, Parent: res[3].P, Cmdline: []string{"bad-actor", "has", "args"}}}219res[1].P.Children = []*process{res[2].P}220res[2].P.Children = []*process{res[3].P}221res[3].P.Children = []*process{res[4].P}222return res223})(),224(func() memoryProc {225res := make(map[int]memoryProcEntry)226res[1] = memoryProcEntry{P: &process{Hash: 1, PID: 1}}227res[2] = memoryProcEntry{228P: &process{Hash: 2, PID: 2, Parent: res[1].P, Cmdline: []string{"/proc/self/exe", "ring1"}},229Env: []string{"GITPOD_WORKSPACE_ID=foobar", "GITPOD_INSTANCE_ID=baz"},230}231res[3] = memoryProcEntry{P: &process{Hash: 3, PID: 3, Parent: res[2].P, Cmdline: []string{"supervisor", "init"}}}232res[4] = memoryProcEntry{P: &process{Hash: 4, PID: 4, Parent: res[3].P, Cmdline: []string{"bad-actor", "has", "args"}}}233res[5] = memoryProcEntry{P: &process{Hash: 5, PID: 5, Parent: res[3].P, Cmdline: []string{"another-bad-actor", "has", "args"}}}234res[1].P.Children = []*process{res[2].P}235res[2].P.Children = []*process{res[3].P}236res[3].P.Children = []*process{res[4].P, res[5].P}237return res238})(),239},240Expectation: []Process{241{Path: "", CommandLine: []string{"bad-actor", "has", "args"}, Kind: ProcessUserWorkload, Workspace: ws},242{Path: "", CommandLine: []string{"another-bad-actor", "has", "args"}, Kind: ProcessUserWorkload, Workspace: ws},243},244},245}246247for _, test := range tests {248t.Run(test.Name, func(t *testing.T) {249cache, _ := lru.New(10)250ps := make(chan Process)251det := ProcfsDetector{252indexSizeGuage: prometheus.NewGauge(prometheus.GaugeOpts{Name: "dont"}),253cacheUseCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"use"}),254workspaceGauge: prometheus.NewGauge(prometheus.GaugeOpts{Name: "dont"}),255cache: cache,256}257258var wg sync.WaitGroup259var res []Process260wg.Add(1)261go func() {262defer wg.Done()263for p := range ps {264res = append(res, p)265}266}()267268for _, proc := range test.Proc {269det.proc = proc270det.run(ps)271}272close(ps)273wg.Wait()274275sort.Slice(res, func(i, j int) bool {276return res[i].Kind < res[j].Kind277})278279if diff := cmp.Diff(test.Expectation, res); diff != "" {280t.Errorf("unexpected run (-want +got):\n%s", diff)281}282})283}284}285286func TestDiscovery(t *testing.T) {287p, err := procfs.NewFS("/proc")288if err != nil {289t.Fatal(err)290}291292proc := realProcfs(p)293res := proc.Discover()294295if len(res) == 0 {296t.Fatal("did not discover any process")297}298}299300func TestParseGitpodEnviron(t *testing.T) {301tests := []struct {302Name string303Content string304Expectation []string305}{306{307Name: "empty set",308Expectation: []string{},309},310{311Name: "happy path",312Content: "GITPOD_INSTANCE_ID=foobar\000GITPOD_SOMETHING=blabla\000SOMETHING_ELSE\000",313Expectation: []string{314"GITPOD_INSTANCE_ID=foobar",315"GITPOD_SOMETHING=blabla",316},317},318{319Name: "exceed token size",320Content: func() string {321r := "12345678"322for i := 0; i < 7; i++ {323r += r324}325return "SOME_ENV_VAR=" + r + "\000GITPOD_FOOBAR=bar"326}(),327Expectation: []string{328"GITPOD_FOOBAR=bar",329},330},331}332for _, test := range tests {333t.Run(test.Name, func(t *testing.T) {334act, err := parseGitpodEnviron(bytes.NewReader([]byte(test.Content)))335if err != nil {336t.Fatal(err)337}338339if diff := cmp.Diff(test.Expectation, act); diff != "" {340t.Errorf("unexpected parseGitpodEnviron (-want +got):\n%s", diff)341}342})343}344}345346func benchmarkParseGitpodEnviron(content string, b *testing.B) {347b.ReportAllocs()348b.ResetTimer()349for n := 0; n < b.N; n++ {350parseGitpodEnviron(bytes.NewReader([]byte(content)))351}352}353354func BenchmarkParseGitpodEnvironP0(b *testing.B) { benchmarkParseGitpodEnviron("", b) }355func BenchmarkParseGitpodEnvironP1(b *testing.B) {356benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000", b)357}358func BenchmarkParseGitpodEnvironP2(b *testing.B) {359benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000", b)360}361func BenchmarkParseGitPodEnvironP4(b *testing.B) {362benchmarkParseGitpodEnviron("GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000GITPOD_INSTANCE_ID=foobar\000", b)363}364func BenchmarkParseGitpodEnvironP8(b *testing.B) {365benchmarkParseGitpodEnviron("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)366}367func BenchmarkParseGitpodEnvironN1(b *testing.B) { benchmarkParseGitpodEnviron("NOT_ME\000", b) }368func BenchmarkParseGitpodEnvironN2(b *testing.B) {369benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000", b)370}371func BenchmarkParseGitpodEnvironN4(b *testing.B) {372benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000", b)373}374func BenchmarkParseGitpodEnvironN8(b *testing.B) {375benchmarkParseGitpodEnviron("NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000NOT_ME\000", b)376}377378func TestParseStat(t *testing.T) {379type Expectation struct {380S *stat381Err string382}383tests := []struct {384Name string385Content string386Expectation Expectation387}{388{389Name: "empty set",390Expectation: Expectation{Err: "cannot parse stat"},391},392{393Name: "happy path",394Content: "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",395Expectation: Expectation{S: &stat{PPID: 717, Starttime: 4733826}},396},397{398Name: "pid 1",399Content: "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",400Expectation: Expectation{S: &stat{Starttime: 63}},401},402{403Name: "kthreadd",404Content: "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",405Expectation: Expectation{S: &stat{Starttime: 63}},406},407}408for _, test := range tests {409t.Run(test.Name, func(t *testing.T) {410var (411act Expectation412err error413)414act.S, err = parseStat(bytes.NewReader([]byte(test.Content)))415if err != nil {416act.Err = err.Error()417}418419if diff := cmp.Diff(test.Expectation, act); diff != "" {420t.Errorf("unexpected parseStat (-want +got):\n%s", diff)421}422})423}424}425426func BenchmarkParseStat(b *testing.B) {427r := 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"))428429b.ReportAllocs()430b.ResetTimer()431for n := 0; n < b.N; n++ {432parseStat(r)433}434}435436437