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 "context"
7 "errors"
8 "net"
9 "net/http"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "sync/atomic"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/hostwrite"
19 "github.com/carlosframework/platform/internal/registry"
20)
21
22// plan9D3Server answers on a unix socket in this test process, so the
23// connection's SO_PEERCRED peer is os.Getpid(). It counts every request.
24func plan9D3Server(t *testing.T, status *atomic.Int32) (registry.Route, *atomic.Int32) {
25 t.Helper()
26 socket := filepath.Join(t.TempDir(), "app.sock")
27 ln, err := net.Listen("unix", socket)
28 if err != nil {
29 t.Fatal(err)
30 }
31 hits := new(atomic.Int32)
32 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 hits.Add(1)
34 if r.URL.Path != "/ready" {
35 w.WriteHeader(http.StatusNotFound)
36 return
37 }
38 if code := int(status.Load()); code >= 300 && code < 400 {
39 w.Header().Set("Location", "/elsewhere")
40 w.WriteHeader(code)
41 return
42 }
43 w.WriteHeader(int(status.Load()))
44 })}
45 go srv.Serve(ln)
46 t.Cleanup(func() { srv.Close() })
47 return registry.Route{Host: recoveryHost, Socket: socket}, hits
48}
49
50// plan9D3SelfCgroup is this process's cgroup v2 directory.
51func plan9D3SelfCgroup(t *testing.T) string {
52 t.Helper()
53 raw, err := os.ReadFile("/proc/self/cgroup")
54 if err != nil {
55 t.Skip("no /proc/self/cgroup")
56 }
57 for _, line := range strings.Split(string(raw), "\n") {
58 if rel, ok := strings.CutPrefix(line, "0::/"); ok && rel != "" {
59 return filepath.Join("/sys/fs/cgroup", rel)
60 }
61 }
62 t.Skip("this test process is not in a named cgroup v2 group")
63 return ""
64}
65
66func plan9D3Cmd(t *testing.T, pid int) *exec.Cmd {
67 t.Helper()
68 p, err := os.FindProcess(pid)
69 if err != nil {
70 t.Fatal(err)
71 }
72 cmd := exec.Command("/bin/true")
73 cmd.Process = p
74 return cmd
75}
76
77func plan9D3Ready(t *testing.T, cmd *exec.Cmd, rt registry.Route, path, group string) error {
78 t.Helper()
79 ctx, cancel := context.WithTimeout(t.Context(), 600*time.Millisecond)
80 defer cancel()
81 return claimProbeSocketReady(ctx, cmd, rt, ClaimLaunchInputs{ReadyPath: path}, group)
82}
83
84func TestPlan9D3ProbeReadinessAcceptsTheSpawnedTreeIn2xx(t *testing.T) {
85 group := plan9D3SelfCgroup(t)
86 status := new(atomic.Int32)
87 status.Store(http.StatusNoContent)
88 rt, _ := plan9D3Server(t, status)
89 // The answering process is the spawned process itself.
90 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", group); err != nil {
91 t.Fatalf("spawned process: %v", err)
92 }
93 // The answering process is a descendant of the spawned one (a preforking
94 // server's worker): this test process descends from its parent.
95 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getppid()), rt, "/ready", group); err != nil {
96 t.Fatalf("descendant: %v", err)
97 }
98}
99
100func TestPlan9D3ProbeReadinessRefusesOutsideTheContract(t *testing.T) {
101 group := plan9D3SelfCgroup(t)
102 status := new(atomic.Int32)
103 for _, code := range []int{http.StatusFound, http.StatusNotFound, http.StatusInternalServerError} {
104 status.Store(int32(code))
105 rt, hits := plan9D3Server(t, status)
106 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", group); err == nil {
107 t.Fatalf("HTTP %d accepted", code)
108 }
109 if hits.Load() == 0 {
110 t.Fatalf("HTTP %d: no request reached the app", code)
111 }
112 }
113 status.Store(http.StatusOK)
114 rt, hits := plan9D3Server(t, status)
115 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/elsewhere", group); err == nil {
116 t.Fatal("an undeclared path was accepted")
117 }
118 // A peer that is not in the spawned tree, or not in the role's group,
119 // never receives a request.
120 other := exec.Command("sleep", "5")
121 if err := other.Start(); err != nil {
122 t.Fatal(err)
123 }
124 t.Cleanup(func() { other.Process.Kill(); other.Wait() })
125 before := hits.Load()
126 if err := plan9D3Ready(t, plan9D3Cmd(t, other.Process.Pid), rt, "/ready", group); err == nil {
127 t.Fatal("a peer outside the spawned tree was accepted")
128 }
129 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", filepath.Join(filepath.Dir(group), "plan9-not-this-role")); err == nil {
130 t.Fatal("a peer outside the role's cgroup was accepted")
131 }
132 if hits.Load() != before {
133 t.Fatal("an unverified peer received a readiness request")
134 }
135 for _, path := range []string{"", "ready", "//evil/ready", "/ready#x", "/re ady"} {
136 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, path, group); !errors.Is(err, hostwrite.ErrUnavailable) {
137 t.Fatalf("path %q: %v", path, err)
138 }
139 }
140 if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", "relative/group"); !errors.Is(err, hostwrite.ErrUnavailable) {
141 t.Fatalf("relative group: %v", err)
142 }
143}
144
145// Serving keeps main's check: GET /, any status below 500, the exact pid.
146func TestPlan9D3ServingReadinessIsUnchanged(t *testing.T) {
147 status := new(atomic.Int32)
148 status.Store(http.StatusNotFound)
149 rt, _ := plan9D3Server(t, status)
150 ctx, cancel := context.WithTimeout(t.Context(), 600*time.Millisecond)
151 defer cancel()
152 if err := claimSocketReady(ctx, plan9D3Cmd(t, os.Getpid()), rt, ClaimLaunchInputs{}); err != nil {
153 t.Fatalf("serving readiness on 404 from the exact pid: %v", err)
154 }
155 ctx, cancel = context.WithTimeout(t.Context(), 600*time.Millisecond)
156 defer cancel()
157 if err := claimSocketReady(ctx, plan9D3Cmd(t, os.Getppid()), rt, ClaimLaunchInputs{}); err == nil {
158 t.Fatal("serving readiness accepted a descendant peer")
159 }
160}
161