carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package boxchoice
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/boxid"
10)
11
12const testKeyFP = "sha256:test-key"
13
14// haBox is an eligible box's edges file in placement "uk", as JSON, so the
15// tests also hold Box's tags to the edges file's.
16func haBox(id string, mut func(m map[string]any)) string {
17 m := map[string]any{
18 "v": 1, "box": id, "box_id": id, "eip": "1.2.3.4", "region": "eu-west-2", "placement": "uk",
19 "hosts": map[string]any{}, "hibernation_configured": true, "serves_shared_pool": true,
20 "accepts_inbound": true, "secrets_key_fp": testKeyFP, "capabilities": []string{"region-ha-1"},
21 "headroom": map[string]any{"mem_free_mib": 1000, "disk_free_mib": 1000, "awake": 1},
22 }
23 if mut != nil {
24 mut(m)
25 }
26 b, _ := json.Marshal(m)
27 return string(b)
28}
29
30// views is a complete snapshot of fresh boxes.
31func views(t *testing.T, bodies ...string) Snapshot {
32 t.Helper()
33 snap := Snapshot{Complete: true}
34 for _, body := range bodies {
35 var b Box
36 if err := json.Unmarshal([]byte(body), &b); err != nil {
37 t.Fatal(err)
38 }
39 b.Fresh = true
40 snap.Boxes = append(snap.Boxes, b)
41 }
42 return snap
43}
44
45func markStale(snap Snapshot, box string) Snapshot {
46 for i := range snap.Boxes {
47 if snap.Boxes[i].Box == box {
48 snap.Boxes[i].Fresh = false
49 }
50 }
51 return snap
52}
53
54func ids(cs []Candidate) []string {
55 out := make([]string, len(cs))
56 for i, c := range cs {
57 out[i] = c.BoxID
58 }
59 return out
60}
61
62// rosterFor enrolls each box in placement "uk" as an instance in eu-west-2.
63func rosterFor(boxes ...string) map[string]RosterBox {
64 out := map[string]RosterBox{}
65 for _, b := range boxes {
66 out[b] = RosterBox{Placement: "uk", Cloud: boxid.Cloud{Provider: "aws", Account: "1", Region: "eu-west-2", InstanceID: "i-" + b}}
67 }
68 return out
69}
70
71func TestEligibleAppliesThePredicateOneBoxAtATime(t *testing.T) {
72 del := func(k string) func(map[string]any) { return func(m map[string]any) { delete(m, k) } }
73 set := func(k string, v any) func(map[string]any) { return func(m map[string]any) { m[k] = v } }
74 cases := []struct {
75 name string
76 mut func(map[string]any)
77 want bool
78 }{
79 {"all present", nil, true},
80 {"no dial address", set("eip", ""), false},
81 {"no box id", set("box_id", ""), false},
82 {"hibernation unconfigured", set("hibernation_configured", false), false},
83 {"hibernation unknown", del("hibernation_configured"), false},
84 {"refuses the shared pool", set("serves_shared_pool", false), false},
85 {"shared pool unknown", del("serves_shared_pool"), false},
86 {"refuses inbound", set("accepts_inbound", false), false},
87 {"inbound unknown", del("accepts_inbound"), false},
88 {"wrong secrets key", set("secrets_key_fp", "sha256:other"), false},
89 {"no capability", del("capabilities"), false},
90 {"draining", set("draining", true), false},
91 {"another placement", set("placement", "us"), false},
92 {"box_id names no roster entry", set("box_id", "i-0zzz"), false},
93 }
94 for _, tc := range cases {
95 t.Run(tc.name, func(t *testing.T) {
96 snap := views(t, haBox("i-0aaa", tc.mut))
97 got := Eligible(snap, rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil)
98 if (len(got) == 1) != tc.want {
99 t.Fatalf("eligible = %v, want %v", ids(got), tc.want)
100 }
101 })
102 }
103}
104
105func TestEligibleCarriesTheHeadroomAndTheRostersRegion(t *testing.T) {
106 got := Eligible(views(t, haBox("i-0aaa", nil)), rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil)
107 want := Candidate{BoxID: "i-0aaa", Region: "eu-west-2", MemFreeMiB: 1000, DiskFreeMiB: 1000, Awake: 1}
108 if len(got) != 1 || got[0] != want {
109 t.Fatalf("got %+v, want %+v", got, want)
110 }
111}
112
113func TestEligibleNeedsARosterEntryFreshnessAndNotLost(t *testing.T) {
114 snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil), haBox("i-0ccc", nil))
115 snap = markStale(snap, "i-0bbb")
116 got := Eligible(snap, rosterFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil)
117 if len(got) != 1 || got[0].BoxID != "i-0aaa" {
118 t.Fatalf("got %v, want only i-0aaa (i-0bbb is stale, i-0ccc has no roster entry)", ids(got))
119 }
120 got = Eligible(snap, rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, map[string]bool{"i-0aaa": true})
121 if len(got) != 0 {
122 t.Fatalf("a lost box is eligible: %v", ids(got))
123 }
124}
125
126// Nearest-region's rule for boxes: one box holding the key and another
127// accepting inbound do not add up to one.
128func TestEligibleNeverCombinesTwoHalfBoxes(t *testing.T) {
129 snap := views(t,
130 haBox("i-0aaa", func(m map[string]any) { m["secrets_key_fp"] = "" }),
131 haBox("i-0bbb", func(m map[string]any) { m["accepts_inbound"] = false }))
132 if got := Eligible(snap, rosterFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
133 t.Fatalf("got %v, want none", ids(got))
134 }
135}
136
137func TestEligibleFromAnIncompleteSnapshotOrWithoutAKeyOrRegionIsNothing(t *testing.T) {
138 snap := views(t, haBox("i-0aaa", nil))
139 snap.Complete = false
140 if got := Eligible(snap, rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
141 t.Fatalf("got %v from an incomplete snapshot", ids(got))
142 }
143 if got := Eligible(views(t, haBox("i-0aaa", nil)), rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", "", nil); len(got) != 0 {
144 t.Fatalf("got %v with no expected key", ids(got))
145 }
146 if got := Eligible(views(t, haBox("i-0aaa", nil)), rosterFor("i-0aaa"), "uk", "", "ie", testKeyFP, nil); len(got) != 0 {
147 t.Fatalf("got %v with no expected region", ids(got))
148 }
149}
150
151func TestEligibleHomePlacementMatchesTheEmptyCode(t *testing.T) {
152 home := haBox("i-0aaa", func(m map[string]any) { m["placement"] = "" })
153 named := haBox("i-0bbb", func(m map[string]any) { m["placement"] = "ie" })
154 snap := views(t, home, named)
155 roster := rosterFor("i-0aaa", "i-0bbb")
156 for _, b := range []string{"i-0aaa", "i-0bbb"} {
157 rb := roster[b]
158 rb.Placement = "ie"
159 roster[b] = rb
160 }
161 if got := Eligible(snap, roster, "", "eu-west-2", "ie", testKeyFP, nil); len(got) != 2 {
162 t.Fatalf("got %v, want both home boxes", ids(got))
163 }
164 if got := Eligible(snap, roster, "ie", "eu-west-2", "ie", testKeyFP, nil); len(got) != 2 {
165 t.Fatalf("got %v, want both home boxes by the home's code", ids(got))
166 }
167}
168
169// A box enrolled in another AWS region than the placement's is not in that
170// placement, even when its own file says it is.
171func TestEligibleRefusesABoxEnrolledInAnotherRegion(t *testing.T) {
172 snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil))
173 roster := rosterFor("i-0aaa", "i-0bbb")
174 roster["i-0bbb"] = RosterBox{Placement: "uk", Cloud: boxid.Cloud{Provider: "aws", Account: "1", Region: "us-east-1", InstanceID: "i-0bbb"}}
175 got := Eligible(snap, roster, "uk", "eu-west-2", "ie", testKeyFP, nil)
176 if len(got) != 1 || got[0].BoxID != "i-0aaa" {
177 t.Fatalf("got %v, want only i-0aaa", ids(got))
178 }
179 // And a file that advertises another AWS region than the one expected.
180 snap = views(t, haBox("i-0aaa", func(m map[string]any) { m["region"] = "us-east-1" }))
181 if got := Eligible(snap, rosterFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
182 t.Fatalf("got %v, want none", ids(got))
183 }
184}
185
186// The roster's enrolled placement is the enrollment's word and must agree
187// with the file's; an incomplete cloud is no identity at all.
188func TestEligibleNeedsTheRosterToAgree(t *testing.T) {
189 snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil))
190 roster := rosterFor("i-0aaa", "i-0bbb")
191 roster["i-0aaa"] = RosterBox{Placement: "us", Cloud: roster["i-0aaa"].Cloud}
192 roster["i-0bbb"] = RosterBox{Placement: "uk", Cloud: boxid.Cloud{Provider: "aws", Region: "eu-west-2", InstanceID: "i-0bbb"}}
193 if got := Eligible(snap, roster, "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
194 t.Fatalf("got %v, want none", ids(got))
195 }
196}
197
198func TestEligibleSortsByBoxID(t *testing.T) {
199 snap := views(t, haBox("i-0ccc", nil), haBox("i-0aaa", nil), haBox("i-0bbb", nil))
200 got := ids(Eligible(snap, rosterFor("i-0aaa", "i-0bbb", "i-0ccc"), "uk", "eu-west-2", "ie", testKeyFP, nil))
201 if len(got) != 3 || got[0] != "i-0aaa" || got[1] != "i-0bbb" || got[2] != "i-0ccc" {
202 t.Fatalf("got %v", got)
203 }
204}
205
206func TestChoose(t *testing.T) {
207 a, b := Candidate{BoxID: "i-0aaa"}, Candidate{BoxID: "i-0bbb"}
208 load := func(hosts int, apps ...string) Load {
209 l := Load{Hosts: hosts, Apps: map[string]int{}}
210 for _, x := range apps {
211 l.Apps[x]++
212 }
213 return l
214 }
215 const host = "shop.bab.oncarlos.com"
216 if _, ok := Choose(host, nil, nil, "bab/jam"); ok {
217 t.Fatal("chose from nothing")
218 }
219 got, _ := Choose(host, []Candidate{a, b}, map[string]Load{"i-0aaa": load(5), "i-0bbb": load(2)}, "bab/jam")
220 if got.BoxID != "i-0bbb" {
221 t.Fatalf("fewest hosts: got %s", got.BoxID)
222 }
223 got, _ = Choose(host, []Candidate{a, b}, map[string]Load{"i-0aaa": load(5), "i-0bbb": load(2, "bab/jam")}, "bab/jam")
224 if got.BoxID != "i-0aaa" {
225 t.Fatalf("spread beats load: got %s", got.BoxID)
226 }
227 got, ok := Choose(host, []Candidate{a, b}, map[string]Load{"i-0aaa": load(5, "bab/jam"), "i-0bbb": load(2, "bab/jam", "bab/jam")}, "bab/jam")
228 if !ok || got.BoxID != "i-0bbb" {
229 t.Fatalf("every box runs the app: fewest hosts overall, never a refusal; got %s ok=%v", got.BoxID, ok)
230 }
231}
232
233// H12, claim P5-D2: an equal load breaks by the greatest SHA-256 of
234// host + "\x00" + reference, so the choice differs host by host instead
235// of always landing on the lowest name, and does not depend on the order
236// the candidates arrive in.
237func TestChooseBreaksATieByTheHostsHash(t *testing.T) {
238 cands := []Candidate{{BoxID: "annie-welding"}, {BoxID: "bert-sawing"}, {BoxID: "cleo-mixing"}}
239 want := func(host string) string {
240 best, bestSum := "", ""
241 for _, c := range cands {
242 sum := sha256.Sum256([]byte(host + "\x00" + c.BoxID))
243 if h := hex.EncodeToString(sum[:]); h > bestSum {
244 best, bestSum = c.BoxID, h
245 }
246 }
247 return best
248 }
249 seen := map[string]bool{}
250 for _, host := range []string{"a.bab.oncarlos.com", "b.bab.oncarlos.com", "c.bab.oncarlos.com", "d.bab.oncarlos.com", "e.bab.oncarlos.com", "f.bab.oncarlos.com"} {
251 forward, _ := Choose(host, cands, nil, "bab/jam")
252 backward, _ := Choose(host, []Candidate{cands[2], cands[1], cands[0]}, nil, "bab/jam")
253 if forward.BoxID != want(host) || backward.BoxID != want(host) {
254 t.Fatalf("%s: chose %s and %s, want %s", host, forward.BoxID, backward.BoxID, want(host))
255 }
256 seen[forward.BoxID] = true
257 }
258 if len(seen) < 2 {
259 t.Fatalf("six hosts all chose %v: the tie-break is not per host", seen)
260 }
261}
262