carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "errors"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "syscall"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostbox"
17 "github.com/carlosframework/platform/internal/instancectl"
18)
19
20func TestPlan11Rev3ObservesRealInitTenant(t *testing.T) {
21 raw, err := os.ReadFile("/proc/self/cgroup")
22 if err != nil || !strings.HasPrefix(string(raw), "0::/") {
23 t.Skip("no unified cgroup membership")
24 }
25 rel := strings.TrimSpace(strings.TrimPrefix(strings.SplitN(string(raw), "\n", 2)[0], "0::/"))
26 if _, err := os.Stat(filepath.Join("/sys/fs/cgroup", rel)); err != nil {
27 t.Skip("cgroup tree not visible:", err)
28 }
29 binary := filepath.Join(t.TempDir(), "instanceexec.test")
30 build := exec.Command("go", "test", "-c", "-o", binary, "../instanceexec")
31 if out, err := build.CombinedOutput(); err != nil {
32 t.Fatalf("build init helper: %v\n%s", err, out)
33 }
34 cmd := exec.Command(binary, "-test.run=^TestPlan11Rev3InitObserveHelper$")
35 cmd.Env = append(os.Environ(), "CARLOS_REV3_OBSERVE_HELPER=1")
36 if err := cmd.Start(); err != nil {
37 t.Fatal(err)
38 }
39 t.Cleanup(func() { _ = cmd.Process.Signal(syscall.SIGTERM); _ = cmd.Wait() })
40 deadline := time.Now().Add(5 * time.Second)
41 for {
42 tasks, err := filepath.Glob(filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid), "task", "*", "children"))
43 if err != nil {
44 t.Fatal(err)
45 }
46 found := false
47 for _, path := range tasks {
48 raw, _ := os.ReadFile(path)
49 for _, child := range strings.Fields(string(raw)) {
50 if st, err := os.Stat(filepath.Join("/proc", child, "exe")); err == nil {
51 want, err := os.Stat("/bin/sleep")
52 if err == nil && os.SameFile(st, want) {
53 found = true
54 }
55 }
56 }
57 }
58 if found {
59 break
60 }
61 if time.Now().After(deadline) {
62 t.Fatal("real init did not fork tenant")
63 }
64 time.Sleep(time.Millisecond)
65 }
66 k := linuxClaimLaunchKernel{CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", BoundApp: "bin/sleep"}
67 started := instancectl.ClaimUnitStarted{Unit: "u", InvocationID: "i", ControlGroup: rel, MainPID: cmd.Process.Pid}
68 got, err := k.Observe(started)
69 if err != nil {
70 t.Fatal(err)
71 }
72 want := plan11LaunchFileID(t, "/bin/sleep")
73 if got.Running != want || got.Executable != want {
74 t.Fatalf("observed trampoline instead of tenant: got=%+v tenant=%+v", got, want)
75 }
76 mainStart, err := claimLaunchProcessStart("/proc", cmd.Process.Pid)
77 if err != nil || got.ProcessStart != mainStart {
78 t.Fatalf("main PID lifetime lost: got=%+v main=%s err=%v", got, mainStart, err)
79 }
80}
81
82func rev3ChildFixture(t *testing.T) (linuxClaimLaunchKernel, instancectl.ClaimUnitStarted, func(string, string)) {
83 t.Helper()
84 root := t.TempDir()
85 write := func(path, value string) {
86 t.Helper()
87 path = filepath.Join(root, path)
88 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
89 t.Fatal(err)
90 }
91 if err := os.WriteFile(path, []byte(value), 0644); err != nil {
92 t.Fatal(err)
93 }
94 }
95 stat := func(pid, ppid, start string) string {
96 return pid + " (app) S " + ppid + strings.Repeat(" 0", 17) + " " + start
97 }
98 write("proc/100/stat", stat("100", "1", "main-start"))
99 write("proc/200/stat", stat("200", "100", "child-start"))
100 write("proc/100/cgroup", "0::/unit\n")
101 write("proc/200/cgroup", "0::/unit\n")
102 write("proc/100/task/100/children", "")
103 write("proc/100/task/101/children", "200")
104 write("proc/100/root/opt/carlos/app", "tenant")
105 if err := os.MkdirAll(filepath.Join(root, "proc/200/root/opt/carlos"), 0755); err != nil {
106 t.Fatal(err)
107 }
108 if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/root/opt/carlos/app")); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/exe")); err != nil {
112 t.Fatal(err)
113 }
114 write("proc/100/exe", "trampoline")
115 if err := os.MkdirAll(filepath.Join(root, "cgroup/unit"), 0755); err != nil {
116 t.Fatal(err)
117 }
118 return linuxClaimLaunchKernel{ProcRoot: filepath.Join(root, "proc"), CgroupRoot: filepath.Join(root, "cgroup")}, instancectl.ClaimUnitStarted{MainPID: 100, ControlGroup: "unit"}, write
119}
120
121func TestPlan11Rev3ChildScope(t *testing.T) {
122 for _, name := range []string{"valid", "other parent", "other cgroup", "ambiguous", "no tenant", "dead tenant", "other namespace bind"} {
123 t.Run(name, func(t *testing.T) {
124 k, s, write := rev3ChildFixture(t)
125 switch name {
126 case "other parent":
127 write("proc/200/stat", "200 (app) S 99"+strings.Repeat(" 0", 17)+" child-start")
128 case "other cgroup":
129 write("proc/200/cgroup", "0::/other\n")
130 case "ambiguous":
131 write("proc/100/task/100/children", "300")
132 case "no tenant":
133 write("proc/100/task/101/children", "")
134 case "dead tenant":
135 write("proc/200/stat", "200 (app) Z 100"+strings.Repeat(" 0", 17)+" child-start")
136 case "other namespace bind":
137 if err := os.Remove(filepath.Join(k.ProcRoot, "200/root/opt/carlos/app")); err != nil {
138 t.Fatal(err)
139 }
140 write("proc/200/root/opt/carlos/app", "other")
141 }
142 got, err := k.Observe(s)
143 if name == "valid" {
144 if err != nil || got.TenantPID != 200 || got.TenantStart != "child-start" || got.ProcessStart != "main-start" || got.Running != got.Executable {
145 t.Fatalf("observation=%+v err=%v", got, err)
146 }
147 } else if name == "no tenant" {
148 if !errors.Is(err, errClaimLaunchPending) || got.ProcessStart != "main-start" || got.TenantPID != 0 {
149 t.Fatalf("pending=%+v err=%v", got, err)
150 }
151 } else if err == nil {
152 t.Fatalf("unsafe child accepted: %+v", got)
153 }
154 })
155 }
156}
157
158func TestPlan11Rev3PinsTenantReadiness(t *testing.T) {
159 for _, name := range []string{"stable", "changed pid", "reused pid", "disappeared"} {
160 t.Run(name, func(t *testing.T) {
161 f := newLaunchFixture(t, servingOp())
162 calls := 0
163 f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
164 calls++
165 got := f.kernel.observed
166 got.TenantPID, got.TenantStart = 4243, "child-start"
167 if calls == 1 {
168 return got, errClaimLaunchPending
169 }
170 switch name {
171 case "changed pid":
172 got.TenantPID++
173 case "reused pid":
174 got.TenantStart = "other-start"
175 case "disappeared":
176 got.TenantPID, got.TenantStart = 0, ""
177 }
178 return got, nil
179 }
180 got, err := f.root.Serve(t.Context(), f.peer, f.req)
181 if name == "stable" {
182 if err != nil || got.Grant == nil || got.Grant.PID != 4242 || got.Grant.ProcessStart != "ps-4242" {
183 t.Fatalf("main supervision identity changed: %+v %v", got, err)
184 }
185 } else if !errors.Is(err, hostbox.ErrChanged) {
186 t.Fatalf("replacement child granted: %+v %v", got, err)
187 }
188 })
189 }
190}
191