carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bufio"
5 "context"
6 "errors"
7 "io"
8 "net"
9 "net/http"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "syscall"
15 "testing"
16 "time"
17)
18
19func TestMain(m *testing.M) {
20 if os.Getenv("ZDDPROBE_AS_MAIN") == "1" {
21 main()
22 os.Exit(0)
23 }
24 os.Exit(m.Run())
25}
26
27// launch starts this test binary as the probe with argv, as the platform
28// starts an instance: argv only, no shell. It returns the command and a
29// reader on the probe's stdout.
30func launch(t *testing.T, env []string, argv ...string) (*exec.Cmd, *bufio.Reader) {
31 t.Helper()
32 cmd := exec.Command(os.Args[0], argv...)
33 cmd.Env = append(append(os.Environ(), "ZDDPROBE_AS_MAIN=1"), env...)
34 cmd.Stderr = os.Stderr
35 out, err := cmd.StdoutPipe()
36 if err != nil {
37 t.Fatal(err)
38 }
39 if err := cmd.Start(); err != nil {
40 t.Fatal(err)
41 }
42 t.Cleanup(func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() })
43 return cmd, bufio.NewReader(out)
44}
45
46func unixClient(sock string) *http.Client {
47 return &http.Client{
48 Timeout: 10 * time.Second,
49 Transport: &http.Transport{
50 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
51 var d net.Dialer
52 return d.DialContext(ctx, "unix", sock)
53 },
54 },
55 }
56}
57
58// getOverUnix retries until the socket answers, with a 10 s cap, and returns
59// the body.
60func getOverUnix(t *testing.T, sock, path string) string {
61 t.Helper()
62 c := unixClient(sock)
63 deadline := time.Now().Add(10 * time.Second)
64 for {
65 resp, err := c.Get("http://probe" + path)
66 if err == nil {
67 defer resp.Body.Close()
68 b, err := io.ReadAll(resp.Body)
69 if err != nil {
70 t.Fatal(err)
71 }
72 return string(b)
73 }
74 if time.Now().After(deadline) {
75 t.Fatalf("probe never answered on %s: %v", sock, err)
76 }
77 time.Sleep(50 * time.Millisecond)
78 }
79}
80
81func TestProbeServesWithThePlatformsRunArgv(t *testing.T) {
82 dir := t.TempDir()
83 sock, db := filepath.Join(dir, "app.sock"), filepath.Join(dir, "app.db")
84 cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE=1", "CARLOS_HOST=shop.example.com"}, "--socket", sock, "--db", db) // backend_exec.go spawnUnitFrom
85 if body := getOverUnix(t, sock, "/healthz"); !strings.HasPrefix(body, "ok ") {
86 t.Fatalf("body %q", body)
87 }
88 if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" {
89 t.Fatalf("trace line %q, %v", line, err)
90 }
91
92 // SIGTERM drains: a request inside the handler still completes, then exit 0.
93 type result struct {
94 status int
95 body string
96 err error
97 }
98 done := make(chan result, 1)
99 go func() {
100 resp, err := unixClient(sock).Get("http://probe/inflight")
101 if err != nil {
102 done <- result{err: err}
103 return
104 }
105 defer resp.Body.Close()
106 b, err := io.ReadAll(resp.Body)
107 done <- result{resp.StatusCode, string(b), err}
108 }()
109 if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" {
110 t.Fatalf("trace line %q, %v", line, err)
111 }
112 if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
113 t.Fatal(err)
114 }
115 r := <-done
116 if r.err != nil || r.status != 200 || !strings.HasPrefix(r.body, "ok ") {
117 t.Fatalf("in-flight request: %d %q %v", r.status, r.body, r.err)
118 }
119 if err := cmd.Wait(); err != nil {
120 t.Fatalf("exit after SIGTERM: %v", err)
121 }
122}
123
124func TestProbeServesWithVetsArgv(t *testing.T) {
125 sock := filepath.Join(t.TempDir(), "app.sock")
126 // Vet's argv: no --db and no CARLOS_HOST. Rule 5 is a platform-launch
127 // check and does not apply.
128 launch(t, []string{"CARLOS_HOST="}, "-socket", sock) // internal/vet/vet.go bootHealthz: no --db, no CARLOS_HOST
129 if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") {
130 t.Fatalf("body %q", body)
131 }
132}
133
134func TestProbeIsSilentWithoutTrace(t *testing.T) {
135 sock := filepath.Join(t.TempDir(), "app.sock")
136 cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE=", "CARLOS_HOST=shop.example.com"}, "--socket", sock)
137 getOverUnix(t, sock, "/")
138 if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
139 t.Fatal(err)
140 }
141 rest, _ := io.ReadAll(stdout)
142 if len(rest) != 0 {
143 t.Fatalf("stdout %q without ZDDPROBE_TRACE", rest)
144 }
145 if err := cmd.Wait(); err != nil {
146 t.Fatalf("exit after SIGTERM: %v", err)
147 }
148}
149
150func TestProbeRequiresSocket(t *testing.T) {
151 cmd, _ := launch(t, []string{"CARLOS_HOST=shop.example.com"}, "--db", "x.db")
152 if err := cmd.Wait(); err == nil {
153 t.Fatal("probe without --socket exited 0")
154 }
155}
156
157// The flat flag-off name, <host>.sock, is served like app.sock.
158func TestProbeServesOnTheFlatHostSocketName(t *testing.T) {
159 sock := filepath.Join(t.TempDir(), "shop.example.com.sock")
160 launch(t, []string{"CARLOS_HOST=shop.example.com"}, "--socket", sock, "--db", "x.db")
161 if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") {
162 t.Fatalf("body %q", body)
163 }
164}
165
166// A vet boot is not checked whatever its socket is called: vet picks the
167// name, and CARLOS_HOST is not set.
168func TestProbeServesVetsArgvWithAnyName(t *testing.T) {
169 for _, form := range []string{"-socket", "--socket"} {
170 t.Run(form, func(t *testing.T) {
171 sock := filepath.Join(t.TempDir(), "probe.3.sock")
172 launch(t, []string{"CARLOS_HOST="}, form, sock)
173 if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") {
174 t.Fatalf("body %q", body)
175 }
176 })
177 }
178}
179
180// Rev 19, rule 5: a platform launch (it passes --db) whose socket file name
181// is not a flag-off one, or that has no CARLOS_HOST, exits 1 with a line on
182// stderr, before it listens, so a probe on such a launch fails its health
183// check as the tenant did. -db and --db are the same flag.
184func TestProbeRefusesALaunchARealAppMightRefuse(t *testing.T) {
185 for _, tc := range []struct {
186 name, host, file, want string
187 }{
188 {"generation file name", "shop.example.com", "app.3.sock", `--socket file name is "app.3.sock"`},
189 {"hashed file name", "shop.example.com", "0123456789ab.3.sock", "want app.sock or shop.example.com.sock"},
190 {"another host's name", "shop.example.com", "other.example.com.sock", `"other.example.com.sock"`},
191 {"no CARLOS_HOST, nested name", "", "app.sock", "CARLOS_HOST is empty"},
192 {"no CARLOS_HOST, flat name", "", "shop.example.com.sock", "CARLOS_HOST is empty"},
193 } {
194 for _, form := range []string{"--db", "-db"} {
195 t.Run(tc.name+" "+form, func(t *testing.T) {
196 refused(t, tc.host, tc.file, form, tc.want)
197 })
198 }
199 }
200}
201
202func refused(t *testing.T, host, file, dbFlag, want string) {
203 t.Helper()
204 sock := filepath.Join(t.TempDir(), file)
205 cmd := exec.Command(os.Args[0], "--socket", sock, dbFlag, "x.db")
206 cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1", "CARLOS_HOST="+host)
207 var stderr strings.Builder
208 cmd.Stderr = &stderr
209 err := cmd.Run()
210 var exit *exec.ExitError
211 if !errors.As(err, &exit) || exit.ExitCode() != 1 {
212 t.Fatalf("exit = %v, want status 1", err)
213 }
214 if !strings.Contains(stderr.String(), want) {
215 t.Fatalf("stderr %q, want it to say %q", stderr.String(), want)
216 }
217 if _, err := os.Lstat(sock); !os.IsNotExist(err) {
218 t.Fatalf("the probe listened before refusing (lstat %s: %v)", sock, err)
219 }
220}
221
222func TestCheckLaunch(t *testing.T) {
223 for _, tc := range []struct {
224 sock, host string
225 ok bool
226 }{
227 {"/run/carlos/instances/shop.example.com/g3/app.sock", "shop.example.com", true},
228 {"/run/carlos/instances/shop.example.com/app.sock", "shop.example.com", true},
229 {"/run/carlos/g/3/shop.example.com.sock", "shop.example.com", true},
230 {"/run/carlos/shop.example.com.sock", "shop.example.com", true},
231 {"/run/carlos/instances/shop.example.com/app.3.sock", "shop.example.com", false},
232 {"/run/carlos/g/0123456789ab.3.sock", "shop.example.com", false},
233 {"/run/carlos/shop.example.com.sock", "", false},
234 {"/run/carlos/.sock", "", false},
235 } {
236 if err := checkLaunch(tc.sock, tc.host); (err == nil) != tc.ok {
237 t.Errorf("checkLaunch(%q, %q) = %v, want ok %v", tc.sock, tc.host, err, tc.ok)
238 }
239 }
240}
241