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 "net/http"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/status"
15 "github.com/carlosframework/platform/internal/updatewords"
16)
17
18// seedSettingsBox writes label's heartbeat at at carrying m as its private
19// metrics block (nil writes a heartbeat with no metrics at all).
20func seedSettingsBox(t *testing.T, fx *consoleFixture, label string, at time.Time, m *status.BoxMetrics) {
21 t.Helper()
22 b := status.CollectBox(nil, status.Probes{Release: "v1"}, at)
23 b.Label = label
24 b.Metrics = m
25 if err := status.WriteBox(context.Background(), fx.rel, b); err != nil {
26 t.Fatal(err)
27 }
28}
29
30func TestUpdatesEnforcedNeedsEveryBox(t *testing.T) {
31 fx := newConsoleFixture(t)
32 seedEdgesRecord(t, fx, "ie-1")
33 seedEdgesRecord(t, fx, "uk-1")
34 now := time.Now()
35 seedSettingsBox(t, fx, "ie-1", now, &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
36 seedSettingsBox(t, fx, "uk-1", now, &status.BoxMetrics{})
37 ok, blocking, err := fx.srv.updatesEnforced(context.Background())
38 if err != nil || ok || len(blocking) != 1 || !strings.Contains(blocking[0], "uk-1") {
39 t.Fatalf("one old binary: %v %q %v", ok, blocking, err)
40 }
41 seedSettingsBox(t, fx, "uk-1", now, &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
42 if ok, _, err := fx.srv.updatesEnforced(context.Background()); !ok || err != nil {
43 t.Fatalf("every box enforces: %v %v", ok, err)
44 }
45}
46
47// Review Focus 4.
48func TestUpdatesEnforcedNamesAStaleBox(t *testing.T) {
49 fx := newConsoleFixture(t)
50 seedEdgesRecord(t, fx, "ie-1")
51 seedSettingsBox(t, fx, "ie-1", time.Now().Add(-3*status.SnapshotPeriod), &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
52 ok, blocking, _ := fx.srv.updatesEnforced(context.Background())
53 if ok || len(blocking) != 1 || !strings.Contains(blocking[0], "ie-1") {
54 t.Fatalf("a stale box: %v %q", ok, blocking)
55 }
56}
57
58func TestUpdatesEnforcedNeedsAHeartbeat(t *testing.T) {
59 fx := newConsoleFixture(t)
60 seedEdgesRecord(t, fx, "ie-1")
61 if ok, blocking, _ := fx.srv.updatesEnforced(context.Background()); ok || len(blocking) != 1 {
62 t.Fatalf("an expected box with no heartbeat: %v %q", ok, blocking)
63 }
64}
65
66// An empty inventory has seen no box at all, so it cannot vouch that every
67// box enforces: it blocks rather than reading as "nothing to wait on".
68func TestUpdatesEnforcedNeedsSomeBox(t *testing.T) {
69 fx := newConsoleFixture(t)
70 if ok, blocking, err := fx.srv.updatesEnforced(context.Background()); ok || err != nil || len(blocking) != 1 {
71 t.Fatalf("no box known: %v %q %v", ok, blocking, err)
72 }
73}
74
75// seedSettingsFleet registers fleet fx with one active box, bx-1, in the
76// fixture's registry, and (when at is not zero) writes bx-1's heartbeat
77// under the fleet's prefix carrying m.
78func seedSettingsFleet(t *testing.T, fx *consoleFixture, at time.Time, m *status.BoxMetrics) {
79 t.Helper()
80 reg, err := registry.Open(fx.regPath)
81 if err != nil {
82 t.Fatal(err)
83 }
84 if _, err := reg.CreateFleet(registry.Fleet{Name: "fx", Account: fx.sqid, Bucket: "fx-data"}); err != nil {
85 t.Fatal(err)
86 }
87 if err := reg.AddFleetBox(registry.FleetBox{Fleet: "fx", Label: "bx-1", TokenHash: strings.Repeat("a", 64)}); err != nil {
88 t.Fatal(err)
89 }
90 if at.IsZero() {
91 return
92 }
93 b := status.CollectBox(nil, status.Probes{Release: "v1"}, at)
94 b.Label = "bx-1"
95 b.Metrics = m
96 if err := status.WriteBox(context.Background(), release.NewPrefixStore(fx.rel, "fleets/fx/"), b); err != nil {
97 t.Fatal(err)
98 }
99}
100
101// Astra r1 #1: an unread fleet inventory is not an empty one. A console
102// whose registry path is unset, or names no file, has not listed the
103// fleets, so it cannot vouch for their boxes and the gate blocks, while the
104// GeoIP verdict's reading of the same inventory is unchanged (its
105// expected list stays complete, as before).
106func TestUpdatesEnforcedNeedsTheFleetRegistry(t *testing.T) {
107 for name, path := range map[string]func(t *testing.T) string{
108 "unset": func(*testing.T) string { return "" },
109 "missing": func(t *testing.T) string { return filepath.Join(t.TempDir(), "absent-registry.db") },
110 } {
111 t.Run(name, func(t *testing.T) {
112 fx := newConsoleFixture(t)
113 seedEdgesRecord(t, fx, "ie-1")
114 seedSettingsBox(t, fx, "ie-1", time.Now(), &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
115 if ok, blocking, err := fx.srv.updatesEnforced(context.Background()); !ok || err != nil {
116 t.Fatalf("with the registry read: %v %q %v", ok, blocking, err)
117 }
118 fx.srv.cfg.Registry = path(t)
119 ok, blocking, err := fx.srv.updatesEnforced(context.Background())
120 if err != nil || ok || len(blocking) != 1 || !strings.Contains(blocking[0], "the fleet registry could not be read") {
121 t.Fatalf("registry %s: %v %q %v", name, ok, blocking, err)
122 }
123 inv, err := fx.srv.geoipInventory(context.Background())
124 if err != nil || !inv.expectedComplete || !inv.heartbeatsComplete {
125 t.Fatalf("the GeoIP reading of an absent registry changed: %+v %v", inv, err)
126 }
127 })
128 }
129}
130
131// Task 5's deferred gap: an active fleet box is expected, keyed by its
132// fleet, and blocks with no report, an old binary or a stale report; once
133// it reports the protocol the gate opens.
134func TestUpdatesEnforcedNeedsEveryFleetBox(t *testing.T) {
135 now := time.Now()
136 for name, c := range map[string]struct {
137 at time.Time
138 m *status.BoxMetrics
139 want string
140 }{
141 "no report": {want: "fx/bx-1: no heartbeat"},
142 "an old binary": {at: now, m: &status.BoxMetrics{}, want: "fx/bx-1: runs a binary that does not apply update settings"},
143 "a stale report": {at: now.Add(-3 * status.SnapshotPeriod), m: &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol},
144 want: "fx/bx-1: no heartbeat since"},
145 "the protocol": {at: now, m: &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol}},
146 } {
147 t.Run(name, func(t *testing.T) {
148 fx := newConsoleFixture(t)
149 seedEdgesRecord(t, fx, "ie-1")
150 seedSettingsBox(t, fx, "ie-1", now, &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
151 seedSettingsFleet(t, fx, c.at, c.m)
152 ok, blocking, err := fx.srv.updatesEnforced(context.Background())
153 if c.want == "" {
154 if !ok || err != nil {
155 t.Fatalf("a fleet box that applies settings: %v %q %v", ok, blocking, err)
156 }
157 return
158 }
159 if err != nil || ok || len(blocking) != 1 || !strings.HasPrefix(blocking[0], c.want) {
160 t.Fatalf("want %q blocking: %v %q %v", c.want, ok, blocking, err)
161 }
162 })
163 }
164}
165
166// Task 5's deferred gap: a heartbeat record that cannot be read leaves the
167// read incomplete, and an incomplete read blocks even when every box that
168// was read applies settings.
169func TestUpdatesEnforcedBlocksAnIncompleteRead(t *testing.T) {
170 fx := newConsoleFixture(t)
171 seedEdgesRecord(t, fx, "ie-1")
172 seedSettingsBox(t, fx, "ie-1", time.Now(), &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
173 if _, err := fx.rel.Put(context.Background(), "status/boxes/zz-1.json", []byte("{not json"), ""); err != nil {
174 t.Fatal(err)
175 }
176 ok, blocking, err := fx.srv.updatesEnforced(context.Background())
177 if err != nil || ok || len(blocking) != 1 || !strings.Contains(blocking[0], "could not be read in full") {
178 t.Fatalf("an unreadable heartbeat: %v %q %v", ok, blocking, err)
179 }
180}
181
182// Astra r1 #1 through the doors: whenever the gate cannot vouch for every
183// box (no registry path, a registry file that is not there, a fleet box with
184// no report, an old binary or a stale report, an unreadable heartbeat), both
185// enabling doors refuse with the not-enforced 409 and write nothing, and
186// clearing through both still answers 200 in that same state.
187func TestEnableDoorsFailClosedOnAnUncheckedBox(t *testing.T) {
188 now := time.Now()
189 notEnforced := strings.SplitN(updatewords.Text("settings.console.not-enforced"), "%d", 2)[0]
190 for name, breakIt := range map[string]func(t *testing.T, fx *updatesFx){
191 "registry unset": func(t *testing.T, fx *updatesFx) { fx.srv.cfg.Registry = "" },
192 "registry missing": func(t *testing.T, fx *updatesFx) {
193 fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "absent-registry.db")
194 },
195 "fleet box with no report": func(t *testing.T, fx *updatesFx) {
196 seedSettingsFleet(t, fx.consoleFixture, time.Time{}, nil)
197 },
198 "fleet box with an old binary": func(t *testing.T, fx *updatesFx) {
199 seedSettingsFleet(t, fx.consoleFixture, now, &status.BoxMetrics{})
200 },
201 "fleet box with a stale report": func(t *testing.T, fx *updatesFx) {
202 seedSettingsFleet(t, fx.consoleFixture, now.Add(-3*status.SnapshotPeriod), &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
203 },
204 "an unreadable heartbeat": func(t *testing.T, fx *updatesFx) {
205 if _, err := fx.rel.Put(context.Background(), "status/boxes/zz-1.json", []byte("{not json"), ""); err != nil {
206 t.Fatal(err)
207 }
208 },
209 } {
210 t.Run(name, func(t *testing.T) {
211 fx := updatesFixture(t)
212 fx.boxesEnforce(true)
213 two := 2
214 fx.putRecord(provision.Record{Schema: 1, Host: "a.example.com", App: "km", Account: "bab", Channel: "stable", Pin: "v1"})
215 fx.setManifest(provision.Manifest{Schema: 1, App: "km", Domain: "example.com", Channel: "stable",
216 Updates: &provision.UpdatePolicy{DefaultMajor: &two}})
217 breakIt(t, fx)
218
219 st, out := sendJSON(t, http.MethodPut, fx.ts.URL+updURL, map[string]string{"updates": "manual"}, fx.member)
220 if st != http.StatusConflict || !strings.Contains(out, notEnforced) {
221 t.Errorf("instance door, manual: %d %s", st, out)
222 }
223 if rec := fx.record("a.example.com"); rec.Updates != "" {
224 t.Errorf("a refused instance enable wrote the record: %+v", rec)
225 }
226 st, out = sendJSON(t, http.MethodPut, fx.ts.URL+policyURL, map[string]any{"default_updates": "manual"}, fx.member)
227 if st != http.StatusConflict || !strings.Contains(out, notEnforced) {
228 t.Errorf("policy door, default manual: %d %s", st, out)
229 }
230 if p := fx.manifest().Updates; p == nil || p.DefaultUpdates != "" {
231 t.Errorf("a refused policy enable wrote the policy: %+v", p)
232 }
233
234 st, out = sendJSON(t, http.MethodPut, fx.ts.URL+updURL, map[string]any{"pin": ""}, fx.member)
235 if rec := fx.record("a.example.com"); st != http.StatusOK || rec.Pin != "" {
236 t.Fatalf("instance door, clearing a pin: %d %s, record %+v", st, out, rec)
237 }
238 st, out = sendJSON(t, http.MethodPut, fx.ts.URL+policyURL, map[string]any{"default_major": nil}, fx.member)
239 if p := fx.manifest().Updates; st != http.StatusOK || (p != nil && p.DefaultMajor != nil) {
240 t.Fatalf("policy door, clearing the default major: %d %s, policy %+v", st, out, p)
241 }
242 })
243 }
244}
245
246// The control for the test above: with the registry read and the fleet box
247// reporting the protocol, both doors enable.
248func TestEnableDoorsOpenWhenEveryFleetBoxEnforces(t *testing.T) {
249 fx := updatesFixture(t)
250 fx.boxesEnforce(true)
251 seedSettingsFleet(t, fx.consoleFixture, time.Now(), &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
252 if st, out := sendJSON(t, http.MethodPut, fx.ts.URL+updURL, map[string]string{"updates": "manual"}, fx.member); st != http.StatusOK {
253 t.Fatalf("instance door: %d %s", st, out)
254 }
255 if st, out := sendJSON(t, http.MethodPut, fx.ts.URL+policyURL, map[string]any{"default_updates": "manual"}, fx.member); st != http.StatusOK {
256 t.Fatalf("policy door: %d %s", st, out)
257 }
258}
259
260// A heartbeat with no metrics block is a binary that does not say it
261// enforces; an unexpected box that has gone stale is retired and does not
262// block; an unexpected fresh box with an old binary does.
263func TestUpdatesEnforcedBoxShapes(t *testing.T) {
264 fx := newConsoleFixture(t)
265 now := time.Now()
266 seedEdgesRecord(t, fx, "ie-1")
267 seedSettingsBox(t, fx, "ie-1", now, &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
268 seedSettingsBox(t, fx, "old-1", now.Add(-time.Hour), &status.BoxMetrics{})
269 if ok, blocking, err := fx.srv.updatesEnforced(context.Background()); !ok || err != nil {
270 t.Fatalf("a retired box blocked: %v %q %v", ok, blocking, err)
271 }
272 seedSettingsBox(t, fx, "new-1", now, &status.BoxMetrics{})
273 ok, blocking, _ := fx.srv.updatesEnforced(context.Background())
274 if ok || len(blocking) != 1 || !strings.Contains(blocking[0], "new-1") {
275 t.Fatalf("an unlisted fresh old binary: %v %q", ok, blocking)
276 }
277 seedSettingsBox(t, fx, "new-1", now, &status.BoxMetrics{UpdateSettings: status.UpdateSettingsProtocol})
278 seedSettingsBox(t, fx, "ie-1", now, nil)
279 ok, blocking, _ = fx.srv.updatesEnforced(context.Background())
280 if ok || len(blocking) != 1 || !strings.Contains(blocking[0], "ie-1") {
281 t.Fatalf("a heartbeat with no metrics: %v %q", ok, blocking)
282 }
283}
284