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 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// stagedFake is fakeBackend with spec §7.9's capability. It records which
18// build each worker spawn asked for (nil = "what the live link names") and
19// reports a synthetic identity, so a test can tell builds apart without real
20// files.
21type stagedFake struct {
22 *fakeBackend
23 smu sync.Mutex
24 onto []*release.StagedIdentity
25 // failNextStaged, when set, is what the next spawn onto a named build
26 // (onto != nil) returns instead of a child, once: the backend refusing a
27 // recycle.
28 failNextStaged error
29}
30
31func fakeIdentity(ver string) release.StagedIdentity {
32 return release.StagedIdentity{Version: ver, StagedPath: "/opt/carlos/versions/" + ver + "/app", Digest: "sha-of-" + ver}
33}
34
35func (b *stagedFake) StartWorkerStaged(ctx context.Context, rt registry.Route, env map[string]string, onto *release.StagedIdentity) (Proc, release.StagedIdentity, error) {
36 b.smu.Lock()
37 if onto != nil {
38 c := *onto
39 b.onto = append(b.onto, &c)
40 } else {
41 b.onto = append(b.onto, nil)
42 }
43 fail := b.failNextStaged
44 if onto != nil {
45 b.failNextStaged = nil
46 }
47 b.smu.Unlock()
48 if onto != nil && fail != nil {
49 return nil, release.StagedIdentity{}, fail
50 }
51 // ExecBackend's contract with the activator's before-exec hook (ruling
52 // R19 item 3): the hook runs with the identity about to be "exec'd", and
53 // a hook error means no child.
54 id := fakeIdentity(b.fakeBackend.liveVerNow())
55 if onto != nil {
56 id = *onto
57 }
58 if err := runBeforeExec(ctx, id); err != nil {
59 return nil, release.StagedIdentity{}, err
60 }
61 // And the caller's exec gate at the start, after the publication (Astra
62 // R3 §5 item 4): ExecBackend asks it at the cgroup manager's start.
63 if err := runExecGate(ctx); err != nil {
64 return nil, release.StagedIdentity{}, err
65 }
66 p, ver, err := b.fakeBackend.StartWorker(ctx, rt, env)
67 if err != nil {
68 return p, release.StagedIdentity{}, err
69 }
70 if onto != nil {
71 return p, *onto, nil
72 }
73 return p, fakeIdentity(ver), nil
74}
75
76// liveVerNow is the version the fake's next exec from the live link would
77// report, as StartWorker computes it.
78func (b *fakeBackend) liveVerNow() string {
79 b.mu.Lock()
80 defer b.mu.Unlock()
81 if b.execVerFn != nil {
82 return b.execVerFn()
83 }
84 return b.workerSpawnVer
85}
86
87func (b *stagedFake) calls() []*release.StagedIdentity {
88 b.smu.Lock()
89 defer b.smu.Unlock()
90 return append([]*release.StagedIdentity(nil), b.onto...)
91}
92
93// stageArgv0Worker stages a worker build at <OptDir>/versions/<ver>/app that
94// writes its own $0 to out and then sleeps, and points live/<host> at it. The
95// version is in the script, so each build hashes differently.
96func stageArgv0Worker(t *testing.T, b *ExecBackend, host, ver, out string) string {
97 t.Helper()
98 vdir := filepath.Join(b.OptDir, "versions", ver)
99 if err := os.MkdirAll(vdir, 0o755); err != nil {
100 t.Fatal(err)
101 }
102 bin := filepath.Join(vdir, "app")
103 script := "#!/bin/sh\n# build " + ver + "\nprintf '%s\\n' \"$0\" > '" + out + ".tmp' && mv '" + out + ".tmp' '" + out + "'\nexec sleep 30\n"
104 if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
105 t.Fatal(err)
106 }
107 if err := os.MkdirAll(filepath.Join(b.OptDir, "live"), 0o755); err != nil {
108 t.Fatal(err)
109 }
110 os.Remove(b.liveLink(host))
111 if err := os.Symlink(bin, b.liveLink(host)); err != nil {
112 t.Fatal(err)
113 }
114 return bin
115}
116
117func workerRoute(host string) registry.Route {
118 return registry.Route{Host: host, Kind: "sidecar", Args: `["sidecar","run"]`}
119}
120
121// Spec §7.9: the serving identity is the build captured at exec, so the exec
122// must be of the file the identity names, never of live/<host>.
123func TestExecBackendWorkerExecsTheStagedPathItResolved(t *testing.T) {
124 b := testExecBackend(t)
125 host := "bot.x.internal"
126 out := filepath.Join(t.TempDir(), "argv0")
127 bin := stageArgv0Worker(t, b, host, "v1", out)
128 p, id, err := b.StartWorkerStaged(context.Background(), workerRoute(host), nil, nil)
129 if err != nil {
130 t.Fatalf("StartWorkerStaged: %v", err)
131 }
132 defer p.Stop(context.Background())
133 if got := strings.TrimSpace(waitForFile(t, out, 5*time.Second)); got != bin {
134 t.Fatalf("$0 = %q; want the staged path %q, never live/%s", got, bin, host)
135 }
136 sum, err := release.FileSHA256(bin)
137 if err != nil {
138 t.Fatal(err)
139 }
140 if want := (release.StagedIdentity{Version: "v1", StagedPath: bin, Digest: sum}); id != want {
141 t.Fatalf("identity = %+v, want %+v", id, want)
142 }
143}
144
145// Spec §7.9: a recycle execs the captured build even after root has
146// repointed the link.
147func TestExecBackendWorkerRecycleExecsTheCapturedBuildAfterARepoint(t *testing.T) {
148 t.Parallel()
149 b := testExecBackend(t)
150 host := "bot.x.internal"
151 out1 := filepath.Join(t.TempDir(), "argv0-v1")
152 bin1 := stageArgv0Worker(t, b, host, "v1", out1)
153 p1, id1, err := b.StartWorkerStaged(context.Background(), workerRoute(host), nil, nil)
154 if err != nil {
155 t.Fatal(err)
156 }
157 waitForFile(t, out1, 5*time.Second)
158 p1.Stop(context.Background())
159 os.Remove(out1)
160
161 stageArgv0Worker(t, b, host, "v2", filepath.Join(t.TempDir(), "argv0-v2")) // root installs v2
162
163 p2, id2, err := b.StartWorkerStaged(context.Background(), workerRoute(host), nil, &id1)
164 if err != nil {
165 t.Fatalf("recycle onto v1: %v", err)
166 }
167 defer p2.Stop(context.Background())
168 if got := strings.TrimSpace(waitForFile(t, out1, 5*time.Second)); got != bin1 {
169 t.Fatalf("recycle exec'd %q, want the captured %q", got, bin1)
170 }
171 if id2 != id1 {
172 t.Fatalf("recycle reported %+v, want the captured %+v", id2, id1)
173 }
174}
175
176// Spec §4.2: a recovery whose staged copy has changed is refused, and nothing
177// runs.
178func TestExecBackendWorkerRecycleRefusesAChangedStagedCopy(t *testing.T) {
179 b := testExecBackend(t)
180 host := "bot.x.internal"
181 out := filepath.Join(t.TempDir(), "argv0")
182 bin := stageArgv0Worker(t, b, host, "v1", out)
183 p1, id1, err := b.StartWorkerStaged(context.Background(), workerRoute(host), nil, nil)
184 if err != nil {
185 t.Fatal(err)
186 }
187 waitForFile(t, out, 5*time.Second)
188 p1.Stop(context.Background())
189 os.Remove(out)
190 if err := os.WriteFile(bin, []byte("#!/bin/sh\n# tampered, and longer than the original\nexec sleep 30\n"), 0o755); err != nil {
191 t.Fatal(err)
192 }
193 p2, _, err := b.StartWorkerStaged(context.Background(), workerRoute(host), nil, &id1)
194 if !errors.Is(err, release.ErrStagedCopyChanged) {
195 t.Fatalf("recycle onto changed bytes = %v, want ErrStagedCopyChanged", err)
196 }
197 if p2 != nil {
198 p2.Stop(context.Background())
199 t.Fatal("a refused recycle still spawned a child")
200 }
201}
202
203func TestSplitBackendForwardsStagedWorkerStarts(t *testing.T) {
204 exec := &stagedFake{fakeBackend: &fakeBackend{workerSpawnVer: "v1"}}
205 sb := &SplitBackend{Exec: exec, Unit: &fakeBackend{workerSpawnVer: "u1"}}
206 rt := testWorkerRoute("bot.x.internal")
207 onto := fakeIdentity("v0")
208 if _, id, err := sb.StartWorkerStaged(context.Background(), rt, nil, &onto); err != nil || id != onto {
209 t.Fatalf("exec route: identity %+v, %v; want %+v forwarded", id, err, onto)
210 }
211 if calls := exec.calls(); len(calls) != 1 || calls[0] == nil || *calls[0] != onto {
212 t.Fatalf("exec backend saw %v, want one call onto v0", calls)
213 }
214 unit := rt
215 unit.Backing = registry.BackingUnit
216 if _, id, err := sb.StartWorkerStaged(context.Background(), unit, nil, &onto); err != nil || id != (release.StagedIdentity{Version: "u1"}) {
217 t.Fatalf("a backend without the capability: %+v, %v; want today's StartWorker answer", id, err)
218 }
219}
220
221// Ruling R19 item 3: ExecBackend runs the activator's before-exec hook with
222// the identity it resolved, before the child exists, and returns that same
223// identity.
224func TestExecBackendWorkerRunsTheBeforeExecHookFirst(t *testing.T) {
225 b := testExecBackend(t)
226 host := "bot.x.internal"
227 out := filepath.Join(t.TempDir(), "argv0")
228 stageArgv0Worker(t, b, host, "v1", out)
229 var seen []release.StagedIdentity
230 ctx := withBeforeExec(context.Background(), func(id release.StagedIdentity) error {
231 if _, err := os.Stat(out); err == nil {
232 t.Error("the child ran before the before-exec hook")
233 }
234 seen = append(seen, id)
235 return nil
236 })
237 p, id, err := b.StartWorkerStaged(ctx, workerRoute(host), nil, nil)
238 if err != nil {
239 t.Fatalf("StartWorkerStaged: %v", err)
240 }
241 t.Cleanup(func() { p.Stop(context.Background()) })
242 if len(seen) != 1 || seen[0] != id {
243 t.Fatalf("hook saw %+v; want exactly the identity exec'd, %+v", seen, id)
244 }
245 waitForFile(t, out, 5*time.Second)
246}
247
248// Ruling R19 item 3: a before-exec hook that fails means no child, for a
249// wake from the live link and for a recycle onto a captured build.
250func TestExecBackendWorkerDoesNotExecWhenTheHookFails(t *testing.T) {
251 t.Parallel()
252 b := testExecBackend(t)
253 host := "bot.x.internal"
254 out := filepath.Join(t.TempDir(), "argv0")
255 stageArgv0Worker(t, b, host, "v1", out)
256 id, err := release.ResolveLive(b.OptDir, host)
257 if err != nil {
258 t.Fatal(err)
259 }
260 refuse := errors.New("publication failed")
261 ctx := withBeforeExec(context.Background(), func(release.StagedIdentity) error { return refuse })
262 for name, onto := range map[string]*release.StagedIdentity{"wake": nil, "recycle": &id} {
263 p, _, err := b.StartWorkerStaged(ctx, workerRoute(host), nil, onto)
264 if !errors.Is(err, refuse) || p != nil {
265 t.Fatalf("%s: StartWorkerStaged = %v, %v; want no child and the hook's error", name, p, err)
266 }
267 }
268 time.Sleep(200 * time.Millisecond)
269 if _, err := os.Stat(out); err == nil {
270 t.Fatal("a child ran although the before-exec hook failed")
271 }
272}
273