carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/carlosframework/platform/internal/economics"
8 "github.com/carlosframework/platform/internal/infraview"
9 "github.com/carlosframework/platform/internal/status"
10)
11
12// sharedAccountFixture is the shape this whole feature exists for: one
13// account carrying a CARLOS deployment AND something else. It is modelled on
14// Tito's real account — a CARLOS box beside a Kubernetes estate, one DNS zone
15// answering for both, and a bucket named as if it were CARLOS's that the
16// console was never told about.
17func sharedAccountFixture() ArchInput {
18 inv := &infraview.Inventory{Provider: "aws", Account: "665106592142", Resources: []infraview.Resource{
19 // One zone, both estates. This is the coupling that makes the two
20 // halves one account rather than two.
21 {Kind: infraview.KindDNSZone, ID: "Z1", Name: "tito.io",
22 Attrs: map[string]string{"record_count": "202"}},
23
24 // --- the CARLOS half ------------------------------------------
25 {Kind: infraview.KindMachine, ID: "i-carlos", Name: "carlos-tito-ie", Region: "eu-west-1",
26 Attrs: map[string]string{
27 "type": "t4g.medium", "state": "running", "public_ip": "52.0.0.1",
28 "security_groups": "sg-carlos",
29 "iam_profile": "arn:aws:iam::665106592142:instance-profile/carlos-tito-host-ie",
30 }},
31 {Kind: infraview.KindIP, ID: "eip-carlos",
32 Attrs: map[string]string{"address": "52.0.0.1", "attached_to": "i-carlos"}},
33 {Kind: infraview.KindDisk, ID: "vol-carlos",
34 Attrs: map[string]string{"size_gb": "40", "attached_to": "i-carlos"}},
35 {Kind: infraview.KindFirewall, ID: "sg-carlos", Name: "carlos-tito-ie"},
36 {Kind: infraview.KindIdentity, ID: "arn:aws:iam::665106592142:role/carlos-tito-host-ie",
37 Name: "carlos-tito-host-ie", Attrs: map[string]string{"kind": "role"}},
38 {Kind: infraview.KindAlarm, ID: "alarm-carlos", Name: "auto-recover-ie",
39 Attrs: map[string]string{"target": "i-carlos", "state": "OK"}},
40 {Kind: infraview.KindDNSRecord, ID: "rec-carlos", Name: "go.tito.io",
41 Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "52.0.0.1"}},
42 {Kind: infraview.KindBucket, ID: "carlos-tito-platform", Name: "carlos-tito-platform"},
43
44 // --- everything else the account carries -----------------------
45 {Kind: infraview.KindMachine, ID: "i-eks", Name: "workers-a-03", Region: "eu-west-1",
46 Attrs: map[string]string{
47 "type": "m5.large", "state": "running", "public_ip": "52.0.0.9",
48 "security_groups": "sg-eks",
49 "iam_profile": "arn:aws:iam::665106592142:instance-profile/eks-2acb3b4e",
50 }},
51 {Kind: infraview.KindFirewall, ID: "sg-eks", Name: "eks-cluster-sg"},
52 {Kind: infraview.KindIdentity, ID: "arn:aws:iam::665106592142:role/eks-node-role",
53 Name: "eks-node-role", Attrs: map[string]string{"kind": "role"}},
54 {Kind: infraview.KindDNSRecord, ID: "rec-legacy", Name: "ti.to",
55 Attrs: map[string]string{"zone": "tito.io", "type": "A", "value": "52.0.0.9"}},
56 {Kind: infraview.KindNetwork, ID: "vpc-1", Name: "tito-production"},
57
58 // A bucket the console was NEVER told about, wearing the CARLOS
59 // name. It is the whole reason this file matches on joins rather
60 // than on names: nothing here can prove it belongs to CARLOS.
61 {Kind: infraview.KindBucket, ID: "carlos-tito-se-665106592142", Name: "carlos-tito-se-665106592142"},
62 }}
63 return ArchInput{
64 Inv: inv,
65 Bucket: "carlos-tito-platform",
66 Fleet: &status.Fleet{Boxes: []status.FleetBox{{Label: "carlos-tito-ie", Region: "eu-west-1"}}},
67 Hosts: []HostRow{{Label: "carlos-tito-ie", Region: "eu-west-1", Metrics: &status.BoxMetrics{
68 Release: "abc1234", InstanceID: "i-carlos",
69 }}},
70 Rates: economics.RateCard{Boxes: map[string]economics.BoxSpec{
71 "": {HourUSD: 0.02, Cores: 2, MemGB: 4, EBSGB: 40},
72 }, EBSGBMonthUSD: 0.088},
73 }
74}
75
76// The membership closure, one join at a time. Each of these is a hop the
77// console can PROVE; the failure this pins is a join quietly widening into a
78// name match, which would make every row on the page a guess.
79func TestEstateClosureReachesWhatItCanProve(t *testing.T) {
80 v := BuildArch(sharedAccountFixture())
81
82 // Keyed by (table, name) rather than name alone: on the real estate a
83 // security group is named after the box it guards, so "carlos-tito-ie"
84 // is both a machine and a firewall — and the fixture keeps that.
85 rows := bankOfEachRow(v)
86
87 for _, c := range []struct{ table, name, want, why string }{
88 {"Compute", "carlos-tito-ie", "CARLOS", "the box itself reported this machine id"},
89 {"Compute", "vol-carlos", "CARLOS", "a disk attached to a reported machine"},
90 {"Network", "eip-carlos", "CARLOS", "an address attached to a reported machine"},
91 {"Alarms", "auto-recover-ie", "CARLOS", "an alarm watching a reported machine"},
92 {"Network", "carlos-tito-ie", "CARLOS", "a security group the machine itself named"},
93 {"Identity", "carlos-tito-host-ie", "CARLOS", "the role behind the machine's instance profile"},
94 {"DNS", "go.tito.io", "CARLOS", "a record resolving to a CARLOS address"},
95 {"DNS", "tito.io", "CARLOS", "the zone holding such a record"},
96 {"Storage", "carlos-tito-platform", "CARLOS", "the deployment bucket the console is configured with"},
97
98 {"Compute", "workers-a-03", "rest", "a machine no box reports"},
99 {"Network", "eks-cluster-sg", "rest", "a security group no CARLOS machine names"},
100 {"Identity", "eks-node-role", "rest", "a role no CARLOS instance profile names"},
101 {"DNS", "ti.to", "rest", "a record resolving into the other estate"},
102 {"Network", "tito-production", "rest", "a VPC nothing joins it to CARLOS"},
103
104 // The load-bearing one. This bucket is named exactly as CARLOS's
105 // own buckets are, and it still lands in the other half, because
106 // the console cannot trace a path to it. A page that put it in
107 // CARLOS would be reading a naming convention, and the day
108 // somebody renames a bucket that page becomes confidently wrong.
109 {"Storage", "carlos-tito-se-665106592142", "rest", "a carlos-NAMED bucket the console cannot prove is its own"},
110 } {
111 if got := rows[c.table+"/"+c.name]; got != c.want {
112 t.Errorf("%s %s is in %q, want %q — %s", c.table, c.name, got, c.want, c.why)
113 }
114 }
115}
116
117// bankOfEachRow maps "<table>/<name>" to the bank its row was rendered in,
118// or "both"/"" when the split lost track of it.
119func bankOfEachRow(v ArchView) map[string]string {
120 out := map[string]string{}
121 for bank, groups := range map[string][]ResourceGroup{"CARLOS": v.CARLOS.Groups, "rest": v.Rest.Groups} {
122 for _, g := range groups {
123 for _, r := range g.Rows {
124 k := g.Title + "/" + r.Name
125 if out[k] != "" {
126 out[k] = "both"
127 continue
128 }
129 out[k] = bank
130 }
131 }
132 }
133 return out
134}
135
136// Nothing is filtered and nothing is hidden: this page GROUPS. Every
137// resource in the inventory appears exactly once across the two banks
138// together — never in neither (which would hide it) and never in both
139// (which would double-count the estate).
140func TestEveryResourceAppearsExactlyOnceAcrossBothBanks(t *testing.T) {
141 in := sharedAccountFixture()
142 v := BuildArch(in)
143
144 count := map[string]int{}
145 total := 0
146 for _, g := range append(append([]ResourceGroup{}, v.CARLOS.Groups...), v.Rest.Groups...) {
147 for _, r := range g.Rows {
148 count[g.Title+"/"+r.Name]++
149 total++
150 }
151 }
152 if total != len(in.Inv.Resources) {
153 t.Errorf("banks carry %d rows, inventory has %d resources", total, len(in.Inv.Resources))
154 }
155 for _, r := range in.Inv.Resources {
156 k := archGroupOf[r.Kind] + "/" + nameOf(r)
157 switch n := count[k]; n {
158 case 1:
159 case 0:
160 t.Errorf("%s (%s) is on neither bank — the split must group, never hide", nameOf(r), r.Kind)
161 default:
162 t.Errorf("%s (%s) appears %d times — the estate must not be double-counted", nameOf(r), r.Kind, n)
163 }
164 }
165}
166
167// The split is only honest if no relationship is lost to it. An edge whose
168// ends fell into different banks would be pruned out of both and vanish
169// silently, so the shape of the graph has to be such that none exists: a
170// node that genuinely serves both halves (the shared DNS zone) is DRAWN IN
171// BOTH pictures rather than picked for one.
172func TestNoEdgeSpansTheTwoBanks(t *testing.T) {
173 v := BuildArch(sharedAccountFixture())
174
175 bank := map[string]string{}
176 for _, n := range v.CARLOS.Nodes() {
177 bank[n.ID] = "CARLOS"
178 }
179 for _, n := range v.Rest.Nodes() {
180 if bank[n.ID] == "CARLOS" {
181 bank[n.ID] = "both"
182 continue
183 }
184 bank[n.ID] = "rest"
185 }
186 for _, e := range archEdges(v) {
187 f, to := bank[e.From], bank[e.To]
188 if f == "both" || to == "both" || f == to {
189 continue
190 }
191 t.Errorf("edge %s (%s) -> %s (%s) spans both banks and would be pruned out of both",
192 e.From, f, e.To, to)
193 }
194
195 // The zone straddles both halves — it answers for records on either side
196 // — and is drawn in NEITHER, because a zone belongs to no region and a
197 // region band is what a picture is now (archregion.go). It reaches the
198 // page as a table row, exactly once, on the side that can prove it.
199 if got := bankOfEachRow(v)["DNS/tito.io"]; got != "CARLOS" {
200 t.Errorf("the shared zone's table row is %q, want exactly one row, on the CARLOS side", got)
201 }
202}
203
204// Each picture is deterministic on its own, and neither depends on the
205// other: layout runs twice over disjoint node sets and the second run must
206// not be able to move the first.
207func TestBothPicturesAreDeterministic(t *testing.T) {
208 a := BuildArch(sharedAccountFixture())
209 b := BuildArch(sharedAccountFixture())
210 if renderBoth(a) != renderBoth(b) {
211 t.Fatal("two builds of the same shared account produced different SVG")
212 }
213 if renderEstate(a.CARLOS) == renderEstate(a.Rest) {
214 t.Fatal("fixture drift: the two pictures should differ, or this test proves nothing")
215 }
216}
217
218// An account with nothing but CARLOS in it — the ordinary deployment —
219// draws an empty second picture and no orphan rows, rather than inventing a
220// second estate out of whatever it could not join.
221func TestAnAccountWithOnlyCARLOSHasAnEmptySecondBank(t *testing.T) {
222 in := sharedAccountFixture()
223 kept := in.Inv.Resources[:0]
224 for _, r := range in.Inv.Resources {
225 switch r.ID {
226 case "i-eks", "sg-eks", "arn:aws:iam::665106592142:role/eks-node-role",
227 "rec-legacy", "vpc-1", "carlos-tito-se-665106592142":
228 continue
229 }
230 kept = append(kept, r)
231 }
232 in.Inv.Resources = kept
233
234 v := BuildArch(in)
235 for _, g := range v.Rest.Groups {
236 t.Errorf("nothing should be in the second bank, found %s: %+v", g.Title, g.Rows)
237 }
238 for _, n := range v.Rest.Nodes() {
239 t.Errorf("nothing should be drawn in the second picture, found %s", n.ID)
240 }
241 // An empty estate still draws ONE band, so the page has a picture where
242 // one belongs rather than a gap, and that band still carries a positive
243 // viewBox.
244 if len(v.Rest.Bands) != 1 {
245 t.Fatalf("an empty second estate should still draw one band, got %d", len(v.Rest.Bands))
246 }
247 if b := v.Rest.Bands[0]; b.Width <= 0 || b.Height <= 0 {
248 t.Errorf("an empty second picture must still carry a positive viewBox: %dx%d", b.Width, b.Height)
249 }
250}
251
252// The one derived join on the page, pinned in both directions. An instance
253// profile's ARN and its role's ARN are matched on their trailing name; a
254// profile whose name matches no role must join nothing rather than reach for
255// the nearest thing.
256func TestInstanceProfileJoinMatchesOnlyItsOwnRole(t *testing.T) {
257 for _, c := range []struct{ arn, want string }{
258 {"arn:aws:iam::1:role/carlos-tito-host-ie", "carlos-tito-host-ie"},
259 {"arn:aws:iam::1:instance-profile/carlos-tito-host-ie", "carlos-tito-host-ie"},
260 {"arn:aws:iam::1:role/service-role/nested", "nested"},
261 {"carlos-tito-host-ie", ""},
262 {"arn:aws:iam::1:role/", ""},
263 {"", ""},
264 } {
265 if got := arnTail(c.arn); got != c.want {
266 t.Errorf("arnTail(%q) = %q, want %q", c.arn, got, c.want)
267 }
268 }
269
270 in := sharedAccountFixture()
271 for i := range in.Inv.Resources {
272 if in.Inv.Resources[i].ID == "i-carlos" {
273 in.Inv.Resources[i].Attrs["iam_profile"] =
274 "arn:aws:iam::665106592142:instance-profile/something-else"
275 }
276 }
277 v := BuildArch(in)
278 for _, g := range v.CARLOS.Groups {
279 for _, r := range g.Rows {
280 if strings.Contains(r.Name, "carlos-tito-host-ie") && g.Title == "Identity" {
281 t.Error("a role whose name no CARLOS instance profile carries must not join")
282 }
283 }
284 }
285}
286