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 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/boxid"
14 "github.com/carlosframework/platform/internal/boxid/boxidtest"
15 "github.com/carlosframework/platform/internal/hostactors"
16 "github.com/carlosframework/platform/internal/moves"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/status"
19)
20
21// moveTestBucket is the deployment bucket the roster armMoveFleet publishes
22// names, and the console is configured with.
23const moveTestBucket = "move-test-bucket"
24
25// moveTestEnrollment is the enrollment the test roster gives a box: 32 hex
26// characters, distinct per box.
27func moveTestEnrollment(box string) string {
28 sum := sha256.Sum256([]byte(box))
29 return hex.EncodeToString(sum[:16])
30}
31
32// putMoveRoster signs r with the fixture's root key and stores it as the
33// host roster, replacing any earlier one, and points the console at its
34// bucket.
35func putMoveRoster(t *testing.T, fx *consoleFixture, r hostactors.Roster) {
36 t.Helper()
37 ctx := context.Background()
38 fx.srv.cfg.Bucket = moveTestBucket
39 r.V, r.DeploymentBucket, r.Revision = 1, moveTestBucket, 1
40 if r.Boxes == nil {
41 r.Boxes = map[string]hostactors.Box{}
42 }
43 r.Consoles, r.Holds, r.Activations = map[string]hostactors.Console{}, map[string]hostactors.Reservation{}, map[string]hostactors.Reservation{}
44 raw, err := release.SignRootEnvelope(release.RootSigner(fx.priv), r)
45 if err != nil {
46 t.Fatal(err)
47 }
48 _, etag, err := fx.rel.Get(ctx, hostactors.Key)
49 if err != nil {
50 etag = ""
51 }
52 if _, err := fx.rel.Put(ctx, hostactors.Key, raw, etag); err != nil {
53 t.Fatal(err)
54 }
55}
56
57// moveRosterBox is the roster entry of a fleet box: active, with the
58// enrollment moveTestEnrollment gives it.
59func moveRosterBox(b fleetBox) hostactors.Box {
60 return hostactors.Box{BoxID: b.Box, Namespace: "platform", Label: b.Box + "-label", Region: b.Placement, Pool: "default",
61 Cloud: hostactors.Cloud{Provider: "aws", Account: moveTestAccount, Region: b.AWS, InstanceID: "i-" + b.Box},
62 Enrollment: moveTestEnrollment(b.Box), EvidenceSHA256: strings.Repeat("f", 64), State: "active", LossIDs: []string{}}
63}
64
65// moveTestAccount is the AWS account armMoveFleet's boxes are signed in and
66// the console is configured to trust.
67const moveTestAccount = "111122223333"
68
69// fleetBox is one box armMoveFleet publishes. PrevRelease defaults to
70// "none". Unverified skips the signed identity record, Stale writes an old
71// status snapshot, FloorUnarmed omits moves_floor_min_release, NoProtocol
72// omits moves_protocol, and SignedAWS (when set) signs the identity record
73// for another AWS region than the edges file names.
74type fleetBox struct {
75 Box, Placement, AWS, EIP, Bucket string
76 PrevRelease, PrevAllowedFor string
77 SignedAWS string
78 Unverified, Stale, FloorUnarmed bool
79 NoProtocol bool
80 Protocol int // 0: moves.ProtocolVersion
81}
82
83// armMoveFleet makes the fleet production-shaped for the move door: the
84// console holds a secrets key, trusts one AWS account and one signer's
85// certificates, and every box has an edges file with every eligibility
86// field, a status snapshot (fresh unless Stale) and, unless Unverified, a
87// signed boxid record. The published floor, if any, is read first so each
88// file can say it armed it.
89func armMoveFleet(t *testing.T, fx *consoleFixture, boxes ...fleetBox) *boxidtest.Signer {
90 t.Helper()
91 ctx := context.Background()
92 fx.srv.cfg.SecretsPub = testKeyPub
93 fx.srv.cfg.AWSAccount = moveTestAccount
94 signer := boxidtest.NewSigner(t)
95 fx.srv.boxCerts = signer.Certs()
96 floor, err := moves.ReadFloor(ctx, fx.rel)
97 if err != nil {
98 t.Fatal(err)
99 }
100 for _, b := range boxes {
101 m := map[string]any{
102 "v": 1, "box": b.Box, "box_id": b.Box, "eip": b.EIP, "region": b.AWS, "placement": b.Placement,
103 "data_bucket": b.Bucket, "data_bucket_region": b.AWS,
104 "hibernation_configured": true, "serves_shared_pool": true, "accepts_inbound": true,
105 "secrets_key_fp": testKeyFP, "capabilities": []string{"region-ha-1"}, "hosts": map[string]any{},
106 "moves_protocol": moves.ProtocolVersion, "prev_release": "none",
107 }
108 if b.Protocol != 0 {
109 m["moves_protocol"] = b.Protocol
110 }
111 if b.NoProtocol {
112 delete(m, "moves_protocol")
113 }
114 if floor != nil && !b.FloorUnarmed {
115 m["moves_floor_min_release"] = floor.MinRelease
116 }
117 if b.PrevRelease != "" {
118 m["prev_release"] = b.PrevRelease
119 }
120 if b.PrevAllowedFor != "" {
121 m["prev_allowed_for"] = b.PrevAllowedFor
122 }
123 body, err := json.Marshal(m)
124 if err != nil {
125 t.Fatal(err)
126 }
127 if _, err := fx.rel.Put(ctx, edgesFilePrefix+b.Box+".json", body, ""); err != nil {
128 t.Fatal(err)
129 }
130 at := time.Now()
131 if b.Stale {
132 at = at.Add(-time.Hour)
133 }
134 if err := status.WriteBox(ctx, fx.rel, status.Box{Label: b.Box, GeneratedAt: at}); err != nil {
135 t.Fatal(err)
136 }
137 if b.Unverified {
138 continue
139 }
140 signed := b.AWS
141 if b.SignedAWS != "" {
142 signed = b.SignedAWS
143 }
144 rec, _ := signer.Record(t, b.Box, moveTestAccount, signed, "i-"+b.Box)
145 if err := boxid.Publish(ctx, fx.rel, rec); err != nil {
146 t.Fatal(err)
147 }
148 }
149 // Every box is enrolled in the roster the door reads.
150 roster := hostactors.Roster{Boxes: map[string]hostactors.Box{}}
151 for _, b := range boxes {
152 roster.Boxes[b.Box] = moveRosterBox(b)
153 }
154 putMoveRoster(t, fx, roster)
155 return signer
156}
157
158func ieBox(box string) fleetBox {
159 return fleetBox{Box: box, Placement: "ie", AWS: "eu-west-1", EIP: "1.2.3.3", Bucket: "data-ie"}
160}
161
162func ukBox(box string) fleetBox {
163 return fleetBox{Box: box, Placement: "uk", AWS: "eu-west-2", EIP: "1.2.3.4", Bucket: "data-uk"}
164}
165
166// ieBox2 and ukBox2 publish protocol 2, above anything this build speaks.
167func ieBox2(box string) fleetBox { b := ieBox(box); b.Protocol = 2; return b }
168func ukBox2(box string) fleetBox { b := ukBox(box); b.Protocol = 2; return b }
169
170// selectMoveBoxes is the first production caller of eligibleBoxes and
171// chooseBox: it composes every input itself (the live snapshot, identities
172// proven by boxid.Verifier.Enroll, the secrets fingerprint, each code's AWS
173// region) and adds the one-box refusal and the protocol floor.
174func TestSelectMoveBoxesComposesTheCreateDoorsInputs(t *testing.T) {
175 const host, app = "acme.go.tito.io", "bab/titogo"
176 floorJSON := `{"min":1,"min_release":"v0.23.0"}`
177 withPrev := func(b fleetBox, prev, allowed string) fleetBox {
178 b.PrevRelease, b.PrevAllowedFor = prev, allowed
179 return b
180 }
181 cases := []struct {
182 name string
183 floor bool
184 boxes []fleetBox
185 setup func(fx *consoleFixture)
186 want moveSelection
187 }{
188 {name: "one verified box each side", boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-box")},
189 want: moveSelection{Target: "uk-box", Source: "ie-box", FloorOK: true}},
190 {name: "two verified boxes in the target", boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-a"), ukBox("uk-b")},
191 want: moveSelection{Many: true, ManyRegion: "uk", FloorOK: true}},
192 {name: "two verified boxes in the source", boxes: []fleetBox{ieBox("ie-a"), ieBox("ie-b"), ukBox("uk-box")},
193 want: moveSelection{Target: "uk-box", Many: true, ManyRegion: "ie", FloorOK: true}},
194 {name: "an unverifiable second box is not eligible",
195 boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-a"), func() fleetBox { b := ukBox("uk-b"); b.Unverified = true; return b }()},
196 want: moveSelection{Target: "uk-a", Source: "ie-box", FloorOK: true}},
197 {name: "an identity signed in another AWS region is not eligible",
198 boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-a"), func() fleetBox { b := ukBox("uk-b"); b.SignedAWS = "eu-north-1"; return b }()},
199 want: moveSelection{Target: "uk-a", Source: "ie-box", FloorOK: true}},
200 {name: "a stale box is not eligible",
201 boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-a"), func() fleetBox { b := ukBox("uk-b"); b.Stale = true; return b }()},
202 want: moveSelection{Target: "uk-a", Source: "ie-box", FloorOK: true}},
203 {name: "no account configured verifies nothing", boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-box")},
204 setup: func(fx *consoleFixture) { fx.srv.cfg.AWSAccount = "" },
205 want: moveSelection{FloorOK: true}},
206 {name: "no file in the source region", boxes: []fleetBox{ukBox("uk-box")},
207 want: moveSelection{Target: "uk-box"}},
208 {name: "a file without moves_protocol",
209 boxes: []fleetBox{ieBox("ie-box"), func() fleetBox { b := ukBox("uk-box"); b.NoProtocol = true; return b }()},
210 want: moveSelection{Target: "uk-box", Source: "ie-box"}},
211 {name: "a published floor, prev none", floor: true, boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-box")},
212 want: moveSelection{Target: "uk-box", Source: "ie-box", FloorOK: true}},
213 {name: "a published floor, prev judged at it", floor: true,
214 boxes: []fleetBox{ieBox("ie-box"), withPrev(ukBox("uk-box"), "v0.23.1", "v0.23.0")},
215 want: moveSelection{Target: "uk-box", Source: "ie-box", FloorOK: true}},
216 {name: "a published floor, prev judged at an older floor", floor: true,
217 boxes: []fleetBox{ieBox("ie-box"), withPrev(ukBox("uk-box"), "v0.23.1", "v0.22.0")},
218 want: moveSelection{Target: "uk-box", Source: "ie-box"}},
219 {name: "a published floor, prev never judged", floor: true,
220 boxes: []fleetBox{withPrev(ieBox("ie-box"), "v0.22.0", ""), ukBox("uk-box")},
221 want: moveSelection{Target: "uk-box", Source: "ie-box"}},
222 {name: "a published floor the box has not armed", floor: true,
223 boxes: []fleetBox{ieBox("ie-box"), func() fleetBox { b := ukBox("uk-box"); b.FloorUnarmed = true; return b }()},
224 want: moveSelection{Target: "uk-box", Source: "ie-box"}},
225 {name: "a floor above this protocol", boxes: []fleetBox{ieBox("ie-box"), ukBox("uk-box")},
226 setup: func(fx *consoleFixture) {
227 if _, err := fx.rel.Put(context.Background(), moves.FloorKey, []byte(`{"min":2}`), ""); err != nil {
228 t.Fatal(err)
229 }
230 },
231 want: moveSelection{Target: "uk-box", Source: "ie-box", ConsoleBelowFloor: true}},
232 {name: "a floor above this console, boxes at it",
233 boxes: []fleetBox{ieBox2("ie-box"), ukBox2("uk-box")},
234 setup: func(fx *consoleFixture) {
235 if _, err := fx.rel.Put(context.Background(), moves.FloorKey, []byte(`{"min":2}`), ""); err != nil {
236 t.Fatal(err)
237 }
238 },
239 want: moveSelection{Target: "uk-box", Source: "ie-box", FloorOK: true, ConsoleBelowFloor: true}},
240 }
241 for _, c := range cases {
242 t.Run(c.name, func(t *testing.T) {
243 fx := newConsoleFixture(t)
244 if c.floor {
245 if _, err := fx.rel.Put(context.Background(), moves.FloorKey, []byte(floorJSON), ""); err != nil {
246 t.Fatal(err)
247 }
248 }
249 armMoveFleet(t, fx, c.boxes...)
250 if c.setup != nil {
251 c.setup(fx)
252 }
253 got, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app)
254 if err != nil {
255 t.Fatal(err)
256 }
257 if got != c.want {
258 t.Fatalf("selection = %+v, want %+v", got, c.want)
259 }
260 })
261 }
262
263 t.Run("two boxes in a home source", func(t *testing.T) {
264 fx := newConsoleFixture(t)
265 fx.srv.cfg.HomeRegion = "ie"
266 armMoveFleet(t, fx, ieBox("ie-a"), ieBox("ie-b"), ukBox("uk-box"))
267 got, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "", app)
268 if err != nil {
269 t.Fatal(err)
270 }
271 if want := (moveSelection{Target: "uk-box", Many: true, ManyRegion: "ie", FloorOK: true}); got != want {
272 t.Fatalf("selection = %+v, want %+v", got, want)
273 }
274 })
275 t.Run("an incomplete snapshot answers nothing", func(t *testing.T) {
276 fx := newConsoleFixture(t)
277 armMoveFleet(t, fx, ieBox("ie-box"), ukBox("uk-box"))
278 if _, err := fx.rel.Put(context.Background(), edgesFilePrefix+"broken.json", []byte("{"), ""); err != nil {
279 t.Fatal(err)
280 }
281 if _, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app); !errors.Is(err, errEdgesIncomplete) {
282 t.Fatalf("err = %v, want errEdgesIncomplete", err)
283 }
284 })
285 t.Run("an unreadable identity record answers nothing", func(t *testing.T) {
286 fx := newConsoleFixture(t)
287 armMoveFleet(t, fx, ieBox("ie-box"), ukBox("uk-a"), ukBox("uk-b"))
288 fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: boxid.Key("uk-b")}
289 if _, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app); !errors.Is(err, errEdgesIncomplete) {
290 t.Fatalf("err = %v, want errEdgesIncomplete", err)
291 }
292 })
293 t.Run("an unreadable floor is an error, never no floor", func(t *testing.T) {
294 fx := newConsoleFixture(t)
295 armMoveFleet(t, fx, ieBox("ie-box"), ukBox("uk-box"))
296 fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: moves.FloorKey}
297 if _, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app); err == nil || errors.Is(err, errEdgesIncomplete) {
298 t.Fatalf("err = %v, want the floor's read error", err)
299 }
300 })
301}
302
303func TestPlan14MoveSelectionMixedRootFloors(t *testing.T) {
304 for _, state := range []string{"", "quarantined", "accepted"} {
305 t.Run(state, func(t *testing.T) {
306 fx := newConsoleFixture(t)
307 if _, err := fx.rel.Put(t.Context(), moves.FloorKey, []byte(`{"min":1,"min_release":"v0.23.0"}`), ""); err != nil {
308 t.Fatal(err)
309 }
310 armMoveFleet(t, fx, ieBox("ie-box"), ukBox("uk-box"))
311 key := edgesFilePrefix + "uk-box.json"
312 raw, tag, err := fx.rel.Get(t.Context(), key)
313 if err != nil {
314 t.Fatal(err)
315 }
316 var body map[string]any
317 if err := json.Unmarshal(raw, &body); err != nil {
318 t.Fatal(err)
319 }
320 body["moves_floor_generation"] = 1
321 body["moves_floor_state"] = state
322 raw, _ = json.Marshal(body)
323 if _, err := fx.rel.Put(t.Context(), key, raw, tag); err != nil {
324 t.Fatal(err)
325 }
326 got, err := fx.srv.selectMoveBoxes(t.Context(), "acme.go.tito.io", "uk", "ie", "bab/titogo")
327 if err != nil || got.Source != "ie-box" || got.Target != "uk-box" || got.FloorOK != (state == "accepted") {
328 t.Fatal(got, err)
329 }
330 })
331 }
332}
333