package console import ( "strings" "testing" "github.com/carlosframework/platform/internal/economics" "github.com/carlosframework/platform/internal/infraview" "github.com/carlosframework/platform/internal/status" ) // sharedAccountFixture is the shape this whole feature exists for: one // account carrying a CARLOS deployment AND something else. It is modelled on // Tito's real account — a CARLOS box beside a Kubernetes estate, one DNS zone // answering for both, and a bucket named as if it were CARLOS's that the // console was never told about. func sharedAccountFixture() ArchInput { inv := &infraview.Inventory{Provider: "aws", Account: "665106592142", Resources: []infraview.Resource{ // One zone, both estates. This is the coupling that makes the two // halves one account rather than two. {Kind: infraview.KindDNSZone, ID: "Z1", Name: "tito.io", Attrs: map[string]string{"record_count": "202"}}, // --- the CARLOS half ------------------------------------------ {Kind: infraview.KindMachine, ID: "i-carlos", Name: "carlos-tito-ie", Region: "eu-west-1", Attrs: map[string]string{ "type": "t4g.medium", "state": "running", "public_ip": "52.0.0.1", "security_groups": "sg-carlos", "iam_profile": "arn:aws:iam::665106592142:instance-profile/carlos-tito-host-ie", }}, {Kind: infraview.KindIP, ID: "eip-carlos", Attrs: map[string]string{"address": "52.0.0.1", "attached_to": "i-carlos"}}, {Kind: infraview.KindDisk, ID: "vol-carlos", Attrs: map[string]string{"size_gb": "40", "attached_to": "i-carlos"}}, {Kind: infraview.KindFirewall, ID: "sg-carlos", Name: "carlos-tito-ie"}, {Kind: infraview.KindIdentity, ID: "arn:aws:iam::665106592142:role/carlos-tito-host-ie", Name: "carlos-tito-host-ie", Attrs: map[string]string{"kind": "role"}}, {Kind: infraview.KindAlarm, ID: "alarm-carlos", Name: "auto-recover-ie", Attrs: map[string]string{"target": "i-carlos", "state": "OK"}}, {Kind: infraview.KindDNSRecord, ID: "rec-carlos", Name: "go.tito.io", Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "52.0.0.1"}}, {Kind: infraview.KindBucket, ID: "carlos-tito-platform", Name: "carlos-tito-platform"}, // --- everything else the account carries ----------------------- {Kind: infraview.KindMachine, ID: "i-eks", Name: "workers-a-03", Region: "eu-west-1", Attrs: map[string]string{ "type": "m5.large", "state": "running", "public_ip": "52.0.0.9", "security_groups": "sg-eks", "iam_profile": "arn:aws:iam::665106592142:instance-profile/eks-2acb3b4e", }}, {Kind: infraview.KindFirewall, ID: "sg-eks", Name: "eks-cluster-sg"}, {Kind: infraview.KindIdentity, ID: "arn:aws:iam::665106592142:role/eks-node-role", Name: "eks-node-role", Attrs: map[string]string{"kind": "role"}}, {Kind: infraview.KindDNSRecord, ID: "rec-legacy", Name: "ti.to", Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "52.0.0.9"}}, {Kind: infraview.KindNetwork, ID: "vpc-1", Name: "tito-production"}, // A bucket the console was NEVER told about, wearing the CARLOS // name. It is the whole reason this file matches on joins rather // than on names: nothing here can prove it belongs to CARLOS. {Kind: infraview.KindBucket, ID: "carlos-tito-se-665106592142", Name: "carlos-tito-se-665106592142"}, }} return ArchInput{ Inv: inv, Bucket: "carlos-tito-platform", Fleet: &status.Fleet{Boxes: []status.FleetBox{{Label: "carlos-tito-ie", Region: "eu-west-1"}}}, Hosts: []HostRow{{Label: "carlos-tito-ie", Region: "eu-west-1", Metrics: &status.BoxMetrics{ Release: "abc1234", InstanceID: "i-carlos", }}}, Rates: economics.RateCard{Boxes: map[string]economics.BoxSpec{ "": {HourUSD: 0.02, Cores: 2, MemGB: 4, EBSGB: 40}, }, EBSGBMonthUSD: 0.088}, } } // The membership closure, one join at a time. Each of these is a hop the // console can PROVE; the failure this pins is a join quietly widening into a // name match, which would make every row on the page a guess. func TestEstateClosureReachesWhatItCanProve(t *testing.T) { v := BuildArch(sharedAccountFixture()) // Keyed by (table, name) rather than name alone: on the real estate a // security group is named after the box it guards, so "carlos-tito-ie" // is both a machine and a firewall — and the fixture keeps that. rows := bankOfEachRow(v) for _, c := range []struct{ table, name, want, why string }{ {"Compute", "carlos-tito-ie", "CARLOS", "the box itself reported this machine id"}, {"Compute", "vol-carlos", "CARLOS", "a disk attached to a reported machine"}, {"Network", "eip-carlos", "CARLOS", "an address attached to a reported machine"}, {"Alarms", "auto-recover-ie", "CARLOS", "an alarm watching a reported machine"}, {"Network", "carlos-tito-ie", "CARLOS", "a security group the machine itself named"}, {"Identity", "carlos-tito-host-ie", "CARLOS", "the role behind the machine's instance profile"}, {"DNS", "go.tito.io", "CARLOS", "a record resolving to a CARLOS address"}, {"DNS", "tito.io", "CARLOS", "the zone holding such a record"}, {"Storage", "carlos-tito-platform", "CARLOS", "the deployment bucket the console is configured with"}, {"Compute", "workers-a-03", "rest", "a machine no box reports"}, {"Network", "eks-cluster-sg", "rest", "a security group no CARLOS machine names"}, {"Identity", "eks-node-role", "rest", "a role no CARLOS instance profile names"}, {"DNS", "ti.to", "rest", "a record resolving into the other estate"}, {"Network", "tito-production", "rest", "a VPC nothing joins it to CARLOS"}, // The load-bearing one. This bucket is named exactly as CARLOS's // own buckets are, and it still lands in the other half, because // the console cannot trace a path to it. A page that put it in // CARLOS would be reading a naming convention, and the day // somebody renames a bucket that page becomes confidently wrong. {"Storage", "carlos-tito-se-665106592142", "rest", "a carlos-NAMED bucket the console cannot prove is its own"}, } { if got := rows[c.table+"/"+c.name]; got != c.want { t.Errorf("%s %s is in %q, want %q — %s", c.table, c.name, got, c.want, c.why) } } } // bankOfEachRow maps "/" to the bank its row was rendered in, // or "both"/"" when the split lost track of it. func bankOfEachRow(v ArchView) map[string]string { out := map[string]string{} for bank, groups := range map[string][]ResourceGroup{"CARLOS": v.CARLOS.Groups, "rest": v.Rest.Groups} { for _, g := range groups { for _, r := range g.Rows { k := g.Title + "/" + r.Name if out[k] != "" { out[k] = "both" continue } out[k] = bank } } } return out } // Nothing is filtered and nothing is hidden: this page GROUPS. Every // resource in the inventory appears exactly once across the two banks // together — never in neither (which would hide it) and never in both // (which would double-count the estate). func TestEveryResourceAppearsExactlyOnceAcrossBothBanks(t *testing.T) { in := sharedAccountFixture() v := BuildArch(in) count := map[string]int{} total := 0 for _, g := range append(append([]ResourceGroup{}, v.CARLOS.Groups...), v.Rest.Groups...) { for _, r := range g.Rows { count[g.Title+"/"+r.Name]++ total++ } } if total != len(in.Inv.Resources) { t.Errorf("banks carry %d rows, inventory has %d resources", total, len(in.Inv.Resources)) } for _, r := range in.Inv.Resources { k := archGroupOf[r.Kind] + "/" + nameOf(r) switch n := count[k]; n { case 1: case 0: t.Errorf("%s (%s) is on neither bank — the split must group, never hide", nameOf(r), r.Kind) default: t.Errorf("%s (%s) appears %d times — the estate must not be double-counted", nameOf(r), r.Kind, n) } } } // The split is only honest if no relationship is lost to it. An edge whose // ends fell into different banks would be pruned out of both and vanish // silently, so the shape of the graph has to be such that none exists: a // node that genuinely serves both halves (the shared DNS zone) is DRAWN IN // BOTH pictures rather than picked for one. func TestNoEdgeSpansTheTwoBanks(t *testing.T) { v := BuildArch(sharedAccountFixture()) bank := map[string]string{} for _, n := range v.CARLOS.Nodes() { bank[n.ID] = "CARLOS" } for _, n := range v.Rest.Nodes() { if bank[n.ID] == "CARLOS" { bank[n.ID] = "both" continue } bank[n.ID] = "rest" } for _, e := range archEdges(v) { f, to := bank[e.From], bank[e.To] if f == "both" || to == "both" || f == to { continue } t.Errorf("edge %s (%s) -> %s (%s) spans both banks and would be pruned out of both", e.From, f, e.To, to) } // The zone straddles both halves — it answers for records on either side // — and is drawn in NEITHER, because a zone belongs to no region and a // region band is what a picture is now (archregion.go). It reaches the // page as a table row, exactly once, on the side that can prove it. if got := bankOfEachRow(v)["DNS/tito.io"]; got != "CARLOS" { t.Errorf("the shared zone's table row is %q, want exactly one row, on the CARLOS side", got) } } // Each picture is deterministic on its own, and neither depends on the // other: layout runs twice over disjoint node sets and the second run must // not be able to move the first. func TestBothPicturesAreDeterministic(t *testing.T) { a := BuildArch(sharedAccountFixture()) b := BuildArch(sharedAccountFixture()) if renderBoth(a) != renderBoth(b) { t.Fatal("two builds of the same shared account produced different SVG") } if renderEstate(a.CARLOS) == renderEstate(a.Rest) { t.Fatal("fixture drift: the two pictures should differ, or this test proves nothing") } } // An account with nothing but CARLOS in it — the ordinary deployment — // draws an empty second picture and no orphan rows, rather than inventing a // second estate out of whatever it could not join. func TestAnAccountWithOnlyCARLOSHasAnEmptySecondBank(t *testing.T) { in := sharedAccountFixture() kept := in.Inv.Resources[:0] for _, r := range in.Inv.Resources { switch r.ID { case "i-eks", "sg-eks", "arn:aws:iam::665106592142:role/eks-node-role", "rec-legacy", "vpc-1", "carlos-tito-se-665106592142": continue } kept = append(kept, r) } in.Inv.Resources = kept v := BuildArch(in) for _, g := range v.Rest.Groups { t.Errorf("nothing should be in the second bank, found %s: %+v", g.Title, g.Rows) } for _, n := range v.Rest.Nodes() { t.Errorf("nothing should be drawn in the second picture, found %s", n.ID) } // An empty estate still draws ONE band, so the page has a picture where // one belongs rather than a gap, and that band still carries a positive // viewBox. if len(v.Rest.Bands) != 1 { t.Fatalf("an empty second estate should still draw one band, got %d", len(v.Rest.Bands)) } if b := v.Rest.Bands[0]; b.Width <= 0 || b.Height <= 0 { t.Errorf("an empty second picture must still carry a positive viewBox: %dx%d", b.Width, b.Height) } } // The one derived join on the page, pinned in both directions. An instance // profile's ARN and its role's ARN are matched on their trailing name; a // profile whose name matches no role must join nothing rather than reach for // the nearest thing. func TestInstanceProfileJoinMatchesOnlyItsOwnRole(t *testing.T) { for _, c := range []struct{ arn, want string }{ {"arn:aws:iam::1:role/carlos-tito-host-ie", "carlos-tito-host-ie"}, {"arn:aws:iam::1:instance-profile/carlos-tito-host-ie", "carlos-tito-host-ie"}, {"arn:aws:iam::1:role/service-role/nested", "nested"}, {"carlos-tito-host-ie", ""}, {"arn:aws:iam::1:role/", ""}, {"", ""}, } { if got := arnTail(c.arn); got != c.want { t.Errorf("arnTail(%q) = %q, want %q", c.arn, got, c.want) } } in := sharedAccountFixture() for i := range in.Inv.Resources { if in.Inv.Resources[i].ID == "i-carlos" { in.Inv.Resources[i].Attrs["iam_profile"] = "arn:aws:iam::665106592142:instance-profile/something-else" } } v := BuildArch(in) for _, g := range v.CARLOS.Groups { for _, r := range g.Rows { if strings.Contains(r.Name, "carlos-tito-host-ie") && g.Title == "Identity" { t.Error("a role whose name no CARLOS instance profile carries must not join") } } } }