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 "fmt"
5 "sort"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/infraview"
10 "github.com/carlosframework/platform/internal/status"
11)
12
13// multiRegionFixture is the estate this feature exists for: boxes in four
14// regions, two of them in the SAME region (so a band has to hold more than
15// one flow), one machine no box reports, one DNS name pointing outside the
16// estate entirely, and one deployment bucket every box writes to.
17func multiRegionFixture() ArchInput {
18 res := []infraview.Resource{
19 {Kind: infraview.KindDNSZone, ID: "Z1", Name: "tito.io"},
20 {Kind: infraview.KindBucket, ID: "carlos-tito-platform", Name: "carlos-tito-platform", Region: "eu-west-1"},
21
22 // A name that leaves the estate. It reaches no machine, so it and
23 // the record naming it belong to no region at all.
24 {Kind: infraview.KindDNSRecord, ID: "rec-away", Name: "mail.tito.io",
25 Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "203.0.113.9"}},
26
27 // A machine nothing reports: drift, and it anchors its own band.
28 {Kind: infraview.KindMachine, ID: "i-orphan", Name: "old-thing", Region: "us-east-1",
29 Attrs: map[string]string{"type": "t3.micro", "state": "stopped"}},
30 }
31 type box struct{ label, region, machine, addr string }
32 boxes := []box{
33 {"carlos-tito-ie", "eu-west-1", "i-ie", "52.0.0.1"},
34 {"carlos-tito-svc-pdf", "eu-west-1", "i-pdf", "52.0.0.2"},
35 {"carlos-tito-uk", "eu-west-2", "i-uk", "52.0.0.3"},
36 {"carlos-tito-se", "eu-north-1", "i-se", "52.0.0.4"},
37 }
38 var fleet []status.FleetBox
39 var hosts []HostRow
40 for _, b := range boxes {
41 res = append(res,
42 infraview.Resource{Kind: infraview.KindMachine, ID: b.machine, Name: b.label, Region: b.region,
43 Attrs: map[string]string{"type": "t4g.small", "state": "running"}},
44 infraview.Resource{Kind: infraview.KindIP, ID: "eip-" + b.label, Region: b.region,
45 Attrs: map[string]string{"address": b.addr, "attached_to": b.machine}},
46 infraview.Resource{Kind: infraview.KindDNSRecord, ID: "rec-" + b.label, Name: b.label + ".tito.io",
47 Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": b.addr}},
48 )
49 fleet = append(fleet, status.FleetBox{Label: b.label, Region: b.region})
50 hosts = append(hosts, HostRow{Label: b.label, Region: b.region,
51 Metrics: &status.BoxMetrics{Release: "abc1234", InstanceID: b.machine}})
52 }
53 return ArchInput{
54 Inv: &infraview.Inventory{Provider: "aws", Account: "665106592142", Resources: res},
55 Bucket: "carlos-tito-platform",
56 Fleet: &status.Fleet{Boxes: fleet},
57 Hosts: hosts,
58 }
59}
60
61// bandNames is every picture on the page, in the order it is drawn.
62func bandNames(e ArchEstate) []string {
63 var out []string
64 for _, b := range e.Bands {
65 out = append(out, b.Region)
66 }
67 return out
68}
69
70// THE property the whole band design rests on, and it has two halves that
71// must BOTH be asserted.
72//
73// A band is drawn as a picture of its own, so an edge with one end in
74// another band cannot be drawn — and pruneDanglingEdges duly removes it from
75// both. That is the trap: checking only that every DRAWN edge has both ends
76// in its band is a test a totally broken banding rule passes, because the
77// spanning edges are gone by the time it looks. So the second half asks the
78// question that actually matters — every edge that ought to be on the page
79// IS on the page — and it is the half that fails when a follower stops
80// following.
81func TestNoEdgeSpansTwoBands(t *testing.T) {
82 in := multiRegionFixture()
83 v := BuildArch(in)
84
85 drawn := map[string]bool{}
86 for _, e := range []ArchEstate{v.CARLOS, v.Rest} {
87 for _, n := range e.Nodes() {
88 drawn[n.ID] = true
89 }
90 }
91 kept := map[Edge]bool{}
92 for _, e := range archEdges(v) {
93 kept[e] = true
94 }
95 // Both ends drawn somewhere on the page means the edge belongs on it:
96 // an edge across the two BANKS is impossible (archestate_test.go pins
97 // that), so anything missing here went missing across two BANDS.
98 for _, e := range wholeGraphOf(in, v).Edges {
99 if drawn[e.From] && drawn[e.To] && !kept[e] {
100 t.Errorf("edge %s -> %s is drawn by no band: both its ends are on the page and the relationship is not",
101 e.From, e.To)
102 }
103 }
104
105 for _, e := range []ArchEstate{v.CARLOS, v.Rest} {
106 // Where each node is drawn. A node may legitimately be in several
107 // bands (the deployment bucket), so this is a set per node.
108 bands := map[string]map[string]bool{}
109 for _, b := range e.Bands {
110 for _, n := range b.Nodes {
111 if bands[n.ID] == nil {
112 bands[n.ID] = map[string]bool{}
113 }
114 bands[n.ID][b.Region] = true
115 }
116 }
117 for _, b := range e.Bands {
118 for _, ed := range b.Edges {
119 if !bands[ed.From][b.Region] || !bands[ed.To][b.Region] {
120 t.Errorf("edge %s -> %s is drawn in band %q, which does not hold both its ends (%v / %v)",
121 ed.From, ed.To, b.Region, keysOf(bands[ed.From]), keysOf(bands[ed.To]))
122 }
123 }
124 }
125 }
126}
127
128// wholeGraphOf rebuilds the graph as it stands BEFORE banding — every node
129// and edge the joins produced — so a test can ask what the page ought to be
130// showing rather than only what it is.
131func wholeGraphOf(in ArchInput, v ArchView) archGraph {
132 claimed := map[string]string{}
133 for _, b := range v.Boxes {
134 if b.CloudID != "" {
135 claimed[b.CloudID] = b.Label
136 }
137 }
138 var g archGraph
139 addDNSNodes(&g, in.Inv, claimed)
140 addBoxNodes(&g, v.Boxes, in)
141 addMachineNodes(&g, in.Inv, claimed)
142 g.Nodes = dropSatellites(g.Nodes)
143 return g
144}
145
146func keysOf(m map[string]bool) []string {
147 var out []string
148 for k := range m {
149 out = append(out, k)
150 }
151 sort.Strings(out)
152 return out
153}
154
155// Banding GROUPS, it does not hide. Every node the estate draws must reach
156// at least one band — a node whose region could not be worked out belongs in
157// the region-less band, never nowhere.
158func TestEveryDrawnNodeReachesABand(t *testing.T) {
159 in := multiRegionFixture()
160 v := BuildArch(in)
161 drawn := map[string]bool{}
162 for _, e := range []ArchEstate{v.CARLOS, v.Rest} {
163 for _, n := range e.Nodes() {
164 drawn[n.ID] = true
165 }
166 }
167 for _, n := range wholeGraphOf(in, v).Nodes {
168 if !drawn[n.ID] {
169 t.Errorf("%s (%s) is drawn by no band — banding groups the page, it must not drop from it", n.ID, n.Kind)
170 }
171 }
172}
173
174// A box states its band; everything joined to it follows. This is the rule
175// in one assertion: the address, the DNS name and the box all land together,
176// in the region the BOX is in, and they do so for every region at once.
177func TestAFlowStaysInTheBandOfItsBox(t *testing.T) {
178 v := BuildArch(multiRegionFixture())
179 for _, c := range []struct{ node, want string }{
180 {"box:carlos-tito-ie", "eu-west-1"},
181 {"ip:eip-carlos-tito-ie", "eu-west-1"},
182 {"record:rec-carlos-tito-ie", "eu-west-1"},
183 {"box:carlos-tito-svc-pdf", "eu-west-1"},
184 {"box:carlos-tito-uk", "eu-west-2"},
185 {"ip:eip-carlos-tito-uk", "eu-west-2"},
186 {"record:rec-carlos-tito-uk", "eu-west-2"},
187 {"box:carlos-tito-se", "eu-north-1"},
188 {"record:rec-carlos-tito-se", "eu-north-1"},
189 // Reaches no machine at all, in either direction. The region-less
190 // band is the honest answer, not a bug to be tidied away.
191 {"record:rec-away", noRegion},
192 {"external:203.0.113.9", noRegion},
193 } {
194 got := bandsHolding(v, c.node)
195 if len(got) != 1 || got[0] != c.want {
196 t.Errorf("%s is drawn in %v, want only %q", c.node, got, c.want)
197 }
198 }
199}
200
201// The deployment bucket is written to from every region, so it is drawn in
202// every band that writes to it. Drawing it once would leave three of four
203// boxes with an edge to a node not in their picture — pruned away, and the
204// one relationship the console knows FIRST-HAND gone from three quarters of
205// the page.
206func TestTheSharedBucketIsDrawnInEveryBandThatWritesToIt(t *testing.T) {
207 v := BuildArch(multiRegionFixture())
208 got := bandsHolding(v, "bucket:carlos-tito-platform")
209 want := []string{"eu-north-1", "eu-west-1", "eu-west-2"}
210 if strings.Join(got, ",") != strings.Join(want, ",") {
211 t.Errorf("the deployment bucket is drawn in %v, want %v — one band per region that writes to it", got, want)
212 }
213}
214
215// Bands are alphabetical, with the region-less band last, and an estate
216// draws one band per region it actually occupies. Alphabetical
217// because this page is read for CHANGE: an order that moved when a box was
218// added would make every band look like it had moved.
219func TestBandsAreOrderedAlphabeticallyWithTheRegionlessBandLast(t *testing.T) {
220 v := BuildArch(multiRegionFixture())
221 if got, want := bandNames(v.CARLOS), []string{"eu-north-1", "eu-west-1", "eu-west-2"}; strings.Join(got, ",") != strings.Join(want, ",") {
222 t.Errorf("CARLOS band order = %v, want %v", got, want)
223 }
224 // The name pointing outside the estate joins no CARLOS box, so it is in
225 // the OTHER bank — and there it is the last band, after the region that
226 // has one.
227 if got, want := bandNames(v.Rest), []string{"us-east-1", noRegion}; strings.Join(got, ",") != strings.Join(want, ",") {
228 t.Errorf("second-bank band order = %v, want %v — the region-less band is drawn last", got, want)
229 }
230 // The region-less band is LABELLED, never left blank: a heading with no
231 // words in it reads as a rendering fault rather than as a fact.
232 last := v.Rest.Bands[len(v.Rest.Bands)-1]
233 if last.Label == "" || last.Label == last.Region {
234 t.Errorf("the region-less band's label is %q, want words a person can read", last.Label)
235 }
236}
237
238// Only a box or a machine may decide a band. An address is IN a region and
239// is still not allowed to say so while it is attached to something, because
240// the thing it is attached to is the one that is really there — and a
241// follower promoted to an anchor stops following, which is how a bucket in
242// eu-west-1 would drag every box in the fleet into one band.
243func TestOnlyMachinesAnchorABand(t *testing.T) {
244 for _, kind := range []string{"box", "machine"} {
245 if !isAnchor(kind) {
246 t.Errorf("%s must anchor a band", kind)
247 }
248 }
249 for _, kind := range []string{"ip", "record", "external", "bucket", "zone", "apps"} {
250 if isAnchor(kind) {
251 t.Errorf("%s must not anchor a band — it follows what it is joined to", kind)
252 }
253 }
254 // And the consequence, on a graph built by hand so the failure is about
255 // the rule rather than about a fixture: an address whose OWN region
256 // disagrees with its box follows the box.
257 g := archGraph{
258 Nodes: []Node{
259 {ID: "box:a", Kind: "box"},
260 {ID: "ip:1", Kind: "ip"},
261 },
262 Edges: []Edge{{From: "ip:1", To: "box:a", Kind: "routes"}},
263 }
264 bands := bandsFrom(g, map[string]string{"box:a": "eu-west-1", "ip:1": "us-east-1"})
265 if len(bands) != 1 || bands[0].Region != "eu-west-1" {
266 t.Fatalf("bands = %v, want one eu-west-1 band holding both", bandRegions(bands))
267 }
268 if len(bands[0].Nodes) != 2 {
269 t.Errorf("the eu-west-1 band holds %d nodes, want the box and its address", len(bands[0].Nodes))
270 }
271}
272
273// An unattached address reaches no machine, so it keeps the region the
274// estate gave it rather than falling into the region-less band. Absence of a
275// join is not absence of a region.
276func TestAnUnattachedAddressKeepsItsOwnRegion(t *testing.T) {
277 g := archGraph{Nodes: []Node{{ID: "ip:loose", Kind: "ip"}}}
278 bands := bandsFrom(g, map[string]string{"ip:loose": "sa-east-1"})
279 if len(bands) != 1 || bands[0].Region != "sa-east-1" {
280 t.Fatalf("bands = %v, want one sa-east-1 band", bandRegions(bands))
281 }
282}
283
284// Every band fits the console's main column (64rem = 1024px). This is the
285// whole user-visible point of the shape: the page scrolls DOWN as an estate
286// gains regions, and never sideways.
287func TestEveryBandFitsTheMainColumn(t *testing.T) {
288 const mainColumnPx = 1024
289 for _, in := range []ArchInput{multiRegionFixture(), sharedAccountFixture(), archFixture()} {
290 v := BuildArch(in)
291 for name, b := range eachBand(v) {
292 if b.Width > mainColumnPx {
293 t.Errorf("%s is %d units wide, want <= %d — a wider band is one the operator has to drag sideways",
294 name, b.Width, mainColumnPx)
295 }
296 if b.Width <= 0 || b.Height <= 0 {
297 t.Errorf("%s has a degenerate viewBox %dx%d", name, b.Width, b.Height)
298 }
299 }
300 }
301}
302
303// Every band in an estate is the same width, so the columns line up down
304// the page. A reader scanning for boxes should not have to find the box
305// column again in each picture.
306func TestBandsInAnEstateShareOneWidth(t *testing.T) {
307 for _, in := range []ArchInput{multiRegionFixture(), sharedAccountFixture(), archFixture()} {
308 v := BuildArch(in)
309 for name, e := range map[string]ArchEstate{"CARLOS": v.CARLOS, "rest of the account": v.Rest} {
310 for _, b := range e.Bands {
311 if b.Width != e.Bands[0].Width {
312 t.Errorf("%s: band %q is %d wide but band %q is %d — the columns will not line up",
313 name, b.Region, b.Width, e.Bands[0].Region, e.Bands[0].Width)
314 }
315 }
316 }
317 }
318}
319
320// bandsHolding names every band one node is drawn in, across both estates,
321// in page order.
322func bandsHolding(v ArchView, id string) []string {
323 var out []string
324 for _, e := range []ArchEstate{v.CARLOS, v.Rest} {
325 for _, b := range e.Bands {
326 for _, n := range b.Nodes {
327 if n.ID == id {
328 out = append(out, b.Region)
329 break
330 }
331 }
332 }
333 }
334 return out
335}
336
337func bandRegions(bands []ArchBand) []string {
338 var out []string
339 for _, b := range bands {
340 out = append(out, fmt.Sprintf("%q(%d nodes)", b.Region, len(b.Nodes)))
341 }
342 return out
343}
344