carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "os"
9 "strconv"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/seeds"
17 "github.com/carlosframework/platform/internal/status"
18)
19
20func seedReadyBox(t *testing.T, fx *consoleFixture, label, placement string, protocol int, floor, prevAllowed, mode string, claim bool) {
21 t.Helper()
22 putSeedEdges(t, fx, label, `{"v":1,"box":"`+label+`","eip":"1.2.3.4","placement":"`+placement+
23 `","hibernation_configured":true,"hosts":{},"seeds_protocol":`+strconv.Itoa(protocol)+
24 `,"seeds_floor_min_release":"`+floor+`","prev_release":"v0.27.1","prev_seeds_allowed_for":"`+prevAllowed+`"}`)
25 putSeedHeartbeat(t, fx, label, mode, claim, time.Now())
26}
27
28// putSeedEdges writes a box's edges file, rewriting it in place: a test
29// moves one box through several states, and putEdges only creates.
30func putSeedEdges(t *testing.T, fx *consoleFixture, label, body string) {
31 t.Helper()
32 key := "control/edges/" + label + ".json"
33 _, etag, err := fx.rel.Get(context.Background(), key)
34 if err != nil && !errors.Is(err, os.ErrNotExist) {
35 t.Fatal(err)
36 }
37 if _, err := fx.rel.Put(context.Background(), key, []byte(body), etag); err != nil {
38 t.Fatal(err)
39 }
40}
41
42// putSeedHeartbeat writes the box's status heartbeat as of at; mode "" is
43// a box with no layout report.
44func putSeedHeartbeat(t *testing.T, fx *consoleFixture, label, mode string, claim bool, at time.Time) {
45 t.Helper()
46 putSeedHeartbeatIn(t, fx.rel, label, mode, claim, at)
47}
48
49func putSeedHeartbeatIn(t *testing.T, store release.Store, label, mode string, claim bool, at time.Time) {
50 t.Helper()
51 var layout *status.LayoutReport
52 if mode != "" {
53 layout = &status.LayoutReport{Mode: mode}
54 }
55 // A release, as every real heartbeat carries: without one a box with no
56 // layout report would publish no metrics at all.
57 b := status.CollectBox(nil, status.Probes{Release: "v0.27.1", Layout: layout, HostClaimEnabled: claim}, at)
58 b.Label = label
59 if err := status.WriteBox(context.Background(), store, b); err != nil {
60 t.Fatal(err)
61 }
62}
63
64func publishSeedsFloorFx(t *testing.T, fx *consoleFixture, rel string) {
65 t.Helper()
66 if err := seeds.PublishFloor(context.Background(), fx.rel, seeds.Floor{Min: seeds.RequiredProtocol, MinRelease: rel}); err != nil {
67 t.Fatal(err)
68 }
69}
70
71func TestSeedRegionReady(t *testing.T) {
72 fx := newConsoleFixture(t)
73 ctx := context.Background()
74 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "no seeds floor yet") {
75 t.Fatalf("no floor: %+v", ref)
76 }
77 publishSeedsFloorFx(t, fx, "v0.27.0")
78 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
79 if ref := fx.srv.seedRegionReady(ctx, "", false); ref != nil {
80 t.Fatalf("a ready region: %+v", ref)
81 }
82 seedReadyBox(t, fx, "kass-2", "", 0, "v0.27.0", "v0.27.0", "nested", false)
83 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "kass-2") || !strings.Contains(ref.Msg, "below the seeds floor") {
84 t.Fatalf("a box on protocol 0: %+v", ref)
85 }
86 seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "", "nested", false)
87 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "below the seeds floor") {
88 t.Fatalf("a carlos.prev without a seeds verdict: %+v", ref)
89 }
90 seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "flat", false)
91 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "nested") {
92 t.Fatalf("a flat box: %+v", ref)
93 }
94 seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "nested", true)
95 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "claim control") {
96 t.Fatalf("a claim-armed box: %+v", ref)
97 }
98 fx.srv.cfg.HostClaim = true
99 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "This console runs with claim control on") {
100 t.Fatalf("a claim console: %+v", ref)
101 }
102}
103
104func TestSeedRegionReadyOneBoxForImports(t *testing.T) {
105 fx := newConsoleFixture(t)
106 publishSeedsFloorFx(t, fx, "v0.27.0")
107 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
108 seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "nested", false)
109 if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref != nil {
110 t.Fatalf("two ready boxes, --from: %+v", ref)
111 }
112 if ref := fx.srv.seedRegionReady(context.Background(), "", true); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "more than one box") {
113 t.Fatalf("two ready boxes, import: %+v", ref)
114 }
115}
116
117func TestSeedRegionReadyRefusesAnUnconfirmedBox(t *testing.T) {
118 fx := newConsoleFixture(t)
119 publishSeedsFloorFx(t, fx, "v0.27.0")
120 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "", false) // no layout report
121 if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref == nil || ref.Status != http.StatusServiceUnavailable {
122 t.Fatalf("a box with no layout report: %+v", ref)
123 }
124}
125
126func TestSeedRegionReadyIgnoresOtherRegions(t *testing.T) {
127 fx := newConsoleFixture(t)
128 publishSeedsFloorFx(t, fx, "v0.27.0")
129 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
130 seedReadyBox(t, fx, "uk-1", "uk", 0, "", "", "flat", true)
131 if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref != nil {
132 t.Fatalf("a bad box in uk blocked the home region: %+v", ref)
133 }
134 if ref := fx.srv.seedRegionReady(context.Background(), "uk", false); ref == nil {
135 t.Fatal("uk's own bad box did not block uk")
136 }
137}
138
139// Every missing, zero or unparseable field is not ready. An old box's edges
140// file reads as zeros, and a box that could not read its own local floor
141// publishes the protocol but an empty floor: neither may pass, whatever the
142// rest of the file says.
143func TestSeedRegionReadyFailsClosedOnTheEdgesFile(t *testing.T) {
144 fx := newConsoleFixture(t)
145 ctx := context.Background()
146 publishSeedsFloorFx(t, fx, "v0.27.0")
147 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
148 for _, tc := range []struct{ name, body string }{
149 {"an old box", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{}}`},
150 {"an old box keeping no carlos.prev", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"prev_release":"none","seeds_floor_min_release":"v0.27.0"}`},
151 {"an unreadable local floor", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"prev_release":"none"}`},
152 {"a stale local floor", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.26.0","prev_release":"none"}`},
153 {"a carlos.prev vouched for an older floor", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0","prev_release":"v0.27.1","prev_seeds_allowed_for":"v0.26.0"}`},
154 {"no carlos.prev answer at all", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0"}`},
155 } {
156 putSeedEdges(t, fx, "kass-2", tc.body)
157 putSeedHeartbeat(t, fx, "kass-2", "nested", false, time.Now())
158 ref := fx.srv.seedRegionReady(ctx, "", false)
159 want := "kass-2 in the home region are below the seeds floor. Roll the platform there, then publish the seeds floor again."
160 if ref == nil || ref.Status != http.StatusConflict || ref.Msg != want {
161 t.Errorf("%s: %+v", tc.name, ref)
162 }
163 }
164 putSeedEdges(t, fx, "kass-2", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0","prev_release":"none"}`)
165 if ref := fx.srv.seedRegionReady(ctx, "", false); ref != nil {
166 t.Fatalf("a box keeping no carlos.prev: %+v", ref)
167 }
168}
169
170// A box in the edges files with no heartbeat at all, or a stale one, is
171// unconfirmed: never nested, never ready.
172func TestSeedRegionReadyRefusesAMissingOrStaleHeartbeat(t *testing.T) {
173 fx := newConsoleFixture(t)
174 ctx := context.Background()
175 publishSeedsFloorFx(t, fx, "v0.27.0")
176 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
177 putSeedEdges(t, fx, "kass-2", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0","prev_release":"none"}`)
178 want := "could not confirm every box in the home region. Try again in a minute."
179 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusServiceUnavailable || ref.Msg != want {
180 t.Fatalf("a box with no heartbeat: %+v", ref)
181 }
182 putSeedHeartbeat(t, fx, "kass-2", "nested", false, time.Now().Add(-time.Hour))
183 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusServiceUnavailable || ref.Msg != want {
184 t.Fatalf("a box with a stale heartbeat: %+v", ref)
185 }
186 putSeedHeartbeat(t, fx, "kass-2", "nested", false, time.Now())
187 if ref := fx.srv.seedRegionReady(ctx, "", false); ref != nil {
188 t.Fatalf("both boxes fresh: %+v", ref)
189 }
190 // A region with no edges file at all is unconfirmed too, named.
191 if ref := fx.srv.seedRegionReady(ctx, "uk", false); ref == nil || ref.Status != http.StatusServiceUnavailable || ref.Msg != "could not confirm every box in uk. Try again in a minute." {
192 t.Fatalf("a region with no box: %+v", ref)
193 }
194}
195
196// A published floor that PublishFloor would never write is unreadable, not
197// a floor: an empty min_release would match every box that could not read
198// its own.
199func TestSeedRegionReadyRefusesAnUnusableFloor(t *testing.T) {
200 fx := newConsoleFixture(t)
201 ctx := context.Background()
202 seedReadyBox(t, fx, "kass-1", "", 1, "", "", "nested", false)
203 for _, body := range []string{`{"min":1,"min_release":""}`, `{"min":1}`, `{"min":1,"min_release":"next"}`, `not json`} {
204 _, etag, err := fx.rel.Get(ctx, seeds.FloorKey)
205 if err != nil && !errors.Is(err, os.ErrNotExist) {
206 t.Fatal(err)
207 }
208 if _, err := fx.rel.Put(ctx, seeds.FloorKey, []byte(body), etag); err != nil {
209 t.Fatal(err)
210 }
211 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusServiceUnavailable || ref.Msg != "could not read the seeds floor. Try again." {
212 t.Errorf("floor %s: %+v", body, ref)
213 }
214 }
215}
216
217// The refusals name every box they found, sorted, and the region by its
218// name: the configured home region's, not the record's "".
219func TestSeedRegionReadyNamesBoxesAndRegion(t *testing.T) {
220 fx := newConsoleFixture(t)
221 fx.srv.cfg.HomeRegion = "eu"
222 ctx := context.Background()
223 publishSeedsFloorFx(t, fx, "v0.27.0")
224 seedReadyBox(t, fx, "kass-3", "eu", 1, "v0.27.0", "v0.27.0", "flat", false)
225 seedReadyBox(t, fx, "kass-1", "eu", 1, "v0.27.0", "v0.27.0", "flat", false)
226 seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "nested", true)
227 want := "kass-1, kass-3 in eu do not run instances nested yet. A seeded instance needs every box there nested."
228 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || ref.Msg != want {
229 t.Fatalf("two flat boxes: %+v", ref)
230 }
231 seedReadyBox(t, fx, "kass-3", "eu", 1, "v0.27.0", "v0.27.0", "nested", true)
232 seedReadyBox(t, fx, "kass-1", "eu", 1, "v0.27.0", "v0.27.0", "nested", false)
233 want = "kass-2, kass-3 in eu run with claim control on. Seeded instances are not supported there yet."
234 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || ref.Msg != want {
235 t.Fatalf("two claim boxes: %+v", ref)
236 }
237 if ref := fx.srv.seedRegionReady(ctx, "", true); ref == nil || ref.Status != http.StatusConflict || ref.Msg != "eu has more than one box. An import needs a region with one box for now." {
238 t.Fatalf("an import into three boxes: %+v", ref)
239 }
240}
241
242// A customer fleet's heartbeat is not a platform box's: a fresh, nested,
243// claim-free fleet heartbeat under the same label cannot stand in for a
244// stale platform box.
245func TestSeedRegionReadyIgnoresFleetHeartbeats(t *testing.T) {
246 fx := newConsoleFixture(t)
247 ctx := context.Background()
248 publishSeedsFloorFx(t, fx, "v0.27.0")
249 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
250 putSeedHeartbeat(t, fx, "kass-1", "nested", false, time.Now().Add(-time.Hour))
251 reg, err := registry.Open(fx.regPath)
252 if err != nil {
253 t.Fatal(err)
254 }
255 t.Cleanup(func() { reg.Close() })
256 if err := reg.UpsertFleet(registry.Fleet{Name: "pi", Account: fx.sqid, Bucket: "pi-bucket"}); err != nil {
257 t.Fatal(err)
258 }
259 putSeedHeartbeatIn(t, release.NewPrefixStore(fx.rel, "fleets/pi/"), "kass-1", "nested", false, time.Now())
260 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusServiceUnavailable {
261 t.Fatalf("a fleet heartbeat covered a stale platform box: %+v", ref)
262 }
263}
264
265// Two platform heartbeats claiming one label are ambiguous: unconfirmed,
266// whichever the read happens to see last.
267func TestSeedRegionReadyRefusesADuplicateLabel(t *testing.T) {
268 fx := newConsoleFixture(t)
269 ctx := context.Background()
270 publishSeedsFloorFx(t, fx, "v0.27.0")
271 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
272 b := status.CollectBox(nil, status.Probes{Release: "v0.27.1", Layout: &status.LayoutReport{Mode: "flat"}}, time.Now())
273 b.Label = "kass-1"
274 body, err := json.Marshal(b)
275 if err != nil {
276 t.Fatal(err)
277 }
278 if _, err := fx.rel.Put(ctx, "status/boxes/kass-1-old.json", body, ""); err != nil {
279 t.Fatal(err)
280 }
281 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusServiceUnavailable {
282 t.Fatalf("two heartbeats for kass-1: %+v", ref)
283 }
284}
285
286// The floor's own min protocol binds too, when it is above the one this
287// console requires.
288func TestSeedRegionReadyHonoursTheFloorsProtocol(t *testing.T) {
289 fx := newConsoleFixture(t)
290 ctx := context.Background()
291 if err := seeds.PublishFloor(ctx, fx.rel, seeds.Floor{Min: seeds.RequiredProtocol + 1, MinRelease: "v0.27.0"}); err != nil {
292 t.Fatal(err)
293 }
294 seedReadyBox(t, fx, "kass-1", "", seeds.RequiredProtocol, "v0.27.0", "v0.27.0", "nested", false)
295 want := "kass-1 in the home region are below the seeds floor. Roll the platform there, then publish the seeds floor again."
296 if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || ref.Msg != want {
297 t.Fatalf("a box below the floor's protocol: %+v", ref)
298 }
299 seedReadyBox(t, fx, "kass-1", "", seeds.RequiredProtocol+1, "v0.27.0", "v0.27.0", "nested", false)
300 if ref := fx.srv.seedRegionReady(ctx, "", false); ref != nil {
301 t.Fatalf("a box at the floor's protocol: %+v", ref)
302 }
303}
304
305// A caller may name the home region rather than pass the record's "".
306func TestSeedRegionReadyAcceptsTheHomeRegionsName(t *testing.T) {
307 fx := newConsoleFixture(t)
308 fx.srv.cfg.HomeRegion = "eu"
309 ctx := context.Background()
310 publishSeedsFloorFx(t, fx, "v0.27.0")
311 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
312 for _, region := range []string{"", "eu"} {
313 if ref := fx.srv.seedRegionReady(ctx, region, false); ref != nil {
314 t.Errorf("region %q: %+v", region, ref)
315 }
316 }
317 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "flat", false)
318 want := "kass-1 in eu do not run instances nested yet. A seeded instance needs every box there nested."
319 if ref := fx.srv.seedRegionReady(ctx, "eu", false); ref == nil || ref.Msg != want {
320 t.Fatalf("region by name, a flat box: %+v", ref)
321 }
322}
323
324func TestSeedsFloorDoorIsForSystemOperators(t *testing.T) {
325 fx := newConsoleFixture(t)
326 if err := fx.st.AddMember(OpsAccountID, "member@keymail.dev", RoleMember, time.Now()); err != nil {
327 t.Fatal(err)
328 }
329 member := cliToken(t, fx, "member@keymail.dev", "code-seeds-member")
330 _, want := getRaw(t, fx.ts.URL+"/api/cli/system/no-such-door", member)
331 if st, body := postRaw(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]string{"min_release": "v0.27.0"}, member); st != http.StatusNotFound || body != want {
332 t.Fatalf("a member = %d %q, want the bare 404", st, body)
333 }
334 owner := cliToken(t, fx, "paul@keymail.dev", "code-seeds-owner")
335 for _, bad := range []string{"latest", "", "0.27.0"} {
336 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": bad}, owner); st != http.StatusBadRequest {
337 t.Fatalf("a bad release %q = %d %v, want 400", bad, st, out)
338 }
339 }
340 if f, err := seeds.ReadFloor(context.Background(), fx.rel); err != nil || f != nil {
341 t.Fatalf("a refused publish wrote a floor: %+v, %v", f, err)
342 }
343 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.27.0"}, owner); st != http.StatusOK || out["min_release"] != "v0.27.0" {
344 t.Fatalf("publish = %d %v", st, out)
345 }
346 st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.26.0"}, owner)
347 if st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "already v0.27.0") {
348 t.Fatalf("a lower floor = %d %v, want 409 naming v0.27.0", st, out)
349 }
350 f, err := seeds.ReadFloor(context.Background(), fx.rel)
351 if err != nil || f == nil || f.MinRelease != "v0.27.0" || f.Min != seeds.RequiredProtocol {
352 t.Fatalf("floor = %+v, %v", f, err)
353 }
354}
355
356func TestPlan14SeedsFloorMixedFleet(t *testing.T) {
357 fx := newConsoleFixture(t)
358 publishSeedsFloorFx(t, fx, "v0.27.0")
359 seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
360 for _, extra := range []string{``, `,"moves_floor_state":"accepted"`, `,"moves_floor_state":"accepted","seeds_floor_state":"quarantined","seeds_floor_generation":1`, `,"moves_floor_state":"accepted","seeds_floor_state":"accepted","seeds_floor_generation":1`} {
361 putSeedEdges(t, fx, "kass-2", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0","prev_release":"none"`+extra+`}`)
362 putSeedHeartbeat(t, fx, "kass-2", "nested", false, time.Now())
363 ref := fx.srv.seedRegionReady(t.Context(), "", false)
364 accepted := extra == "" || strings.Contains(extra, `"seeds_floor_state":"accepted"`)
365 if (ref == nil) != accepted {
366 t.Fatal("mixed fleet", extra, ref)
367 }
368 }
369}
370