carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Which region band each drawn node belongs to.
2//
3// THE GOVERNING RULE: A NODE'S BAND IS THE BAND OF THE MACHINE IT REACHES.
4//
5// A region is a physical fact about a MACHINE and about almost nothing else
6// on this page. DNS is global — every one of Route 53's records and zones
7// comes back from the estate read with no region at all — an Elastic IP is
8// in a region but is only interesting for the box it is attached to, and a
9// bucket is written to from everywhere. So exactly two kinds ANCHOR a band
10// by stating their own region (a box, and a machine no box reports), and
11// everything else FOLLOWS the anchors it is joined to.
12//
13// The payoff is a property, not a preference: because a follower takes the
14// band of whatever it is joined to, no edge can span two bands, and each
15// band is therefore a self-contained little diagram that can be laid out
16// and drawn on its own. That is what lets the page scroll down instead of
17// across — the width of the picture stops being the width of the whole
18// estate and becomes the width of one region's flow.
19//
20// A follower joined to two regions is drawn in BOTH, exactly as the zone
21// shared by both estate banks is (archestate.go). The deployment bucket is
22// the everyday case: nine boxes in six regions all write to it, and the
23// alternative — drawing it once and letting five edges dangle — would hide
24// a relationship the console knows first-hand.
25//
26// A node that reaches no anchor at all lands in the region-less band. That
27// is not a failure state: a DNS name pointing outside the estate genuinely
28// has no region, and saying so is the honest answer. It is drawn last,
29// under its own heading, and never dropped.
30package console
31
32import (
33 "sort"
34
35 "github.com/carlosframework/platform/internal/infraview"
36)
37
38// noRegion is the band for everything that reaches no machine. It sorts
39// last and is labelled in words rather than left blank, because an unlabelled
40// band reads as a rendering fault.
41const noRegion = ""
42
43// anchorKinds state their own region. Deliberately just these two: they are
44// the only nodes on the page that stand for something with a physical
45// location. Adding a kind here would make it a source of truth about where
46// its NEIGHBOURS live, which is how a bucket in eu-west-1 would drag nine
47// boxes out of five other regions into one band.
48func isAnchor(kind string) bool { return kind == "box" || kind == "machine" }
49
50// regionSeeds is what each node says about its OWN region, before any
51// propagation. Anchors are believed; a follower's own region is kept only
52// as the last resort in bandsOf (an allocated address attached to nothing
53// still has a region, and putting it in the region-less band would be a
54// worse answer than the one the estate gave us).
55func regionSeeds(inv *infraview.Inventory, boxes []BoxCard) map[string]string {
56 seeds := map[string]string{}
57 for _, b := range boxes {
58 if b.Region != "" {
59 seeds["box:"+b.Label] = b.Region
60 }
61 }
62 if inv == nil {
63 return seeds
64 }
65 for _, r := range inv.Resources {
66 if r.Region == "" {
67 continue
68 }
69 switch r.Kind {
70 case infraview.KindMachine:
71 seeds["machine:"+r.ID] = r.Region
72 case infraview.KindIP:
73 seeds["ip:"+r.ID] = r.Region
74 case infraview.KindBucket:
75 seeds["bucket:"+nameOf(r)] = r.Region
76 }
77 }
78 return seeds
79}
80
81// bandsOf decides, for every node, the set of bands it is drawn in.
82//
83// Anchors take their seed and are then fixed. Followers accumulate the
84// bands of their neighbours — in BOTH directions along an edge, because a
85// record reaches its box forwards and a bucket reaches its boxes backwards,
86// and a rule that only looked one way would put half the followers in the
87// region-less band. The sweep repeats until nothing changes; it is bounded
88// by the node count, since each pass either adds a band to some node or is
89// the last one.
90//
91// Union is commutative, so the result cannot depend on the order edges or
92// nodes are visited — which is what keeps the whole picture deterministic
93// (archlayout.go's contract) without this pass having to sort anything.
94func bandsOf(nodes []Node, edges []Edge, seeds map[string]string) map[string]map[string]bool {
95 in := make(map[string]map[string]bool, len(nodes))
96 kind := make(map[string]string, len(nodes))
97 for _, n := range nodes {
98 in[n.ID] = map[string]bool{}
99 kind[n.ID] = n.Kind
100 if isAnchor(n.Kind) {
101 if r, ok := seeds[n.ID]; ok && r != "" {
102 in[n.ID][r] = true
103 }
104 }
105 }
106 for pass := 0; pass <= len(nodes); pass++ {
107 changed := false
108 for _, e := range edges {
109 from, okF := in[e.From]
110 to, okT := in[e.To]
111 if !okF || !okT {
112 continue
113 }
114 if !isAnchor(kind[e.To]) {
115 for r := range from {
116 if !to[r] {
117 to[r] = true
118 changed = true
119 }
120 }
121 }
122 if !isAnchor(kind[e.From]) {
123 for r := range to {
124 if !from[r] {
125 from[r] = true
126 changed = true
127 }
128 }
129 }
130 }
131 if !changed {
132 break
133 }
134 }
135 // Last resort, and only now: a follower that reached no anchor keeps
136 // whatever region the estate gave it. Applying this before the sweep
137 // would let an Elastic IP's own region compete with the box it is
138 // attached to, which is the same mistake as making it an anchor.
139 for id, set := range in {
140 if len(set) == 0 {
141 if r := seeds[id]; r != "" {
142 set[r] = true
143 }
144 }
145 }
146 return in
147}
148
149// bandOrder is the order the bands are drawn: alphabetical by region code,
150// with the region-less band last.
151//
152// Alphabetical rather than by size, and rather than by any home-region
153// notion, for the reason the whole diagram is deterministic: an operator
154// reads this page to see what CHANGED, and an order that reshuffles when a
155// box is added makes every band look like it moved. Region CODES rather
156// than city names, because a code-to-city table would be a second source of
157// truth about a cloud's geography and the code is what every other console,
158// every CLI and every bill already says.
159func bandOrder(bands map[string]bool) []string {
160 var named []string
161 for r := range bands {
162 if r != noRegion {
163 named = append(named, r)
164 }
165 }
166 sort.Strings(named)
167 if bands[noRegion] {
168 named = append(named, noRegion)
169 }
170 return named
171}
172
173// bandLabel is the heading over one band.
174func bandLabel(region string) string {
175 if region == noRegion {
176 return "Not in a region"
177 }
178 return region
179}
180