package cgroup import ( "errors" "os" "os/exec" "path/filepath" "strconv" "testing" ) // writeProcs makes dir a fixture cgroup listing pids. func writeProcs(t *testing.T, dir string, pids ...int) { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } body := "" for _, p := range pids { body += strconv.Itoa(p) + "\n" } if err := os.WriteFile(filepath.Join(dir, "cgroup.procs"), []byte(body), 0o644); err != nil { t.Fatal(err) } } // HostProcs over a fixture cgroupfs: the exec group and every group below // it count, and so does the unit's group; absent groups count 0; another // host's group never counts. func TestHostProcsCountsTheHostsGroupsOnly(t *testing.T) { root := t.TempDir() const host = "shop.example.com" if n, err := HostProcs(root, host, ""); err != nil || n != 0 { t.Fatalf("no group: (%d, %v), want (0, nil)", n, err) } writeProcs(t, filepath.Join(root, ExecName(host))) if n, err := HostProcs(root, host, ""); err != nil || n != 0 { t.Fatalf("an empty group: (%d, %v), want (0, nil)", n, err) } writeProcs(t, filepath.Join(root, ExecName(host)), 101) writeProcs(t, filepath.Join(root, ExecName(host), "sidecar"), 102, 103) writeProcs(t, filepath.Join(root, ExecName("other.example.com")), 201, 202, 203) if n, err := HostProcs(root, host, ""); err != nil || n != 3 { t.Fatalf("exec group and child: (%d, %v), want (3, nil)", n, err) } slice := filepath.Join(t.TempDir(), "carlos.slice", "carlos-instances.slice", "carlos-instances-shop.slice") if n, err := HostProcs(root, host, slice); err != nil || n != 3 { t.Fatalf("an absent unit group: (%d, %v), want (3, nil)", n, err) } writeProcs(t, filepath.Join(slice, "carlos-instance@shop.service"), 301) if n, err := HostProcs(root, host, slice); err != nil || n != 4 { t.Fatalf("with the unit's group: (%d, %v), want (4, nil)", n, err) } if _, err := HostProcs(root, host, "relative/slice"); err == nil { t.Fatal("a relative unit path was read") } for _, bad := range []string{"", "..", "a/b"} { if _, err := HostProcs(root, bad, ""); err == nil { t.Fatalf("host %q was read", bad) } } } // Without a delegated subtree the exec children cannot be told apart from // the agent's own processes: unknown, never 0. func TestHostProcsWithoutDelegationIsUnknown(t *testing.T) { if _, err := HostProcs("", "shop.example.com", ""); !errors.Is(err, ErrUnknown) { t.Fatalf("no root: %v, want ErrUnknown", err) } } // A real process in a real cgroup, where the kernel lets this test make one: // a child group of the test's own cgroup, with a sleep moved into it. Where // the cgroup is not writable (the usual case outside a delegated unit) the // fixture test above is the witness. func TestHostProcsSeesARealProcess(t *testing.T) { b, err := os.ReadFile("/proc/self/cgroup") if err != nil { t.Skipf("no /proc/self/cgroup: %v", err) } rel, ok := ParseSelfCgroup(string(b)) if !ok { t.Skip("no cgroup v2 path") } parent := filepath.Join("/sys/fs/cgroup", rel) root := filepath.Join(parent, "hostprocs-test-"+strconv.Itoa(os.Getpid())) const host = "shop.example.com" group := filepath.Join(root, ExecName(host)) if err := os.MkdirAll(group, 0o755); err != nil { t.Skipf("this cgroup is not writable: %v", err) } t.Cleanup(func() { _ = os.Remove(group) _ = os.Remove(root) }) cmd := exec.Command("sleep", "30") if err := cmd.Start(); err != nil { t.Skipf("no sleep: %v", err) } t.Cleanup(func() { _ = cmd.Process.Kill() _ = cmd.Wait() }) if err := os.WriteFile(filepath.Join(group, "cgroup.procs"), []byte(strconv.Itoa(cmd.Process.Pid)), 0o644); err != nil { t.Skipf("the kernel refused the move: %v", err) } if n, err := HostProcs(root, host, ""); err != nil || n != 1 { t.Fatalf("a sleep in the host's group: (%d, %v), want (1, nil)", n, err) } _ = cmd.Process.Kill() _ = cmd.Wait() if n, err := HostProcs(root, host, ""); err != nil || n != 0 { t.Fatalf("after the sleep exited: (%d, %v), want (0, nil)", n, err) } }