// The architecture graph behind /fleet/architecture: one pure function that // takes a cloud inventory plus everything CARLOS knows about itself and // produces the page's whole view model — nodes, edges, box cards and // resource groups. // // Three rules run through all of it (2026-08-19-fleet-architecture-diagram- // design.md §6): // // - UNJOINED IS DRAWN, NEVER HIDDEN. A machine no box reports is drift, a // box with no machine is a customer's own hardware, a DNS record pointing // outside the estate is an arrow leaving the diagram, and a resource this // vocabulary has no word for still reaches the resource groups with its // native type. The resource nobody wrote code for is the one worth // seeing, so nothing is silently dropped. // - COST COMES FROM THE RATE CARD AND NOWHERE ELSE, so this page and // /fleet/economics can never disagree about what a box costs. The // discovered instance type and disk sit BESIDE the price and are used // only to flag drift — never to compute a second one. // - EVERY READ IS INDEPENDENT. A nil inventory, a nil fleet, an empty host // table and an empty route list each cost their own section and nothing // else. BuildArch never panics on any combination, including all-nil. // // BuildArch is pure — no I/O, no clock, no registry or bucket access. // Everything it needs arrives in ArchInput, which is what makes it // table-testable and what lets archlayout.go depend on it being // deterministic. package console import ( "fmt" "html/template" "sort" "strconv" "strings" "time" "github.com/carlosframework/platform/internal/economics" "github.com/carlosframework/platform/internal/infraview" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/status" ) // ArchInput is every read the page makes, handed over as data. Each field is // independently optional: whichever reads succeeded are drawn, and the ones // that did not simply do not appear. type ArchInput struct { Inv *infraview.Inventory Fleet *status.Fleet Hosts []HostRow Routes []registry.Route Rates economics.RateCard // Bucket is the deployment bucket the console itself is configured with // — the ONE bucket relationship the console can state as fact (spec // §6.2). Empty draws no bucket edge at all: the estate may hold six // buckets and guessing which one the fleet writes to would put a false // arrow on a page whose whole value is that its arrows are true. Bucket string } // KV is one detail line on a node. type KV struct{ K, V string } // Node is one drawn thing. X/Y/W/H are filled by layout (archlayout.go), not // here: BuildArch owns what the picture MEANS, layout owns where it sits. type Node struct { ID, Kind, Title, Subtitle string Detail []KV // Alarms are the alarms watching THIS node's subject. They ride the // node rather than standing as nodes of their own, for two reasons an // earlier draft learned the hard way: a satellite column placed // between the IP and the box sits underneath every IP-to-box edge, and // nodes paint after edges, so the line vanished behind it and the // picture read as a request flowing THROUGH the alarm; and an alarm is // not a hop a request takes, it is a property of the thing it watches. // The Alarms resource table still lists every one of them in full. Alarms []NodeAlarm // Extra are further muted lines under the subtitle, and they are why a // node's height is computed rather than constant. They exist so a fact // that is a PROPERTY of a node can sit on it instead of standing in the // flow as a node of its own: the apps a box runs used to be their own // column, six of nine cards in it reading "no apps", and that column was // a fifth of the width of a picture too wide for the page. Extra []string X, Y, W, H int } // NodeAlarm is one alarm as its subject wears it: enough to say which // alarm and whether it is firing, and nothing else — the table below the // diagram carries the metric, the region and the ARN. type NodeAlarm struct { Name string State string Firing bool } // Edge is one relationship the console can state as fact. Kind is the // vocabulary of spec §6.1: resolves, routes, runs, stores, watches. type Edge struct{ From, To, Kind string } // BoxCard is one box below the diagram: what CARLOS reports about it, what // the estate says it is, what the rate card says it costs, and every way // those three disagree. type BoxCard struct { Label, Region, InstanceType, State string Cores, MemGB, EBSGB float64 DiscoveredDiskGB float64 USDMonth float64 Apps []string // Static/Resident/Hibernating come from the fleet aggregate and ONLY // from it, so a box that read never described has no counts at all — // not three zeroes. CountsKnown is that fact, and it gates every // rendering of them: a zero here would be a claim about a read that // never happened, two words from an Awake/Asleep that correctly says // "unknown". Static, Resident, Hibernating int CountsKnown bool Awake, Asleep *int Release, Load string Stale bool Dark string Drift []string CloudID string // "" = no cloud machine behind this box } // ResourceGroup is one table below the diagram. type ResourceGroup struct { Title string Rows []ResourceRow } // ResourceRow is one resource in one of those tables. type ResourceRow struct{ Name, NativeType, Region, Detail string } // archGraph is a set of nodes and the edges between them — the shape every // pass below builds into, before anything has been dealt into a bank or a // band. It is unexported because it is a stage, not a thing the page shows. type archGraph struct { Nodes []Node Edges []Edge } // ArchBand is one region's picture: the nodes that reach a machine in that // region, the edges between them, and its own size. It is a whole diagram // on its own — laid out, sized and rendered without reference to any other // band — which is what lets the page stack six of them down the screen // instead of drawing one picture wider than the column. // // Region is the code the cloud uses ("eu-west-1") or "" for the band of // things that reach no machine at all; Label is how that reads to a person. type ArchBand struct { archGraph Region string Label string Width, Height int } // SVG renders this band. A method rather than a field on the page struct // because a band knows how to draw itself and the number of bands is not // known until the estate is built — the template ranges over them. func (b ArchBand) SVG() template.HTML { return RenderArchSVG(b) } // ArchEstate is one half of the page: its own bands, and its own tables. // Nothing in this type knows which half it is — layout and RenderArchSVG // take one BAND and have no opinion about the estate it came from, which is // what makes running them per band free and each picture independently // deterministic. type ArchEstate struct { Bands []ArchBand Groups []ResourceGroup } // Nodes and Edges are the estate whole again, across every band. A node // drawn in two bands (the deployment bucket, which every region writes to) // appears once per band here — the callers that count resources dedupe by // id, exactly as they already do for a node drawn in both banks. func (e ArchEstate) Nodes() []Node { var out []Node for _, b := range e.Bands { out = append(out, b.Nodes...) } return out } func (e ArchEstate) Edges() []Edge { var out []Edge for _, b := range e.Bands { out = append(out, b.Edges...) } return out } // ArchView is the whole page: two estates drawn one above the other, and // the facts that belong to neither. // // Rest is deliberately NOT called "legacy" anywhere a person can read it. // This console serves every CARLOS deployment, and what shares Tito's // account is a Kubernetes estate while what shares somebody else's will be // something else entirely. The page says "also in this account", which is // the only thing that is true in all of them. type ArchView struct { CARLOS ArchEstate Rest ArchEstate Boxes []BoxCard TakenAt time.Time Account string Partial []string Missing string // "" | "never" | "ungranted" | "unreadable" // Firing names every alarm currently in ALARM, with the node it watches, // so the page can say so above the fold. // // The marks themselves ride their nodes, which is right — an alarm is a // property of the thing it watches — but it made a firing alarm invisible // on arrival: the marks are off to the right of a drawing wider than the // window, and the Alarms table is thousands of pixels down (discussion // #9). A page that knows something is on fire has to say so where the // reader already is. // // It spans BOTH estates: the reader wants to know something is firing, // not which half of the page to look in. Firing []string } // firingAlarms collects the ALARM-state marks across the drawn nodes, in the // nodes' own order so the summary is as deterministic as the diagram. A node // drawn in more than one band (ArchEstate.Nodes) is counted once: the banner // names alarms, and an alarm does not fire twice for being drawn twice. func firingAlarms(nodes []Node) []string { var out []string seen := map[string]bool{} for _, n := range nodes { if seen[n.ID] { continue } seen[n.ID] = true for _, a := range n.Alarms { if a.Firing { out = append(out, a.Name+" ("+n.Title+")") } } } return out } // BuildArch joins the estate to the platform's own model of itself. It reads // as the sequence of joins spec §6.2 names, in the order a request travels: // boxes first (everything else needs to know which machine belongs to whom), // then DNS in at the left, then the boxes and their apps, then the // satellites. func BuildArch(in ArchInput) ArchView { var v ArchView if in.Inv == nil { // The caller overwrites this with the reason it actually has // ("ungranted", "unreadable"); "never" is the honest default for a // builder handed nothing and told nothing. v.Missing = "never" } else { v.TakenAt = in.Inv.TakenAt v.Account = in.Inv.Account v.Partial = in.Inv.Partial } v.Boxes = buildBoxCards(in) // machineID -> box label, the join every later step asks about. Built // once here so "does a box report this machine?" is one lookup and // cannot drift between the DNS pass and the alarm pass. claimed := map[string]string{} for _, b := range v.Boxes { if b.CloudID != "" { claimed[b.CloudID] = b.Label } } // The whole graph is built once, exactly as it always was, and only then // dealt into two banks. Building it whole is what keeps the joins in one // place: a record still finds its IP, and an alarm still finds its box, // without either pass having to know which half of the account it is // looking at. var whole archGraph addDNSNodes(&whole, in.Inv, claimed) addBoxNodes(&whole, v.Boxes, in) addMachineNodes(&whole, in.Inv, claimed) addAlarmBadges(&whole, in.Inv, claimed) carlos := carlosResources(in.Inv, claimed, in.Bucket) bank := splitNodes(whole.Nodes, whole.Edges, carlos) var forBank [2]archGraph for _, n := range whole.Nodes { if bank[n.ID]&estateCARLOS != 0 { forBank[0].Nodes = append(forBank[0].Nodes, n) } if bank[n.ID]&estateRest != 0 { forBank[1].Nodes = append(forBank[1].Nodes, n) } } // Both banks get every edge and each then prunes what it does not draw. // An edge whose ends are in different banks therefore survives in // neither — which is why archestate_test.go asserts no such edge exists, // rather than this loop quietly deciding it does not matter. forBank[0].Edges = append([]Edge(nil), whole.Edges...) forBank[1].Edges = append([]Edge(nil), whole.Edges...) v.CARLOS.Groups, v.Rest.Groups = splitResourceGroups(in.Inv, carlos) seeds := regionSeeds(in.Inv, v.Boxes) for i, e := range []*ArchEstate{&v.CARLOS, &v.Rest} { e.Bands = bandsFrom(forBank[i], seeds) } // After layout, for the same reason the pruning is: layout is what makes // the node set final, and a summary naming an alarm on a node the diagram // does not draw would send a reader looking for a mark that is not there. // // Both estates, CARLOS first, and each estate's bands in page order // (ArchEstate.Nodes walks them so), so the order is the order the page // draws them in — a firing alarm should be findable by reading downwards. v.Firing = append(firingAlarms(v.CARLOS.Nodes()), firingAlarms(v.Rest.Nodes())...) return v } // bandsFrom deals one bank's graph into its region bands and lays each one // out. The ORDER of the three steps is the whole of it: // // 1. drop the satellite kinds, because banding must not propagate a region // along an edge to a node that is not going to be drawn — a zone is // global and joined to records in every region, and letting it carry // bands would put every record in every band; // 2. band what is left, which is what makes an edge unable to span two // bands; // 3. lay each band out and only THEN prune its dangling edges, because // layout is what makes a band's node set final. func bandsFrom(g archGraph, seeds map[string]string) []ArchBand { g.Nodes = dropSatellites(g.Nodes) in := bandsOf(g.Nodes, g.Edges, seeds) present := map[string]bool{} for _, n := range g.Nodes { if len(in[n.ID]) == 0 { present[noRegion] = true continue } for r := range in[n.ID] { present[r] = true } } // An estate with nothing in it still gets one empty band, so the page // has a picture to draw rather than a gap where one should be — the // same reason layout clamps an empty graph to a positive viewBox. if len(present) == 0 { present[noRegion] = true } var bands []ArchBand for _, region := range bandOrder(present) { b := ArchBand{Region: region, Label: bandLabel(region)} for _, n := range g.Nodes { set := in[n.ID] if (len(set) == 0 && region == noRegion) || set[region] { b.Nodes = append(b.Nodes, n) } } b.Edges = append([]Edge(nil), g.Edges...) layout(&b) pruneDanglingEdges(&b) bands = append(bands, b) } // One width for every band in an estate, so the columns line up down the // page: the box column is in the same place in eu-west-1 as in // us-east-1, and a reader's eye can travel down it. Layout sizes each // band to its own deepest column, which left a band of two columns half // the width of one with four and the stack looking ragged. Node // positions do not move — X is the column index, not a fraction of the // width — so this only ever widens the viewBox. widest := 0 for _, b := range bands { if b.Width > widest { widest = b.Width } } for i := range bands { bands[i].Width = widest } return bands } // dropSatellites removes the kinds the page lists in a table instead of // drawing. Identities, firewalls, networks, disks and certificates because // a graph that draws every IAM role is a hairball and the resource groups // carry them in full; ZONES because a zone is global — Route 53 hands one // back with no region and records in six of them — so a drawn zone is // either in no band, cutting every record loose from its name, or in all of // them, which is six copies of one node whose only edge is to a record // already labelled with the zone's own name. The DNS table below still // lists every zone. // // Alarms are not in this list because they are no longer nodes at all — // they ride the node they watch (see archLayer). func dropSatellites(nodes []Node) []Node { kept := nodes[:0] for _, n := range nodes { if strings.Contains(satelliteKinds, n.Kind) { continue } kept = append(kept, n) } return kept } // pruneDanglingEdges drops any edge whose endpoints this view does not // draw. One rule in one place, applied last — after layout, which is what // decides which nodes are drawn — so every pass above can join on whatever // it reads without each of them checking. It matters because a // PARTIAL inventory is normal (infraview.Inventory.Partial): an EIP whose // instance read failed, or an alarm naming a machine that has since been // terminated, would otherwise leave an arrow pointing at nothing — and an // arrow pointing at nothing is exactly the kind of claim this page exists // to avoid making. func pruneDanglingEdges(v *ArchBand) { drawn := make(map[string]bool, len(v.Nodes)) for _, n := range v.Nodes { drawn[n.ID] = true } kept := v.Edges[:0] for _, e := range v.Edges { if drawn[e.From] && drawn[e.To] { kept = append(kept, e) } } v.Edges = kept } // ---- the joins --------------------------------------------------------- // machineFor finds the cloud machine a box runs on. // // The box's OWN report wins: status.BoxMetrics.InstanceID is the id it read // from the instance metadata service, and nothing else can be as sure. When // a box reports one, that id is the answer — a hit joins, and a MISS stays a // miss rather than falling through to a name match, because a box that has // told us which machine it is must not be quietly attached to a different // one. (A miss is real: an instance terminated between the box's last // snapshot and this inventory.) // // The Name-tag match is the fallback, and only for a box that reports no id // at all — an agent that predates the field, which during a roll is every // box until it restarts. It is a fallback rather than the rule because it // is a naming convention, and on the flagship that convention never held: // the instances are tagged carlos-flagship-ie while the boxes call // themselves annie-welding, so this function matched nothing at all and // every box on the page read as bring-your-own hardware. func machineFor(inv *infraview.Inventory, label string, reported string) *infraview.Resource { if inv == nil { return nil } if reported != "" { for i := range inv.Resources { r := &inv.Resources[i] if r.Kind == infraview.KindMachine && r.ID == reported { return r } } return nil } if label == "" { return nil } for i := range inv.Resources { r := &inv.Resources[i] if r.Kind == infraview.KindMachine && strings.EqualFold(r.Name, label) { return r } } return nil } // diskGBFor sums every disk attached to one machine. A size that will not // parse is absence, not zero — it simply does not contribute, and the card // then makes no disk claim rather than a false one. func diskGBFor(inv *infraview.Inventory, machineID string) float64 { if inv == nil || machineID == "" { return 0 } var total float64 for _, d := range inv.ByKind(infraview.KindDisk) { if d.Attr("attached_to") != machineID { continue } if gb, err := strconv.ParseFloat(d.Attr("size_gb"), 64); err == nil { total += gb } } return total } // appsFor names the apps a box is running: its own RouteVersions keys are // its hostnames (the one place a box's real hostnames are carried), looked // up in the registry for the app they belong to. Distinct and sorted, so two // routes of one app read as one app and the order never moves. func appsFor(host HostRow, routes []registry.Route) []string { if host.Metrics == nil || len(host.Metrics.RouteVersions) == 0 { return nil } byHost := make(map[string]string, len(routes)) for _, r := range routes { byHost[r.Host] = r.App } seen := map[string]bool{} var out []string for h := range host.Metrics.RouteVersions { app := byHost[h] if app == "" || seen[app] { continue } seen[app] = true out = append(out, app) } sort.Strings(out) return out } // boxCost prices one box the way /fleet/economics prices it and no other // way: the rate card's own spec for that label, falling back to the "" // default exactly as economics.AllocateBox does. explicit is false when the // fallback was used, which is itself worth telling the operator — a box // priced by the default spec is priced by a guess. func boxCost(rc economics.RateCard, label string) (usd float64, spec economics.BoxSpec, explicit bool) { spec, explicit = rc.Boxes[label] if !explicit { spec = rc.Boxes[""] } usd = spec.HourUSD*economics.HoursPerMonth + spec.EBSGB*rc.EBSGBMonthUSD return usd, spec, explicit } // driftFor names every way the rate card and the estate disagree about this // box. Two checks only — the two this page can make honestly. Comparing the // instance TYPE against the card's cores/memory would need a type->shape // table, which is a second source of truth and out of scope (spec §10). func driftFor(card BoxCard, spec economics.BoxSpec, explicit bool) []string { var out []string if !explicit { out = append(out, "priced by the default spec — no rate-card entry for this box") } if card.DiscoveredDiskGB > 0 && spec.EBSGB > 0 { if d := card.DiscoveredDiskGB - spec.EBSGB; d >= 1 || d <= -1 { out = append(out, fmt.Sprintf("rate card says %.0f GB of EBS, the estate has %.0f GB", spec.EBSGB, card.DiscoveredDiskGB)) } } return out } // ---- box cards --------------------------------------------------------- // buildBoxCards is the union of the two reads that know about boxes: the // published fleet (regions, load, route counts, staleness) and the private // host table (release, hostnames, dark time). Either one alone still // produces a full set of cards, because either one alone is a legitimate // state of the page — the fleet read and the host read fail separately. func buildBoxCards(in ArchInput) []BoxCard { fleetBoxes := map[string]status.FleetBox{} var labels []string seen := map[string]bool{} add := func(label string) { if label == "" || seen[label] { return } seen[label] = true labels = append(labels, label) } if in.Fleet != nil { for _, b := range in.Fleet.Boxes { fleetBoxes[b.Label] = b add(b.Label) } } hosts := map[string]HostRow{} for _, h := range in.Hosts { hosts[h.Label] = h add(h.Label) } sort.Strings(labels) cards := make([]BoxCard, 0, len(labels)) for _, label := range labels { // described is whether the FLEET read produced a row for this box. // A box the host table alone knows about — and every box when the // fleet read failed — has no counts, and fb's zero value must not be // mistaken for a fleet that counted zero of everything. fb, described := fleetBoxes[label] host := hosts[label] card := BoxCard{ Label: label, Region: firstNonEmpty(fb.Region, host.Region), Static: fb.Routes.Static, Resident: fb.Routes.Resident, Hibernating: fb.Routes.Hibernating, CountsKnown: described, Awake: fb.Routes.Awake, Asleep: fb.Routes.Asleep, Stale: fb.Stale || host.Stale, Dark: host.Dark, Apps: appsFor(host, in.Routes), } if fb.Load != nil { card.Load = *fb.Load } // Metrics is nil for a stale box by design (see HostRow) — an // unreachable host's last happy numbers must never reach the page, // and reading through the pointer is where that promise is kept. // reportedID is the machine this box says it is. It comes from the // same Metrics block as Release, so a STALE box reports neither — // which is correct: a dark box's claim about which machine it was is // exactly as old as its other readings, and the page falls back to // the name match rather than trusting history. var reportedID string if host.Metrics != nil { card.Release = host.Metrics.Release reportedID = host.Metrics.InstanceID } if m := machineFor(in.Inv, label, reportedID); m != nil { card.CloudID = m.ID card.InstanceType = m.Attr("type") card.State = m.Attr("state") card.Region = firstNonEmpty(card.Region, m.Region) card.DiscoveredDiskGB = diskGBFor(in.Inv, m.ID) } // A box with no cloud machine behind it is somebody else's // hardware — a customer's fleet box, the Pi on a desk. The rate // card prices what THIS platform pays for, so putting a monthly // figure on one of those is a claim about a bill we do not pay, // and "priced by the default spec" is a warning about a pricing // that should not be happening at all. Both are withheld; the // card still says plainly that there is no cloud machine. usd, spec, explicit := boxCost(in.Rates, label) if card.CloudID != "" { card.USDMonth = usd card.Cores, card.MemGB, card.EBSGB = spec.Cores, spec.MemGB, spec.EBSGB card.Drift = driftFor(card, spec, explicit) } cards = append(cards, card) } return cards } // ---- nodes and edges --------------------------------------------------- // addDNSNodes draws the left of the picture: zones, their records, and where // each record actually points. A record whose value is an address the estate // does not own gets an external node — an arrow leaving the diagram, per // spec §6.3 — rather than being dropped, because "this name points somewhere // we do not run" is exactly the thing an operator opens this page to see. func addDNSNodes(v *archGraph, inv *infraview.Inventory, claimed map[string]string) { if inv == nil { return } zones := inv.ByKind(infraview.KindDNSZone) for _, z := range zones { sub := "" if n := z.Attr("record_count"); n != "" { sub = n + " records" } v.Nodes = append(v.Nodes, Node{ ID: "zone:" + z.ID, Kind: "zone", Title: nameOf(z), Subtitle: sub, }) } for _, ip := range inv.ByKind(infraview.KindIP) { id := "ip:" + ip.ID v.Nodes = append(v.Nodes, Node{ ID: id, Kind: "ip", Title: firstNonEmpty(ip.Attr("address"), nameOf(ip)), Subtitle: ipSubtitle(ip, claimed), }) if to := targetOf(ip.Attr("attached_to"), claimed); to != "" { v.Edges = append(v.Edges, Edge{From: id, To: to, Kind: "routes"}) } } addRecordNodes(v, inv, zones, claimed) } // recordGroup collects every DNS record that shares a zone and a resolved // destination. The flagship carries one A record per app host, all pointing // at the same Elastic IP — without grouping that layer is dozens of nodes // wide and unreadable. Grouping happens only in the diagram: every record // still appears individually, in full, in the DNS resource group below // (resourceGroups walks the inventory directly and never sees a group). type recordGroup struct { ids []string // record ids, sorted; ids[0] names the drawn node names []string // record display names, sorted, for the example list zone string // the raw "zone" attr this group's records share typ string // record type, meaningful only when len(names) == 1 value string // record value, meaningful only when len(names) == 1 target string // edge destination: "", "ip:…", "box:…"/"machine:…", "external:…" } // addRecordNodes groups the estate's DNS records by (zone, resolved // destination) and draws one node per group, sorted by each group's lowest // record id — never by map iteration, which Go randomises on every run and // this diagram must not. func addRecordNodes(v *archGraph, inv *infraview.Inventory, zones []infraview.Resource, claimed map[string]string) { groups := map[string]*recordGroup{} for _, rec := range inv.ByKind(infraview.KindDNSRecord) { zoneName := rec.Attr("zone") value := rec.Attr("value") target := recordTarget(inv, value, claimed) key := zoneName + "\x00" + target g := groups[key] if g == nil { g = &recordGroup{zone: zoneName, target: target} groups[key] = g } g.ids = append(g.ids, rec.ID) g.names = append(g.names, nameOf(rec)) g.typ, g.value = rec.Attr("type"), value } keys := make([]string, 0, len(groups)) for k, g := range groups { sort.Strings(g.ids) sort.Strings(g.names) keys = append(keys, k) } sort.Slice(keys, func(a, b int) bool { return groups[keys[a]].ids[0] < groups[keys[b]].ids[0] }) external := map[string]bool{} for _, k := range keys { g := groups[k] id := "record:" + g.ids[0] title, sub := recordNodeText(g) v.Nodes = append(v.Nodes, Node{ID: id, Kind: "record", Title: title, Subtitle: sub}) for _, z := range zones { if z.Name != "" && g.zone == z.Name { v.Edges = append(v.Edges, Edge{From: "zone:" + z.ID, To: id, Kind: "resolves"}) } } switch { case g.target == "": // An absent value is absence. Nothing is claimed about it. case strings.HasPrefix(g.target, "external:"): val := strings.TrimPrefix(g.target, "external:") if !external[val] { external[val] = true v.Nodes = append(v.Nodes, Node{ ID: g.target, Kind: "external", Title: val, Subtitle: "outside this estate", }) } v.Edges = append(v.Edges, Edge{From: id, To: g.target, Kind: "resolves"}) default: v.Edges = append(v.Edges, Edge{From: id, To: g.target, Kind: "resolves"}) } } } // recordTarget is where a DNS record's value actually points: an allocated // IP, a machine holding that address directly, or outside the estate // entirely. Pure — it only reads the inventory — so the grouping pass above // can call it once per record before deciding which records share a // destination. Find refuses to match on an empty value, so an absent value // falls straight through to the "" case. func recordTarget(inv *infraview.Inventory, value string, claimed map[string]string) string { if value == "" { return "" } if ip := inv.Find(infraview.KindIP, "address", value); ip != nil { return "ip:" + ip.ID } if m := inv.Find(infraview.KindMachine, "public_ip", value); m != nil { // An address the estate owns but holds directly on a machine rather // than through an allocated IP. return targetOf(m.ID, claimed) } return "external:" + value } // recordNodeText is the node's label. A single record keeps exactly the // shape it had before grouping existed: its own name, and its type and // value as the subtitle. A group of more than one names the count and up to // three example names, sorted, with the remainder folded into a "+N". func recordNodeText(g *recordGroup) (title, subtitle string) { if len(g.names) == 1 { return g.names[0], strings.TrimSpace(g.typ + " " + g.value) } examples := g.names if len(examples) > 3 { examples = examples[:3] } sub := strings.Join(examples, ", ") if rest := len(g.names) - len(examples); rest > 0 { sub += fmt.Sprintf(", +%d", rest) } return fmt.Sprintf("%d records", len(g.names)), sub } // addBoxNodes draws every box CARLOS reports — with or without a machine // behind it — carrying its apps and instance counts on the card itself, and // the deployment-bucket edge when the caller named a bucket. // // The counts are one line per box, never one per instance: a fleet of a // hundred tenants must still produce a diagram a person can read (spec // §6.4). func addBoxNodes(v *archGraph, boxes []BoxCard, in ArchInput) { for _, b := range boxes { boxID := "box:" + b.Label // The apps and the counts ride the box (Node.Extra) rather than // standing as an "apps" node beside it. They were never a hop a // request takes — they are what this box IS — and as a column they // cost the picture 280 units of width to tell six boxes out of nine // that they run nothing. v.Nodes = append(v.Nodes, Node{ ID: boxID, Kind: "box", Title: b.Label, Subtitle: boxSubtitle(b), Extra: []string{appsTitle(b), countsSubtitle(b)}, }) if in.Bucket != "" { v.Edges = append(v.Edges, Edge{From: boxID, To: "bucket:" + in.Bucket, Kind: "stores"}) } } if in.Bucket == "" { return } // The bucket node exists whether or not the estate read found it: the // console knows this bucket first-hand, and an un-granted S3 read must // not erase a relationship the console is certain of. id := "bucket:" + in.Bucket sub := "deployment bucket" if in.Inv != nil { for _, b := range in.Inv.ByKind(infraview.KindBucket) { if nameOf(b) == in.Bucket && b.Region != "" { sub = "deployment bucket · " + b.Region } } } v.Nodes = append(v.Nodes, Node{ID: id, Kind: "bucket", Title: in.Bucket, Subtitle: sub}) } // addMachineNodes draws the machines NO box reports. That is the whole // point: an instance nobody is running CARLOS on, or a box that has gone // dark, is drift — and drift is only visible if it is drawn. A machine a box // does report is already on the page as that box, and drawing it twice would // say the estate holds two things where it holds one. func addMachineNodes(v *archGraph, inv *infraview.Inventory, claimed map[string]string) { if inv == nil { return } for _, m := range inv.ByKind(infraview.KindMachine) { if claimed[m.ID] != "" { continue } sub := "no box reporting" if t := m.Attr("type"); t != "" { sub = "no box reporting · " + t } v.Nodes = append(v.Nodes, Node{ ID: "machine:" + m.ID, Kind: "machine", Title: firstNonEmpty(m.Name, m.ID), Subtitle: sub, }) } } // addAlarmBadges hangs each alarm on whatever it watches — the box when a // box reports that machine, the bare machine otherwise. An alarm naming // nothing this page draws is still listed in the resource groups, and one // naming something the page DOES draw becomes a mark on that node rather // than a node in the flow (see Node.Alarms for why). func addAlarmBadges(v *archGraph, inv *infraview.Inventory, claimed map[string]string) { if inv == nil { return } at := map[string]int{} for i := range v.Nodes { at[v.Nodes[i].ID] = i } for _, a := range inv.ByKind(infraview.KindAlarm) { to := targetOf(a.Attr("target"), claimed) if to == "" { continue } i, drawn := at[to] if !drawn { continue } state := a.Attr("state") v.Nodes[i].Alarms = append(v.Nodes[i].Alarms, NodeAlarm{ Name: a.Name, State: state, Firing: state == "ALARM", }) } } // ---- resource groups --------------------------------------------------- // archGroupOrder is the order the tables appear in, and archGroupOf maps a // kind onto one of them. A kind neither map knows lands in Other rather than // being dropped: infraview carries an unrecognised kind through verbatim // precisely so the page can show it. var archGroupOrder = []string{"Compute", "Network", "Storage", "DNS", "Identity", "Alarms", "Certificates", "Other"} var archGroupOf = map[string]string{ infraview.KindMachine: "Compute", infraview.KindDisk: "Compute", infraview.KindIP: "Network", infraview.KindFirewall: "Network", infraview.KindNetwork: "Network", infraview.KindBucket: "Storage", infraview.KindDNSZone: "DNS", infraview.KindDNSRecord: "DNS", infraview.KindIdentity: "Identity", infraview.KindAlarm: "Alarms", infraview.KindCertificate: "Certificates", infraview.KindOther: "Other", } // splitResourceGroups deals the inventory's tables into the two banks. The // exactly-once promise below is now a promise across BOTH banks together: // every resource is in one of them, never in neither and never in both, and // archestate_test.go counts it. func splitResourceGroups(inv *infraview.Inventory, carlos map[string]bool) (inEstate, rest []ResourceGroup) { if inv == nil { return nil, nil } var mine, theirs []infraview.Resource for _, r := range inv.Resources { if carlos[r.ID] { mine = append(mine, r) } else { theirs = append(theirs, r) } } return resourceGroups(mine), resourceGroups(theirs) } // resourceGroups turns a set of resources into the tables below one // diagram. EVERY resource appears exactly once, in inventory order, and an // empty group is omitted rather than rendered as a confident "none". func resourceGroups(resources []infraview.Resource) []ResourceGroup { rows := map[string][]ResourceRow{} for _, r := range resources { g, ok := archGroupOf[r.Kind] if !ok { g = "Other" } rows[g] = append(rows[g], ResourceRow{ Name: nameOf(r), NativeType: firstNonEmpty(r.NativeType, r.Kind), Region: r.Region, Detail: detailOf(r), }) } var out []ResourceGroup for _, title := range archGroupOrder { if len(rows[title]) == 0 { continue } out = append(out, ResourceGroup{Title: title, Rows: rows[title]}) } return out } // detailOf renders a resource's own attributes, sorted by key, so an // attribute nobody wrote code for still reaches the page. The id leads when // it says something the Name column does not — for an `other` resource that // is its ARN, which is the whole of what spec §6.3 promises about them. func detailOf(r infraview.Resource) string { keys := make([]string, 0, len(r.Attrs)) for k := range r.Attrs { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)+1) if r.ID != "" && r.ID != r.Name { parts = append(parts, "id="+r.ID) } for _, k := range keys { parts = append(parts, k+"="+r.Attrs[k]) } return strings.Join(parts, ", ") } // ---- small shared helpers ---------------------------------------------- // targetOf resolves a machine id to the node that stands for it: the box // when one reports it, the machine node otherwise. "" for an empty id — an // absent attribute is absence, and joining on it would attach every // unattached alarm to every machine with no id. func targetOf(machineID string, claimed map[string]string) string { if machineID == "" { return "" } if label := claimed[machineID]; label != "" { return "box:" + label } return "machine:" + machineID } func ipSubtitle(ip infraview.Resource, claimed map[string]string) string { to := ip.Attr("attached_to") if to == "" { return "attached to nothing" } if label := claimed[to]; label != "" { return label } return to } func boxSubtitle(b BoxCard) string { if b.CloudID == "" { return "bring-your-own box" } return strings.TrimSpace(b.InstanceType + " " + b.State) } func appsTitle(b BoxCard) string { if len(b.Apps) == 0 { return "no apps" } return strings.Join(b.Apps, ", ") } // countsSubtitle renders the box's instance counts. Two absences, both of // which must read as absence: a box the fleet read never described has NO // counts (three zeroes would state a read that never happened), and Awake // and Asleep are *int whose null means the socket table was unreadable. // "unknown", never 0, because 0 is a claim. // // The unknown form is deliberately short: this string is a subtitle inside a // 190-unit node rect, and the long form already overruns it. func countsSubtitle(b BoxCard) string { if !b.CountsKnown { return "counts unknown" } return fmt.Sprintf("static %d · resident %d · hibernating %d · awake %s · asleep %s", b.Static, b.Resident, b.Hibernating, countOrUnknown(b.Awake), countOrUnknown(b.Asleep)) } func countOrUnknown(n *int) string { if n == nil { return "unknown" } return strconv.Itoa(*n) } // nameOf is a resource's display name, falling back to its id — every // resource has an id, not every provider gives everything a name. func nameOf(r infraview.Resource) string { return firstNonEmpty(r.Name, r.ID) } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" }