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 "errors"
6 "net/http"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// The layout door is the update door's gate, byte for byte: a member of
15// the ops account gets the bare 404 an unknown path gives, and a system
16// operator who is not an owner gets in, as they do at update and rollback.
17func TestSystemLayoutDoorIsTheSystemGate(t *testing.T) {
18 fx := newConsoleFixture(t)
19 if err := fx.st.AddMember(OpsAccountID, "member@keymail.dev", RoleMember, time.Now()); err != nil {
20 t.Fatal(err)
21 }
22 member := cliToken(t, fx, "member@keymail.dev", "code-layout-member")
23 _, want := getRaw(t, fx.ts.URL+"/api/cli/system/no-such-door", member)
24 if st, body := getRaw(t, fx.ts.URL+"/api/cli/system/layout", member); st != http.StatusNotFound || body != want {
25 t.Errorf("GET as a member = %d %q, want 404 %q", st, body, want)
26 }
27 if st, body := postRaw(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, member); st != http.StatusNotFound || body != want {
28 t.Errorf("POST as a member = %d %q, want 404 %q", st, body, want)
29 }
30 // A member that nothing was recorded for: the object stays absent.
31 if _, found, _ := release.LoadLayoutIntents(context.Background(), fx.rel); found {
32 t.Error("a refused POST recorded an intent")
33 }
34
35 fx.srv.cfg.SystemOperators = []string{"Roller@keymail.dev"}
36 if err := fx.st.AddMember(OpsAccountID, "roller@keymail.dev", RoleMember, time.Now()); err != nil {
37 t.Fatal(err)
38 }
39 roller := cliToken(t, fx, "roller@keymail.dev", "code-layout-roller")
40 if st, out := getJSON(t, fx.ts.URL+"/api/cli/system/layout", roller); st != http.StatusOK || out["mode"] != "none" {
41 t.Errorf("GET as a system operator = %d %v, want 200 mode none", st, out)
42 }
43}
44
45func TestSystemLayoutDoorRecordsTheIntentAndBumpsTheSerial(t *testing.T) {
46 fx := newConsoleFixture(t)
47 owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-owner")
48 // Every box in scope runs the floor release, so the migrate's floor
49 // check (TestSystemLayoutMigrateRefusedBelowTheFloor) is not what this
50 // test is about.
51 seedBox(t, fx, "kass-1", "linux-arm64", "stable", "v0.25.0")
52
53 for _, body := range []map[string]any{{"mode": "sideways"}, {"mode": ""}, {"mode": "nested", "box": "Not A Label!"}, {"mode": "nested", "bogus": 1}} {
54 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", body, owner); st != http.StatusBadRequest {
55 t.Errorf("POST %v = %d %v, want 400", body, st, out)
56 }
57 }
58 if serial(t, fx) != 0 {
59 t.Fatal("a refused request bumped the serial")
60 }
61
62 st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]any{"mode": "nested", "box": "kass-1", "force": true}, owner)
63 if st != http.StatusOK || out["mode"] != "nested" || out["box"] != "kass-1" || out["force"] != true || str(t, out, "id") == "" {
64 t.Fatalf("record = %d %v", st, out)
65 }
66 in, found, err := release.LoadLayoutIntent(context.Background(), fx.rel, "kass-1")
67 if err != nil || !found || in.Mode != "nested" || in.Scope.Box != "kass-1" || !in.Force || in.ID != str(t, out, "id") {
68 t.Fatalf("object = %+v %v %v", in, found, err)
69 }
70 if _, found, _ := release.LoadLayoutIntent(context.Background(), fx.rel, "kass-2"); found {
71 t.Error("a box-scoped intent applies to another box")
72 }
73 if got := serial(t, fx); got != 1 {
74 t.Fatalf("serial = %d, want 1: boxes converge on the serial move", got)
75 }
76
77 st, out = getJSON(t, fx.ts.URL+"/api/cli/system/layout", owner)
78 if st != http.StatusOK || out["mode"] != "nested" || out["box"] != "kass-1" || out["force"] != true || out["requests"] != float64(1) {
79 t.Fatalf("get = %d %v", st, out)
80 }
81
82 // A fleet-wide revert after it is the newest request everywhere.
83 st, out = postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]any{"mode": "flat"}, owner)
84 if st != http.StatusOK || out["box"] != "" || out["force"] != false {
85 t.Fatalf("revert = %d %v", st, out)
86 }
87 in, found, _ = release.LoadLayoutIntent(context.Background(), fx.rel, "kass-1")
88 if !found || in.Mode != "flat" {
89 t.Fatalf("kass-1 after a fleet-wide revert = %+v", in)
90 }
91 if got := serial(t, fx); got != 2 {
92 t.Fatalf("serial = %d, want 2", got)
93 }
94 // The audit row names the act.
95 events, err := fx.st.Events(OpsAccountID, "platform", 10)
96 if err != nil {
97 t.Fatal(err)
98 }
99 var saw bool
100 for _, e := range events {
101 if e.Action == "system-layout" && strings.Contains(e.Detail, "nested for box kass-1") && strings.Contains(e.Detail, "force") {
102 saw = true
103 }
104 }
105 if !saw {
106 t.Errorf("audit rows = %+v, want a system-layout row naming nested, box kass-1 and force", events)
107 }
108}
109
110func TestSystemLayoutDoorWithoutABucketRefuses(t *testing.T) {
111 fx := newConsoleFixture(t)
112 owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-nobucket")
113 fx.srv.cfg.Releases = nil
114 if st, out := getJSON(t, fx.ts.URL+"/api/cli/system/layout", owner); st != http.StatusServiceUnavailable {
115 t.Errorf("GET without a bucket = %d %v, want 503", st, out)
116 }
117 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner); st != http.StatusServiceUnavailable {
118 t.Errorf("POST without a bucket = %d %v, want 503", st, out)
119 }
120}
121
122// #132's interlock: a revert is refused for a box with an active or
123// revoking maintenance worker head, in scope. A detached head, or a head
124// on a box outside a box-scoped revert, does not block. Migrating is never
125// blocked by a worker. A reader that fails blocks: unknown is not "none".
126func TestSystemLayoutRevertRefusedWhileAWorkerHeadIsAttached(t *testing.T) {
127 fx := newConsoleFixture(t)
128 owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-worker")
129 heads := []release.WorkerHead{
130 {Box: "kass-1", Status: "active"},
131 {Box: "kass-2", Status: "detached"},
132 {Box: "kass-3", Status: "revoking"},
133 }
134 var readErr error
135 fx.srv.cfg.WorkerHeads = func(context.Context) ([]release.WorkerHead, error) { return heads, readErr }
136 for _, b := range []string{"kass-1", "kass-2", "kass-3"} {
137 seedBox(t, fx, b, "linux-arm64", "stable", "v0.25.0")
138 }
139
140 st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "flat"}, owner)
141 if st != http.StatusConflict {
142 t.Fatalf("fleet revert with workers attached = %d %v, want 409", st, out)
143 }
144 if msg := str(t, out, "error"); !strings.Contains(msg, "kass-1 has a maintenance worker (active)") || !strings.Contains(msg, "kass-3 has a maintenance worker (revoking)") || strings.Contains(msg, "kass-2") {
145 t.Errorf("reason = %q, want kass-1 and kass-3 named and kass-2 not", msg)
146 }
147 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "flat", "box": "kass-1"}, owner); st != http.StatusConflict {
148 t.Errorf("box revert on the worker's box = %d %v, want 409", st, out)
149 }
150 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "flat", "box": "kass-2"}, owner); st != http.StatusOK {
151 t.Errorf("box revert beside a detached head = %d %v, want 200", st, out)
152 }
153 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner); st != http.StatusOK {
154 t.Errorf("migrate with workers attached = %d %v, want 200", st, out)
155 }
156 readErr = errors.New("store down")
157 if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "flat", "box": "kass-2"}, owner); st != http.StatusConflict {
158 t.Errorf("revert with an unreadable head store = %d %v, want 409", st, out)
159 }
160}
161