diff --git a/pkg/observer/observertesthelper/observer_test_helper.go b/pkg/observer/observertesthelper/observer_test_helper.go index ab46e459eff..9f30ee57448 100644 --- a/pkg/observer/observertesthelper/observer_test_helper.go +++ b/pkg/observer/observertesthelper/observer_test_helper.go @@ -86,6 +86,15 @@ func WithMyPid() TestOption { } } +// Filter for a container id in event export +func WithContainerId(id string) TestOption { + return func(o *TestOptions) { + o.exporter.allowList = append(o.exporter.allowList, &tetragon.Filter{ + ContainerId: []string{id}, + }) + } +} + func WithAllowList(allowList *tetragon.Filter) TestOption { return func(o *TestOptions) { o.exporter.allowList = append(o.exporter.allowList, allowList) @@ -511,6 +520,37 @@ func ExecWGCurl(readyWG *sync.WaitGroup, retries uint, args ...string) error { return err } +// DockerCreate creates a new docker container in the background. The container will +// be killed and removed on test cleanup. +// It returns the containerId on success, or an error if spawning the container failed. +func DockerCreate(tb testing.TB, args ...string) (containerId string) { + // note: we are not using `--rm` so we can choose to wait on the container + // with `docker wait`. We remove it manually below in t.Cleanup instead + args = append([]string{"create"}, args...) + id, err := exec.Command("docker", args...).Output() + if err != nil { + tb.Fatalf("failed to spawn docker container %v: %s", args, err) + } + + containerId = strings.TrimSpace(string(id)) + tb.Cleanup(func() { + err := exec.Command("docker", "rm", "--force", containerId).Run() + if err != nil { + tb.Logf("failed to remove container %s: %s", containerId, err) + } + }) + + return containerId +} + +// DockerStart starts a new docker container with a given ID. +func DockerStart(tb testing.TB, id string) { + err := exec.Command("docker", "start", id).Run() + if err != nil { + tb.Fatalf("failed to start docker container %s: %s", id, err) + } +} + // dockerRun starts a new docker container in the background. The container will // be killed and removed on test cleanup. // It returns the containerId on success, or an error if spawning the container failed. @@ -534,6 +574,15 @@ func DockerRun(tb testing.TB, args ...string) (containerId string) { return containerId } +// dockerExec executes a command in a container. +func DockerExec(tb testing.TB, id string, args ...string) { + args = append([]string{"exec", id}, args...) + err := exec.Command("docker", args...).Run() + if err != nil { + tb.Fatalf("failed to exec in docker container %v: %s", args, err) + } +} + type fakeK8sWatcher struct { fakePod, fakeNamespace string } diff --git a/pkg/sensors/exec/exec_test.go b/pkg/sensors/exec/exec_test.go index 9273e9822c6..58b3653eba1 100644 --- a/pkg/sensors/exec/exec_test.go +++ b/pkg/sensors/exec/exec_test.go @@ -626,6 +626,86 @@ func TestDocker(t *testing.T) { assert.NoError(t, err) } +func TestInInitTree(t *testing.T) { + if err := exec.Command("docker", "version").Run(); err != nil { + t.Skipf("docker not available. skipping test: %s", err) + } + + var doneWG, readyWG sync.WaitGroup + defer doneWG.Wait() + + ctx, cancel := context.WithTimeout(context.Background(), tus.Conf().CmdWaitTime) + defer cancel() + + containerID := observertesthelper.DockerCreate(t, "--name", "in-init-tree-test", "bash", "bash", "-c", "sleep 1 & sleep infinity") + // Tetragon sends 31 bytes + \0 to user-space. Since it might have an arbitrary prefix, + // match only on the first 24 bytes. + trimmedContainerID := sm.Prefix(containerID[:24]) + + obs, err := observertesthelper.GetDefaultObserver(t, ctx, tus.Conf().TetragonLib, observertesthelper.WithContainerId(containerID[:24])) + if err != nil { + t.Fatalf("GetDefaultObserver error: %s", err) + } + + observertesthelper.LoopEvents(ctx, t, &doneWG, &readyWG, obs) + + readyWG.Wait() + observertesthelper.DockerStart(t, "in-init-tree-test") + time.Sleep(1 * time.Second) + observertesthelper.DockerExec(t, "in-init-tree-test", "ls") + + // This is the initial cmd, so inInitTree should be true + entrypointChecker := ec.NewProcessChecker(). + WithBinary(sm.Suffix("/docker-entrypoint.sh")). + WithCwd(sm.Full("/")). + WithUid(0). + WithDocker(trimmedContainerID). + WithInInitTree(true) + + // This is forked from the initial cmd, so inInitTree should be true + bashChecker := ec.NewProcessChecker(). + WithBinary(sm.Suffix("/bash")). + WithCwd(sm.Full("/")). + WithUid(0). + WithDocker(trimmedContainerID). + WithInInitTree(true) + + // This is forked from the initial cmd, so inInitTree should be true + sleepChecker := ec.NewProcessChecker(). + WithBinary(sm.Suffix("/sleep")). + WithArguments(sm.Full("infinity")). + WithCwd(sm.Full("/")). + WithUid(0). + WithDocker(trimmedContainerID). + WithInInitTree(true) + + // This is run via docker exec, so inInitTree should be false + lsChecker := ec.NewProcessChecker(). + WithBinary(sm.Suffix("/ls")). + WithCwd(sm.Full("/")). + WithUid(0). + WithDocker(trimmedContainerID). + WithInInitTree(false) + + checker := ec.NewUnorderedEventChecker( + ec.NewProcessExecChecker("entrypoint"). + WithProcess(entrypointChecker). + WithParent(ec.NewProcessChecker().WithInInitTree(false)), + ec.NewProcessExecChecker("bash"). + WithProcess(bashChecker). + WithParent(entrypointChecker), + ec.NewProcessExecChecker("sleep"). + WithProcess(sleepChecker). + WithParent(bashChecker), + ec.NewProcessExecChecker("ls"). + WithProcess(lsChecker). + WithParent(ec.NewProcessChecker().WithInInitTree(false)), + ) + + err = jsonchecker.JsonTestCheck(t, checker) + assert.NoError(t, err) +} + func TestUpdateStatsMap(t *testing.T) { m, err := ebpf.NewMap(&ebpf.MapSpec{ Type: ebpf.PerCPUArray,