//go:build linux package host import ( "context" "errors" "net" "net/http" "os" "os/exec" "path/filepath" "strings" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" ) // plan9D3Server answers on a unix socket in this test process, so the // connection's SO_PEERCRED peer is os.Getpid(). It counts every request. func plan9D3Server(t *testing.T, status *atomic.Int32) (registry.Route, *atomic.Int32) { t.Helper() socket := filepath.Join(t.TempDir(), "app.sock") ln, err := net.Listen("unix", socket) if err != nil { t.Fatal(err) } hits := new(atomic.Int32) srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) if r.URL.Path != "/ready" { w.WriteHeader(http.StatusNotFound) return } if code := int(status.Load()); code >= 300 && code < 400 { w.Header().Set("Location", "/elsewhere") w.WriteHeader(code) return } w.WriteHeader(int(status.Load())) })} go srv.Serve(ln) t.Cleanup(func() { srv.Close() }) return registry.Route{Host: recoveryHost, Socket: socket}, hits } // plan9D3SelfCgroup is this process's cgroup v2 directory. func plan9D3SelfCgroup(t *testing.T) string { t.Helper() raw, err := os.ReadFile("/proc/self/cgroup") if err != nil { t.Skip("no /proc/self/cgroup") } for _, line := range strings.Split(string(raw), "\n") { if rel, ok := strings.CutPrefix(line, "0::/"); ok && rel != "" { return filepath.Join("/sys/fs/cgroup", rel) } } t.Skip("this test process is not in a named cgroup v2 group") return "" } func plan9D3Cmd(t *testing.T, pid int) *exec.Cmd { t.Helper() p, err := os.FindProcess(pid) if err != nil { t.Fatal(err) } cmd := exec.Command("/bin/true") cmd.Process = p return cmd } func plan9D3Ready(t *testing.T, cmd *exec.Cmd, rt registry.Route, path, group string) error { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), 600*time.Millisecond) defer cancel() return claimProbeSocketReady(ctx, cmd, rt, ClaimLaunchInputs{ReadyPath: path}, group) } func TestPlan9D3ProbeReadinessAcceptsTheSpawnedTreeIn2xx(t *testing.T) { group := plan9D3SelfCgroup(t) status := new(atomic.Int32) status.Store(http.StatusNoContent) rt, _ := plan9D3Server(t, status) // The answering process is the spawned process itself. if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", group); err != nil { t.Fatalf("spawned process: %v", err) } // The answering process is a descendant of the spawned one (a preforking // server's worker): this test process descends from its parent. if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getppid()), rt, "/ready", group); err != nil { t.Fatalf("descendant: %v", err) } } func TestPlan9D3ProbeReadinessRefusesOutsideTheContract(t *testing.T) { group := plan9D3SelfCgroup(t) status := new(atomic.Int32) for _, code := range []int{http.StatusFound, http.StatusNotFound, http.StatusInternalServerError} { status.Store(int32(code)) rt, hits := plan9D3Server(t, status) if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", group); err == nil { t.Fatalf("HTTP %d accepted", code) } if hits.Load() == 0 { t.Fatalf("HTTP %d: no request reached the app", code) } } status.Store(http.StatusOK) rt, hits := plan9D3Server(t, status) if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/elsewhere", group); err == nil { t.Fatal("an undeclared path was accepted") } // A peer that is not in the spawned tree, or not in the role's group, // never receives a request. other := exec.Command("sleep", "5") if err := other.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { other.Process.Kill(); other.Wait() }) before := hits.Load() if err := plan9D3Ready(t, plan9D3Cmd(t, other.Process.Pid), rt, "/ready", group); err == nil { t.Fatal("a peer outside the spawned tree was accepted") } if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", filepath.Join(filepath.Dir(group), "plan9-not-this-role")); err == nil { t.Fatal("a peer outside the role's cgroup was accepted") } if hits.Load() != before { t.Fatal("an unverified peer received a readiness request") } for _, path := range []string{"", "ready", "//evil/ready", "/ready#x", "/re ady"} { if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, path, group); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("path %q: %v", path, err) } } if err := plan9D3Ready(t, plan9D3Cmd(t, os.Getpid()), rt, "/ready", "relative/group"); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("relative group: %v", err) } } // Serving keeps main's check: GET /, any status below 500, the exact pid. func TestPlan9D3ServingReadinessIsUnchanged(t *testing.T) { status := new(atomic.Int32) status.Store(http.StatusNotFound) rt, _ := plan9D3Server(t, status) ctx, cancel := context.WithTimeout(t.Context(), 600*time.Millisecond) defer cancel() if err := claimSocketReady(ctx, plan9D3Cmd(t, os.Getpid()), rt, ClaimLaunchInputs{}); err != nil { t.Fatalf("serving readiness on 404 from the exact pid: %v", err) } ctx, cancel = context.WithTimeout(t.Context(), 600*time.Millisecond) defer cancel() if err := claimSocketReady(ctx, plan9D3Cmd(t, os.Getppid()), rt, ClaimLaunchInputs{}); err == nil { t.Fatal("serving readiness accepted a descendant peer") } }