package console import ( "fmt" "sort" "strings" "testing" "github.com/carlosframework/platform/internal/infraview" "github.com/carlosframework/platform/internal/status" ) // multiRegionFixture is the estate this feature exists for: boxes in four // regions, two of them in the SAME region (so a band has to hold more than // one flow), one machine no box reports, one DNS name pointing outside the // estate entirely, and one deployment bucket every box writes to. func multiRegionFixture() ArchInput { res := []infraview.Resource{ {Kind: infraview.KindDNSZone, ID: "Z1", Name: "tito.io"}, {Kind: infraview.KindBucket, ID: "carlos-tito-platform", Name: "carlos-tito-platform", Region: "eu-west-1"}, // A name that leaves the estate. It reaches no machine, so it and // the record naming it belong to no region at all. {Kind: infraview.KindDNSRecord, ID: "rec-away", Name: "mail.tito.io", Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "203.0.113.9"}}, // A machine nothing reports: drift, and it anchors its own band. {Kind: infraview.KindMachine, ID: "i-orphan", Name: "old-thing", Region: "us-east-1", Attrs: map[string]string{"type": "t3.micro", "state": "stopped"}}, } type box struct{ label, region, machine, addr string } boxes := []box{ {"carlos-tito-ie", "eu-west-1", "i-ie", "52.0.0.1"}, {"carlos-tito-svc-pdf", "eu-west-1", "i-pdf", "52.0.0.2"}, {"carlos-tito-uk", "eu-west-2", "i-uk", "52.0.0.3"}, {"carlos-tito-se", "eu-north-1", "i-se", "52.0.0.4"}, } var fleet []status.FleetBox var hosts []HostRow for _, b := range boxes { res = append(res, infraview.Resource{Kind: infraview.KindMachine, ID: b.machine, Name: b.label, Region: b.region, Attrs: map[string]string{"type": "t4g.small", "state": "running"}}, infraview.Resource{Kind: infraview.KindIP, ID: "eip-" + b.label, Region: b.region, Attrs: map[string]string{"address": b.addr, "attached_to": b.machine}}, infraview.Resource{Kind: infraview.KindDNSRecord, ID: "rec-" + b.label, Name: b.label + ".tito.io", Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": b.addr}}, ) fleet = append(fleet, status.FleetBox{Label: b.label, Region: b.region}) hosts = append(hosts, HostRow{Label: b.label, Region: b.region, Metrics: &status.BoxMetrics{Release: "abc1234", InstanceID: b.machine}}) } return ArchInput{ Inv: &infraview.Inventory{Provider: "aws", Account: "665106592142", Resources: res}, Bucket: "carlos-tito-platform", Fleet: &status.Fleet{Boxes: fleet}, Hosts: hosts, } } // bandNames is every picture on the page, in the order it is drawn. func bandNames(e ArchEstate) []string { var out []string for _, b := range e.Bands { out = append(out, b.Region) } return out } // THE property the whole band design rests on, and it has two halves that // must BOTH be asserted. // // A band is drawn as a picture of its own, so an edge with one end in // another band cannot be drawn — and pruneDanglingEdges duly removes it from // both. That is the trap: checking only that every DRAWN edge has both ends // in its band is a test a totally broken banding rule passes, because the // spanning edges are gone by the time it looks. So the second half asks the // question that actually matters — every edge that ought to be on the page // IS on the page — and it is the half that fails when a follower stops // following. func TestNoEdgeSpansTwoBands(t *testing.T) { in := multiRegionFixture() v := BuildArch(in) drawn := map[string]bool{} for _, e := range []ArchEstate{v.CARLOS, v.Rest} { for _, n := range e.Nodes() { drawn[n.ID] = true } } kept := map[Edge]bool{} for _, e := range archEdges(v) { kept[e] = true } // Both ends drawn somewhere on the page means the edge belongs on it: // an edge across the two BANKS is impossible (archestate_test.go pins // that), so anything missing here went missing across two BANDS. for _, e := range wholeGraphOf(in, v).Edges { if drawn[e.From] && drawn[e.To] && !kept[e] { t.Errorf("edge %s -> %s is drawn by no band: both its ends are on the page and the relationship is not", e.From, e.To) } } for _, e := range []ArchEstate{v.CARLOS, v.Rest} { // Where each node is drawn. A node may legitimately be in several // bands (the deployment bucket), so this is a set per node. bands := map[string]map[string]bool{} for _, b := range e.Bands { for _, n := range b.Nodes { if bands[n.ID] == nil { bands[n.ID] = map[string]bool{} } bands[n.ID][b.Region] = true } } for _, b := range e.Bands { for _, ed := range b.Edges { if !bands[ed.From][b.Region] || !bands[ed.To][b.Region] { t.Errorf("edge %s -> %s is drawn in band %q, which does not hold both its ends (%v / %v)", ed.From, ed.To, b.Region, keysOf(bands[ed.From]), keysOf(bands[ed.To])) } } } } } // wholeGraphOf rebuilds the graph as it stands BEFORE banding — every node // and edge the joins produced — so a test can ask what the page ought to be // showing rather than only what it is. func wholeGraphOf(in ArchInput, v ArchView) archGraph { claimed := map[string]string{} for _, b := range v.Boxes { if b.CloudID != "" { claimed[b.CloudID] = b.Label } } var g archGraph addDNSNodes(&g, in.Inv, claimed) addBoxNodes(&g, v.Boxes, in) addMachineNodes(&g, in.Inv, claimed) g.Nodes = dropSatellites(g.Nodes) return g } func keysOf(m map[string]bool) []string { var out []string for k := range m { out = append(out, k) } sort.Strings(out) return out } // Banding GROUPS, it does not hide. Every node the estate draws must reach // at least one band — a node whose region could not be worked out belongs in // the region-less band, never nowhere. func TestEveryDrawnNodeReachesABand(t *testing.T) { in := multiRegionFixture() v := BuildArch(in) drawn := map[string]bool{} for _, e := range []ArchEstate{v.CARLOS, v.Rest} { for _, n := range e.Nodes() { drawn[n.ID] = true } } for _, n := range wholeGraphOf(in, v).Nodes { if !drawn[n.ID] { t.Errorf("%s (%s) is drawn by no band — banding groups the page, it must not drop from it", n.ID, n.Kind) } } } // A box states its band; everything joined to it follows. This is the rule // in one assertion: the address, the DNS name and the box all land together, // in the region the BOX is in, and they do so for every region at once. func TestAFlowStaysInTheBandOfItsBox(t *testing.T) { v := BuildArch(multiRegionFixture()) for _, c := range []struct{ node, want string }{ {"box:carlos-tito-ie", "eu-west-1"}, {"ip:eip-carlos-tito-ie", "eu-west-1"}, {"record:rec-carlos-tito-ie", "eu-west-1"}, {"box:carlos-tito-svc-pdf", "eu-west-1"}, {"box:carlos-tito-uk", "eu-west-2"}, {"ip:eip-carlos-tito-uk", "eu-west-2"}, {"record:rec-carlos-tito-uk", "eu-west-2"}, {"box:carlos-tito-se", "eu-north-1"}, {"record:rec-carlos-tito-se", "eu-north-1"}, // Reaches no machine at all, in either direction. The region-less // band is the honest answer, not a bug to be tidied away. {"record:rec-away", noRegion}, {"external:203.0.113.9", noRegion}, } { got := bandsHolding(v, c.node) if len(got) != 1 || got[0] != c.want { t.Errorf("%s is drawn in %v, want only %q", c.node, got, c.want) } } } // The deployment bucket is written to from every region, so it is drawn in // every band that writes to it. Drawing it once would leave three of four // boxes with an edge to a node not in their picture — pruned away, and the // one relationship the console knows FIRST-HAND gone from three quarters of // the page. func TestTheSharedBucketIsDrawnInEveryBandThatWritesToIt(t *testing.T) { v := BuildArch(multiRegionFixture()) got := bandsHolding(v, "bucket:carlos-tito-platform") want := []string{"eu-north-1", "eu-west-1", "eu-west-2"} if strings.Join(got, ",") != strings.Join(want, ",") { t.Errorf("the deployment bucket is drawn in %v, want %v — one band per region that writes to it", got, want) } } // Bands are alphabetical, with the region-less band last, and an estate // draws one band per region it actually occupies. Alphabetical // because this page is read for CHANGE: an order that moved when a box was // added would make every band look like it had moved. func TestBandsAreOrderedAlphabeticallyWithTheRegionlessBandLast(t *testing.T) { v := BuildArch(multiRegionFixture()) if got, want := bandNames(v.CARLOS), []string{"eu-north-1", "eu-west-1", "eu-west-2"}; strings.Join(got, ",") != strings.Join(want, ",") { t.Errorf("CARLOS band order = %v, want %v", got, want) } // The name pointing outside the estate joins no CARLOS box, so it is in // the OTHER bank — and there it is the last band, after the region that // has one. if got, want := bandNames(v.Rest), []string{"us-east-1", noRegion}; strings.Join(got, ",") != strings.Join(want, ",") { t.Errorf("second-bank band order = %v, want %v — the region-less band is drawn last", got, want) } // The region-less band is LABELLED, never left blank: a heading with no // words in it reads as a rendering fault rather than as a fact. last := v.Rest.Bands[len(v.Rest.Bands)-1] if last.Label == "" || last.Label == last.Region { t.Errorf("the region-less band's label is %q, want words a person can read", last.Label) } } // Only a box or a machine may decide a band. An address is IN a region and // is still not allowed to say so while it is attached to something, because // the thing it is attached to is the one that is really there — and a // follower promoted to an anchor stops following, which is how a bucket in // eu-west-1 would drag every box in the fleet into one band. func TestOnlyMachinesAnchorABand(t *testing.T) { for _, kind := range []string{"box", "machine"} { if !isAnchor(kind) { t.Errorf("%s must anchor a band", kind) } } for _, kind := range []string{"ip", "record", "external", "bucket", "zone", "apps"} { if isAnchor(kind) { t.Errorf("%s must not anchor a band — it follows what it is joined to", kind) } } // And the consequence, on a graph built by hand so the failure is about // the rule rather than about a fixture: an address whose OWN region // disagrees with its box follows the box. g := archGraph{ Nodes: []Node{ {ID: "box:a", Kind: "box"}, {ID: "ip:1", Kind: "ip"}, }, Edges: []Edge{{From: "ip:1", To: "box:a", Kind: "routes"}}, } bands := bandsFrom(g, map[string]string{"box:a": "eu-west-1", "ip:1": "us-east-1"}) if len(bands) != 1 || bands[0].Region != "eu-west-1" { t.Fatalf("bands = %v, want one eu-west-1 band holding both", bandRegions(bands)) } if len(bands[0].Nodes) != 2 { t.Errorf("the eu-west-1 band holds %d nodes, want the box and its address", len(bands[0].Nodes)) } } // An unattached address reaches no machine, so it keeps the region the // estate gave it rather than falling into the region-less band. Absence of a // join is not absence of a region. func TestAnUnattachedAddressKeepsItsOwnRegion(t *testing.T) { g := archGraph{Nodes: []Node{{ID: "ip:loose", Kind: "ip"}}} bands := bandsFrom(g, map[string]string{"ip:loose": "sa-east-1"}) if len(bands) != 1 || bands[0].Region != "sa-east-1" { t.Fatalf("bands = %v, want one sa-east-1 band", bandRegions(bands)) } } // Every band fits the console's main column (64rem = 1024px). This is the // whole user-visible point of the shape: the page scrolls DOWN as an estate // gains regions, and never sideways. func TestEveryBandFitsTheMainColumn(t *testing.T) { const mainColumnPx = 1024 for _, in := range []ArchInput{multiRegionFixture(), sharedAccountFixture(), archFixture()} { v := BuildArch(in) for name, b := range eachBand(v) { if b.Width > mainColumnPx { t.Errorf("%s is %d units wide, want <= %d — a wider band is one the operator has to drag sideways", name, b.Width, mainColumnPx) } if b.Width <= 0 || b.Height <= 0 { t.Errorf("%s has a degenerate viewBox %dx%d", name, b.Width, b.Height) } } } } // Every band in an estate is the same width, so the columns line up down // the page. A reader scanning for boxes should not have to find the box // column again in each picture. func TestBandsInAnEstateShareOneWidth(t *testing.T) { for _, in := range []ArchInput{multiRegionFixture(), sharedAccountFixture(), archFixture()} { v := BuildArch(in) for name, e := range map[string]ArchEstate{"CARLOS": v.CARLOS, "rest of the account": v.Rest} { for _, b := range e.Bands { if b.Width != e.Bands[0].Width { t.Errorf("%s: band %q is %d wide but band %q is %d — the columns will not line up", name, b.Region, b.Width, e.Bands[0].Region, e.Bands[0].Width) } } } } } // bandsHolding names every band one node is drawn in, across both estates, // in page order. func bandsHolding(v ArchView, id string) []string { var out []string for _, e := range []ArchEstate{v.CARLOS, v.Rest} { for _, b := range e.Bands { for _, n := range b.Nodes { if n.ID == id { out = append(out, b.Region) break } } } } return out } func bandRegions(bands []ArchBand) []string { var out []string for _, b := range bands { out = append(out, fmt.Sprintf("%q(%d nodes)", b.Region, len(b.Nodes))) } return out }