// Which region band each drawn node belongs to. // // THE GOVERNING RULE: A NODE'S BAND IS THE BAND OF THE MACHINE IT REACHES. // // A region is a physical fact about a MACHINE and about almost nothing else // on this page. DNS is global — every one of Route 53's records and zones // comes back from the estate read with no region at all — an Elastic IP is // in a region but is only interesting for the box it is attached to, and a // bucket is written to from everywhere. So exactly two kinds ANCHOR a band // by stating their own region (a box, and a machine no box reports), and // everything else FOLLOWS the anchors it is joined to. // // The payoff is a property, not a preference: because a follower takes the // band of whatever it is joined to, no edge can span two bands, and each // band is therefore a self-contained little diagram that can be laid out // and drawn on its own. That is what lets the page scroll down instead of // across — the width of the picture stops being the width of the whole // estate and becomes the width of one region's flow. // // A follower joined to two regions is drawn in BOTH, exactly as the zone // shared by both estate banks is (archestate.go). The deployment bucket is // the everyday case: nine boxes in six regions all write to it, and the // alternative — drawing it once and letting five edges dangle — would hide // a relationship the console knows first-hand. // // A node that reaches no anchor at all lands in the region-less band. That // is not a failure state: a DNS name pointing outside the estate genuinely // has no region, and saying so is the honest answer. It is drawn last, // under its own heading, and never dropped. package console import ( "sort" "github.com/carlosframework/platform/internal/infraview" ) // noRegion is the band for everything that reaches no machine. It sorts // last and is labelled in words rather than left blank, because an unlabelled // band reads as a rendering fault. const noRegion = "" // anchorKinds state their own region. Deliberately just these two: they are // the only nodes on the page that stand for something with a physical // location. Adding a kind here would make it a source of truth about where // its NEIGHBOURS live, which is how a bucket in eu-west-1 would drag nine // boxes out of five other regions into one band. func isAnchor(kind string) bool { return kind == "box" || kind == "machine" } // regionSeeds is what each node says about its OWN region, before any // propagation. Anchors are believed; a follower's own region is kept only // as the last resort in bandsOf (an allocated address attached to nothing // still has a region, and putting it in the region-less band would be a // worse answer than the one the estate gave us). func regionSeeds(inv *infraview.Inventory, boxes []BoxCard) map[string]string { seeds := map[string]string{} for _, b := range boxes { if b.Region != "" { seeds["box:"+b.Label] = b.Region } } if inv == nil { return seeds } for _, r := range inv.Resources { if r.Region == "" { continue } switch r.Kind { case infraview.KindMachine: seeds["machine:"+r.ID] = r.Region case infraview.KindIP: seeds["ip:"+r.ID] = r.Region case infraview.KindBucket: seeds["bucket:"+nameOf(r)] = r.Region } } return seeds } // bandsOf decides, for every node, the set of bands it is drawn in. // // Anchors take their seed and are then fixed. Followers accumulate the // bands of their neighbours — in BOTH directions along an edge, because a // record reaches its box forwards and a bucket reaches its boxes backwards, // and a rule that only looked one way would put half the followers in the // region-less band. The sweep repeats until nothing changes; it is bounded // by the node count, since each pass either adds a band to some node or is // the last one. // // Union is commutative, so the result cannot depend on the order edges or // nodes are visited — which is what keeps the whole picture deterministic // (archlayout.go's contract) without this pass having to sort anything. func bandsOf(nodes []Node, edges []Edge, seeds map[string]string) map[string]map[string]bool { in := make(map[string]map[string]bool, len(nodes)) kind := make(map[string]string, len(nodes)) for _, n := range nodes { in[n.ID] = map[string]bool{} kind[n.ID] = n.Kind if isAnchor(n.Kind) { if r, ok := seeds[n.ID]; ok && r != "" { in[n.ID][r] = true } } } for pass := 0; pass <= len(nodes); pass++ { changed := false for _, e := range edges { from, okF := in[e.From] to, okT := in[e.To] if !okF || !okT { continue } if !isAnchor(kind[e.To]) { for r := range from { if !to[r] { to[r] = true changed = true } } } if !isAnchor(kind[e.From]) { for r := range to { if !from[r] { from[r] = true changed = true } } } } if !changed { break } } // Last resort, and only now: a follower that reached no anchor keeps // whatever region the estate gave it. Applying this before the sweep // would let an Elastic IP's own region compete with the box it is // attached to, which is the same mistake as making it an anchor. for id, set := range in { if len(set) == 0 { if r := seeds[id]; r != "" { set[r] = true } } } return in } // bandOrder is the order the bands are drawn: alphabetical by region code, // with the region-less band last. // // Alphabetical rather than by size, and rather than by any home-region // notion, for the reason the whole diagram is deterministic: an operator // reads this page to see what CHANGED, and an order that reshuffles when a // box is added makes every band look like it moved. Region CODES rather // than city names, because a code-to-city table would be a second source of // truth about a cloud's geography and the code is what every other console, // every CLI and every bill already says. func bandOrder(bands map[string]bool) []string { var named []string for r := range bands { if r != noRegion { named = append(named, r) } } sort.Strings(named) if bands[noRegion] { named = append(named, noRegion) } return named } // bandLabel is the heading over one band. func bandLabel(region string) string { if region == noRegion { return "Not in a region" } return region }