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 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/registry"
16)
17
18// serveOn binds an HTTP server at sock that answers body, the way a
19// generation's process would. It returns the server so a test can stop it.
20func serveOn(t *testing.T, sock, body string) *http.Server {
21 t.Helper()
22 ln, err := net.Listen("unix", sock)
23 if err != nil {
24 if strings.Contains(err.Error(), "operation not permitted") {
25 t.Skipf("sandbox forbids unix sockets: %v", err)
26 }
27 t.Fatalf("listen %s: %v", sock, err)
28 }
29 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, body) })}
30 go srv.Serve(ln)
31 t.Cleanup(func() { srv.Close() })
32 return srv
33}
34
35// dialAsOutsider GETs / over a plain unix dial of path, the way a member of
36// the carlos-sockets group dials an instance: no unixdial checks, so the
37// kernel follows a symlink at path.
38func dialAsOutsider(t *testing.T, path string) string {
39 t.Helper()
40 c := &http.Client{Timeout: 2 * time.Second, Transport: &http.Transport{
41 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
42 return (&net.Dialer{}).DialContext(ctx, "unix", path)
43 },
44 }}
45 resp, err := c.Get("http://carlos/")
46 if err != nil {
47 t.Fatalf("outside dial of %s: %v", path, err)
48 }
49 defer resp.Body.Close()
50 b, _ := io.ReadAll(resp.Body)
51 return string(b)
52}
53
54// servingProc is a fake child whose Stop also stops its server, so the
55// generation's socket goes away with it.
56type servingProc struct {
57 Proc
58 srv *http.Server
59}
60
61func (p *servingProc) Stop(ctx context.Context) error {
62 p.srv.Close()
63 return p.Proc.Stop(ctx)
64}
65
66// sockGenBackend is genFakeBackend whose generations really serve: perGen
67// binds <dir>/g<n>.sock, otherwise rt.Socket, each answering "gen <n>".
68type sockGenBackend struct {
69 *fakeBackend
70 t *testing.T
71 dir string
72}
73
74func (b *sockGenBackend) StartGen(ctx context.Context, rt registry.Route, n uint64, perGen bool) (Proc, Gen, error) {
75 proc, ver, err := b.fakeBackend.Start(ctx, rt)
76 if err != nil {
77 return proc, Gen{}, err
78 }
79 sock := rt.Socket
80 if perGen {
81 sock = filepath.Join(b.dir, fmt.Sprintf("g%d.sock", n))
82 }
83 srv := serveOn(b.t, sock, fmt.Sprintf("gen %d", n))
84 return &servingProc{Proc: proc, srv: srv}, Gen{N: n, Socket: sock, Control: sock + ".control", Build: ver}, nil
85}
86
87func (b *sockGenBackend) NextDueVia(ctx context.Context, rt registry.Route, _ string) (int64, error) {
88 return b.fakeBackend.NextDue(ctx, rt)
89}
90
91// sockGenActivator is a Service over sockGenBackend, its sockets in a short
92// directory (run with TMPDIR=/tmp/k), and a route whose canonical socket is
93// <dir>/c.sock.
94func sockGenActivator(t *testing.T, on bool) (*Service, registry.Route) {
95 t.Helper()
96 withGenerations(t, on)
97 dir := shortRunDir(t)
98 reg, err := registry.Open(filepath.Join(dir, "r.db"))
99 if err != nil {
100 t.Fatal(err)
101 }
102 a := New(Config{Bucket: "test-bucket", Idle: time.Hour}, &sockGenBackend{fakeBackend: &fakeBackend{}, t: t, dir: dir}, reg)
103 rt := testRoute("link.example.com")
104 rt.Socket = filepath.Join(dir, "c.sock")
105 return a, rt
106}
107
108func isSymlink(path string) bool {
109 fi, err := os.Lstat(path)
110 return err == nil && fi.Mode()&os.ModeSymlink != 0
111}
112
113// The helper plan 3 calls at every switch: it replaces whatever is at the
114// canonical path, and leaves no temporary link behind.
115func TestSwapCanonicalLinkReplacesAtomically(t *testing.T) {
116 dir := t.TempDir()
117 canonical := filepath.Join(dir, "c.sock")
118 if err := os.WriteFile(canonical, []byte("stale socket file"), 0o600); err != nil {
119 t.Fatal(err)
120 }
121 for _, target := range []string{"/run/carlos/g/aaaaaaaaaaaa.1.sock", "/run/carlos/g/aaaaaaaaaaaa.2.sock"} {
122 if err := swapCanonicalLink(canonical, target); err != nil {
123 t.Fatalf("swapCanonicalLink(%s): %v", target, err)
124 }
125 if got, err := os.Readlink(canonical); err != nil || got != target {
126 t.Fatalf("canonical names %q (%v), want %q", got, err, target)
127 }
128 }
129 if _, err := os.Lstat(canonical + ".lnk"); !os.IsNotExist(err) {
130 t.Fatalf("temporary link left behind (lstat: %v)", err)
131 }
132}
133
134// A process dialling the canonical path follows the link to whichever
135// generation it names, before and after a switch.
136func TestSwapCanonicalLinkMovesOutsideDiallers(t *testing.T) {
137 dir := shortRunDir(t)
138 g1, g2 := filepath.Join(dir, "g1.sock"), filepath.Join(dir, "g2.sock")
139 serveOn(t, g1, "one")
140 serveOn(t, g2, "two")
141 canonical := filepath.Join(dir, "c.sock")
142 if err := swapCanonicalLink(canonical, g1); err != nil {
143 t.Fatal(err)
144 }
145 if got := dialAsOutsider(t, canonical); got != "one" {
146 t.Fatalf("before the switch: %q, want one", got)
147 }
148 if err := swapCanonicalLink(canonical, g2); err != nil {
149 t.Fatal(err)
150 }
151 if got := dialAsOutsider(t, canonical); got != "two" {
152 t.Fatalf("after the switch: %q, want two", got)
153 }
154}
155
156func TestRemoveCanonicalLinkRemovesOnlyItsOwnLink(t *testing.T) {
157 dir := t.TempDir()
158 canonical := filepath.Join(dir, "c.sock")
159 if err := swapCanonicalLink(canonical, "/a"); err != nil {
160 t.Fatal(err)
161 }
162 if err := removeCanonicalLink(canonical, "/b"); err != nil || !isSymlink(canonical) {
163 t.Fatalf("a link to another generation was removed (err %v)", err)
164 }
165 if err := removeCanonicalLink(canonical, "/a"); err != nil {
166 t.Fatal(err)
167 }
168 if _, err := os.Lstat(canonical); !os.IsNotExist(err) {
169 t.Fatalf("own link not removed (lstat: %v)", err)
170 }
171 if err := os.WriteFile(canonical, nil, 0o600); err != nil {
172 t.Fatal(err)
173 }
174 if err := removeCanonicalLink(canonical, "/a"); err != nil || !isRegular(canonical) {
175 t.Fatalf("a real file at the canonical path was removed (err %v)", err)
176 }
177}
178
179// Spec §7.1 rev 15, end to end through the Service: the link appears when
180// the generation becomes current, an outside dialler reaches that
181// generation through it, and it goes when the entry stops.
182func TestCanonicalPathLinksToTheCurrentGeneration(t *testing.T) {
183 requireUnixSockets(t)
184 a, rt := sockGenActivator(t, true)
185 arrangeAwake(t, a, rt)
186 g, _ := a.ServingGen(rt.Host)
187 if target, err := os.Readlink(rt.Socket); err != nil || target != g.Socket {
188 t.Fatalf("canonical path names %q (%v), want the current generation %q", target, err, g.Socket)
189 }
190 if got, want := dialAsOutsider(t, rt.Socket), fmt.Sprintf("gen %d", g.N); got != want {
191 t.Fatalf("outside dial of the canonical path got %q, want %q", got, want)
192 }
193 stopAndWait(t, a, rt.Host)
194 if _, err := os.Lstat(rt.Socket); !os.IsNotExist(err) {
195 t.Fatalf("canonical link outlived its entry (lstat: %v)", err)
196 }
197}
198
199// Flag off: the canonical path is the app's own socket, never a link.
200func TestCanonicalPathIsARealSocketWithGenerationsOff(t *testing.T) {
201 requireUnixSockets(t)
202 a, rt := sockGenActivator(t, false)
203 arrangeAwake(t, a, rt)
204 if isSymlink(rt.Socket) || !isSocket(rt.Socket) {
205 t.Fatalf("%s is not a real socket with generations off", rt.Socket)
206 }
207 g, _ := a.ServingGen(rt.Host)
208 if got, want := dialAsOutsider(t, rt.Socket), fmt.Sprintf("gen %d", g.N); got != want {
209 t.Fatalf("outside dial got %q, want %q", got, want)
210 }
211}
212
213// A generation that IS on the canonical socket (a unit tenant, a backend
214// without generations) is never linked, even with the switch on: the link
215// would replace the real socket.
216func TestNoCanonicalLinkWhenTheGenerationOwnsTheCanonicalSocket(t *testing.T) {
217 withGenerations(t, true)
218 a, _, _, _ := testActivator(t, time.Hour)
219 rt := genTestRoute(t, "plain-link.example.com")
220 arrangeAwake(t, a, rt)
221 if _, err := os.Lstat(rt.Socket); !os.IsNotExist(err) {
222 t.Fatalf("something was created at the canonical path (lstat: %v)", err)
223 }
224}
225
226// Flag off, after an agent that ran with generations on died without
227// stopping its entries: the leftover link is cleared so the app can bind.
228func TestStartGenWithGenerationsOffClearsALeftoverCanonicalLink(t *testing.T) {
229 requireUnixSockets(t)
230 b := testExecBackend(t)
231 b.RunDirOverride = shortRunDir(t)
232 host := "leftover.example.com"
233 stageServingLive(t, b, host)
234 rt := flatGenRoute(b, host)
235 if err := os.Symlink(filepath.Join(b.RunDirOverride, "g", "gone.1.sock"), rt.Socket); err != nil {
236 t.Fatal(err)
237 }
238 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
239 defer cancel()
240 proc, _, err := b.StartGen(ctx, rt, 1, false)
241 if err != nil {
242 t.Fatalf("StartGen over a leftover link: %v", err)
243 }
244 defer proc.Stop(context.Background())
245 if !isSocket(rt.Socket) {
246 t.Fatalf("%s is not the app's socket", rt.Socket)
247 }
248}
249