carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "encoding/json"
6 "net"
7 "net/http"
8 "os"
9 "path/filepath"
10 "slices"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/instancectl"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18)
19
20// shortRunDir is a RunDirOverride short enough for sockets under it: the
21// test name is not part of the path (t.TempDir's would be), so a long test
22// name cannot push a socket past sockaddr_un's limit. Run socket tests with
23// TMPDIR=/tmp/k.
24func shortRunDir(t *testing.T) string {
25 t.Helper()
26 dir, err := os.MkdirTemp("", "g")
27 if err != nil {
28 t.Fatal(err)
29 }
30 t.Cleanup(func() { os.RemoveAll(dir) })
31 return dir
32}
33
34// flatGenRoute is a route that classifies as FLAT against b: the default
35// db path and <RunDirOverride>/<host>.sock.
36func flatGenRoute(b *ExecBackend, host string) registry.Route {
37 return registry.Route{Host: host, Kind: "instance", Hibernate: true,
38 DBPath: provision.DefaultDBPath(b.DataDir, host),
39 Socket: filepath.Join(b.RunDirOverride, host+".sock")}
40}
41
42func isSocket(path string) bool {
43 fi, err := os.Lstat(path)
44 return err == nil && fi.Mode()&os.ModeSocket != 0
45}
46
47func isRegular(path string) bool {
48 fi, err := os.Lstat(path)
49 return err == nil && fi.Mode().IsRegular()
50}
51
52// Flag off: StartGen(perGen=false) is Start, byte for byte. The child gets
53// the canonical socket in argv and the canonical control socket in env.
54func TestStartGenWithGenerationsOffIsStart(t *testing.T) {
55 b := testExecBackend(t)
56 host := "gen-off.example.com"
57 argvPath, envPath := stageRecordingLive(t, b, host, "v1")
58 rt := registry.Route{Host: host, Kind: "instance", Socket: filepath.Join(shortRunDir(t), "instance.sock")}
59 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
60 defer cancel()
61 done := make(chan struct{})
62 var proc Proc
63 go func() {
64 proc, _, _ = b.StartGen(ctx, rt, 5, false) // the recorder never binds; a health error is expected
65 close(done)
66 }()
67 gotArgv := strings.Fields(waitForFile(t, argvPath, time.Second))
68 gotEnv := strings.TrimSpace(waitForFile(t, envPath, time.Second))
69 cancel() // recorded: end the health wait the recorder never answers
70 <-done
71 if proc != nil {
72 defer proc.Stop(context.Background())
73 }
74 wantArgv := []string{"--socket", rt.Socket, "--db", filepath.Join(b.DataDir, host+".db")}
75 if !slices.Equal(gotArgv, wantArgv) {
76 t.Fatalf("argv = %v, want %v (today's argv, unchanged)", gotArgv, wantArgv)
77 }
78 if gotEnv != rt.Socket+".control" {
79 t.Fatalf("CARLOS_CONTROL_SOCKET = %q, want %q", gotEnv, rt.Socket+".control")
80 }
81}
82
83// Flag off, healthy: the handle names the canonical socket the app bound.
84func TestStartGenWithGenerationsOffBindsTheCanonicalSocket(t *testing.T) {
85 requireUnixSockets(t)
86 b := testExecBackend(t)
87 b.RunDirOverride = shortRunDir(t)
88 host := "canon.example.com"
89 stageServingLive(t, b, host)
90 rt := flatGenRoute(b, host)
91 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
92 defer cancel()
93 proc, g, err := b.StartGen(ctx, rt, 11, false)
94 if err != nil {
95 t.Fatalf("StartGen: %v", err)
96 }
97 defer proc.Stop(context.Background())
98 if g.N != 11 || g.Socket != rt.Socket || g.Control != rt.Socket+".control" {
99 t.Fatalf("handle = %+v, want the canonical socket and control", g)
100 }
101 if !isSocket(rt.Socket) {
102 t.Fatalf("%s is not a bound socket", rt.Socket)
103 }
104 if _, err := os.Stat(filepath.Join(b.RunDirOverride, "g")); !os.IsNotExist(err) {
105 t.Fatalf("the generation directory exists with generations off (stat: %v)", err)
106 }
107}
108
109// Flag on, flat: the host binds its generation socket, g/<n>/<host>.sock
110// (rev 19), and its control socket beside it, and only the generation's OWN
111// stale file is removed.
112func TestStartGenFlatBindsItsOwnGenerationSocket(t *testing.T) {
113 requireUnixSockets(t)
114 b := testExecBackend(t)
115 b.RunDirOverride = shortRunDir(t)
116 host := "shop.example.com"
117 stageServingLive(t, b, host)
118 rt := flatGenRoute(b, host)
119 if l, _ := b.Layout(rt); l != LayoutFlat {
120 t.Fatalf("fixture classifies %q, want flat", l)
121 }
122 own := genSocketPathIn(filepath.Join(b.RunDirOverride, "g"), host, 9)
123 sibling := genSocketPathIn(filepath.Join(b.RunDirOverride, "g"), host, 8)
124 if want := filepath.Join(b.RunDirOverride, "g", "9", host+".sock"); own != want {
125 t.Fatalf("own generation path %s, want %s", own, want)
126 }
127 for _, p := range []string{own, sibling} {
128 if err := os.MkdirAll(filepath.Dir(p), 0o750); err != nil {
129 t.Fatal(err)
130 }
131 if err := os.WriteFile(p, []byte("stale"), 0o600); err != nil {
132 t.Fatal(err)
133 }
134 }
135 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
136 defer cancel()
137 proc, g, err := b.StartGen(ctx, rt, 9, true)
138 if err != nil {
139 t.Fatalf("StartGen: %v", err)
140 }
141 defer proc.Stop(context.Background())
142 if g.N != 9 || g.Socket != own || g.Control != own+".control" {
143 t.Fatalf("handle = %+v, want socket %s", g, own)
144 }
145 if !isSocket(own) {
146 t.Fatalf("own generation path %s is not a bound socket: the stale file was not removed", own)
147 }
148 if !isSocket(g.Control) {
149 t.Fatalf("control socket %s was not bound", g.Control)
150 }
151 if !isRegular(sibling) {
152 t.Fatalf("sibling generation's file %s was touched; only the own path may be removed", sibling)
153 }
154 if _, err := os.Lstat(rt.Socket); !os.IsNotExist(err) {
155 t.Fatalf("canonical path %s exists with generations on (lstat: %v)", rt.Socket, err)
156 }
157}
158
159// Flag on, nested: the launch file names g<n>/app.sock (rev 19), the
160// control env follows it, and prepare removes only that generation's stale
161// file.
162func TestStartGenNestedUsesItsOwnDirectory(t *testing.T) {
163 t.Parallel()
164 ctl := &fakeControl{inv: "inv1"}
165 unit := &fakeUnit{}
166 b, rt := nestedBackend(t, ctl, unit)
167 runDir := filepath.Dir(rt.Socket)
168 own := filepath.Join(runDir, "g9", "app.sock")
169 sibling := filepath.Join(runDir, "g8", "app.sock")
170 for _, p := range []string{rt.Socket, sibling, own} {
171 if err := os.MkdirAll(filepath.Dir(p), 0o750); err != nil {
172 t.Fatal(err)
173 }
174 if err := os.WriteFile(p, []byte("stale"), 0o600); err != nil {
175 t.Fatal(err)
176 }
177 }
178 var stopServing func()
179 ctl.onStart = func() { stopServing = serveOK(t, own) }
180 defer func() {
181 if stopServing != nil {
182 stopServing()
183 }
184 }()
185 unit.set("active", "inv1", true, true)
186 p, g, err := b.StartGen(context.Background(), rt, 9, true)
187 if err != nil {
188 t.Fatalf("StartGen: %v", err)
189 }
190 if g.Socket != own || g.Control != own+".control" {
191 t.Fatalf("handle = %+v, want %s", g, own)
192 }
193 raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), "launch.run.json"))
194 if err != nil {
195 t.Fatal(err)
196 }
197 var l instancectl.Launch
198 if err := json.Unmarshal(raw, &l); err != nil {
199 t.Fatal(err)
200 }
201 if len(l.Argv) != 5 || l.Argv[1] != "--socket" || l.Argv[2] != own {
202 t.Fatalf("launch argv = %v, want --socket %s", l.Argv, own)
203 }
204 if !slices.Contains(l.Env, "CARLOS_CONTROL_SOCKET="+own+".control") {
205 t.Fatalf("launch env lacks CARLOS_CONTROL_SOCKET=%s.control: %v", own, l.Env)
206 }
207 if !isRegular(rt.Socket) || !isRegular(sibling) {
208 t.Fatal("prepare removed a file that is not this generation's own socket")
209 }
210 unit.set("inactive", "inv1", false, false)
211 waitFor(t, "the unit to exit", func() bool { return p.Exited() })
212 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
213 defer cancel()
214 _ = p.Stop(ctx)
215}
216
217// NextDueVia dials the socket it is given, not rt.Socket.
218func TestNextDueViaDialsTheGivenSocket(t *testing.T) {
219 requireUnixSockets(t)
220 b := testExecBackend(t)
221 host := "due.example.com"
222 if err := os.WriteFile(filepath.Join(b.DataDir, host+".env"), []byte("CARLOS_ADMIN_TOKEN=tok\n"), 0o600); err != nil {
223 t.Fatal(err)
224 }
225 sock := tmpSock(t)
226 ln, err := net.Listen("unix", sock)
227 if err != nil {
228 t.Fatal(err)
229 }
230 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
231 if r.URL.Path != "/api/next-due" || r.Header.Get("Authorization") != "Bearer tok" {
232 http.Error(w, "no", http.StatusForbidden)
233 return
234 }
235 w.Write([]byte(`{"due":42}`))
236 })}
237 go srv.Serve(ln)
238 defer srv.Close()
239 rt := registry.Route{Host: host, Kind: "instance", Socket: "/nonexistent/canonical.sock"}
240 due, err := b.NextDueVia(context.Background(), rt, sock)
241 if err != nil || due != 42 {
242 t.Fatalf("NextDueVia = %d, %v; want 42 from the generation socket", due, err)
243 }
244 if _, err := b.NextDue(context.Background(), rt); err == nil {
245 t.Fatal("NextDue reached something at the canonical path; the fixture has nothing there")
246 }
247}
248
249// A unit-backed route has no generation sockets: SplitBackend starts it
250// with Start and hands back the canonical handle, whatever perGen says.
251func TestSplitBackendStartGenKeepsUnitTenantsCanonical(t *testing.T) {
252 unitSide := &fakeBackend{execVerFn: func() string { return "v3" }}
253 sb := &SplitBackend{Exec: &fakeBackend{}, Unit: unitSide}
254 rt := registry.Route{Host: "u.example.com", Kind: "instance", Backing: registry.BackingUnit, Socket: "/run/carlos/u.example.com.sock"}
255 _, g, err := sb.StartGen(context.Background(), rt, 4, true)
256 if err != nil {
257 t.Fatal(err)
258 }
259 if g != canonicalGen(rt, 4, "v3") {
260 t.Fatalf("handle = %+v, want the canonical one", g)
261 }
262 if unitSide.starts != 1 {
263 t.Fatalf("unit side Start calls = %d, want 1", unitSide.starts)
264 }
265}
266