| index ffad5a956..1efa33faf 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -14,6 +14,7 @@ package main_test |
| import ( |
| "context" |
| "crypto/ed25519" |
| + "fmt" |
| "io" |
| "net" |
| "net/http" |
| @@ -88,6 +89,85 @@ func e2eShipBinary(t *testing.T, store release.Store, version string, bin []byte |
| } |
| } |
| |
| +// The carlos-app@.socket listener handoff, systemd-free: the test plays |
| +// root systemd — binds the tenant's unix socket itself, passes the fd as fd |
| +// 3 with LISTEN_FDS=1 — and proves the tenant serves on the inherited |
| +// listener without ever touching the socket path. This is the contract that |
| +// lets the socket file stay carlos-owned 0600 (only the edge connects, |
| +// tenants can't reach each other) while a DynamicUser tenant owns the |
| +// process. |
| +func TestTenantInheritsActivationSocket(t *testing.T) { |
| + goBin, err := exec.LookPath("go") |
| + if err != nil { |
| + t.Skip("go toolchain not found") |
| + } |
| + tmp := t.TempDir() |
| + appBin := filepath.Join(tmp, "echoapp") |
| + build := exec.Command(goBin, "build", "-o", appBin, "./testdata/echoapp") |
| + build.Env = os.Environ() |
| + if out, err := build.CombinedOutput(); err != nil { |
| + t.Fatalf("build echoapp: %v\n%s", err, out) |
| + } |
| + |
| + // Short path — sun_path is ~104 bytes and t.TempDir() can blow it. |
| + sock := filepath.Join(os.TempDir(), fmt.Sprintf("carlos-e2e-%d.sock", os.Getpid())) |
| + os.Remove(sock) |
| + t.Cleanup(func() { os.Remove(sock) }) |
| + ln, err := net.Listen("unix", sock) |
| + if err != nil { |
| + if strings.Contains(err.Error(), "operation not permitted") { |
| + t.Skipf("unix-socket listeners not permitted in this environment: %v", err) |
| + } |
| + t.Fatalf("listen unix: %v", err) |
| + } |
| + defer ln.Close() |
| + f, err := ln.(*net.UnixListener).File() |
| + if err != nil { |
| + t.Fatalf("listener fd: %v", err) |
| + } |
| + defer f.Close() |
| + |
| + app := exec.Command(appBin) // no -socket, no -addr: the fd is the listener |
| + app.ExtraFiles = []*os.File{f} // child fd 3, where sd_listen_fds starts |
| + app.Env = append(os.Environ(), "LISTEN_FDS=1", "BUILD=sockv1") |
| + if err := app.Start(); err != nil { |
| + t.Fatalf("start echoapp: %v", err) |
| + } |
| + t.Cleanup(func() { |
| + _ = app.Process.Kill() |
| + _, _ = app.Process.Wait() |
| + }) |
| + |
| + client := &http.Client{Transport: &http.Transport{ |
| + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| + return (&net.Dialer{}).DialContext(ctx, "unix", sock) |
| + }, |
| + }} |
| + get := func(path string) (int, string, error) { |
| + resp, err := client.Get("http://tenant.local" + path) |
| + if err != nil { |
| + return 0, "", err |
| + } |
| + defer resp.Body.Close() |
| + b, _ := io.ReadAll(resp.Body) |
| + return resp.StatusCode, string(b), nil |
| + } |
| + deadline := time.Now().Add(10 * time.Second) |
| + for { |
| + code, body, err := get("/healthz") |
| + if err == nil && code == 200 && body == "ok" { |
| + break |
| + } |
| + if time.Now().After(deadline) { |
| + t.Fatalf("tenant never healthy on the inherited listener: %d %q %v", code, body, err) |
| + } |
| + time.Sleep(100 * time.Millisecond) |
| + } |
| + if code, body, err := get("/"); err != nil || code != 200 || !strings.Contains(body, "BUILD=sockv1") { |
| + t.Fatalf("GET / over inherited socket = %d %q %v, want 200 containing BUILD=sockv1", code, body, err) |
| + } |
| +} |
| + |
| func TestEndToEndSmoke(t *testing.T) { |
| goBin, err := exec.LookPath("go") |
| if err != nil { |