carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package domains
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strconv"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/registry"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// addInstance upserts a provisioned instance route the way the provision
16// pass mints one: Slug is the host's first label.
17func addInstance(t *testing.T, reg *registry.Registry, host, slug, account, app string) {
18 t.Helper()
19 if err := reg.Upsert(registry.Route{
20 Host: host, Slug: slug, App: app, Account: account, Kind: "instance", Status: "ready",
21 }); err != nil {
22 t.Fatal(err)
23 }
24 if err := reg.SetProvisioned(host, true); err != nil {
25 t.Fatal(err)
26 }
27}
28
29func aliasMap(t *testing.T, reg *registry.Registry) map[string]registry.RouteAlias {
30 t.Helper()
31 aliases, err := reg.ListAliases()
32 if err != nil {
33 t.Fatal(err)
34 }
35 out := map[string]registry.RouteAlias{}
36 for _, a := range aliases {
37 out[a.Alias] = a
38 }
39 return out
40}
41
42// jellyStore is the letsjelly.dev testbed in miniature: account bad holds
43// the apex through its static site, and dev-managed-jelly holds the
44// instance catch-all.
45func jellyStore(t *testing.T) release.Store {
46 t.Helper()
47 s := release.NewDirStore(t.TempDir())
48 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
49 putClaim(t, s, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "dev-managed-jelly", Instances: true})
50 return s
51}
52
53func reconcile(t *testing.T, s release.Store, reg *registry.Registry, deploymentRoutes map[string]bool) {
54 t.Helper()
55 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), deploymentRoutes, nil); err != nil {
56 t.Fatal(err)
57 }
58}
59
60func TestInstanceCatchallDerivesOneAliasPerInstance(t *testing.T) {
61 s := jellyStore(t)
62 reg := aliasRegWith(t)
63 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
64 addInstance(t, reg, "smoketest.bad.carlos.exciting.io", "smoketest", "bad", "dev-managed-jelly")
65 reconcile(t, s, reg, nil)
66 got := aliasMap(t, reg)
67 if len(got) != 2 {
68 t.Fatalf("got %d aliases, want 2: %+v", len(got), got)
69 }
70 a := got["acme.letsjelly.dev"]
71 if a.Host != "acme.bad.carlos.exciting.io" || a.Account != "bad" || a.App != "dev-managed-jelly" {
72 t.Fatalf("acme alias = %+v", a)
73 }
74 if got["smoketest.letsjelly.dev"].Host != "smoketest.bad.carlos.exciting.io" {
75 t.Fatalf("smoketest alias = %+v", got["smoketest.letsjelly.dev"])
76 }
77}
78
79func TestInstanceCatchallFollowsTheInstance(t *testing.T) {
80 s := jellyStore(t)
81 reg := aliasRegWith(t)
82 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
83 reconcile(t, s, reg, nil)
84 if _, ok := aliasMap(t, reg)["acme.letsjelly.dev"]; !ok {
85 t.Fatal("no alias for a live instance")
86 }
87 // The instance leaves the box (deleted, or moved elsewhere): its name
88 // leaves with it on the next pass.
89 if err := reg.Delete("acme.bad.carlos.exciting.io"); err != nil {
90 t.Fatal(err)
91 }
92 reconcile(t, s, reg, nil)
93 if got := aliasMap(t, reg); len(got) != 0 {
94 t.Fatalf("alias outlived its instance: %+v", got)
95 }
96}
97
98func TestInstanceCatchallDetachDropsEveryName(t *testing.T) {
99 s := jellyStore(t)
100 reg := aliasRegWith(t)
101 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
102 reconcile(t, s, reg, nil)
103 deleteClaim(t, s, "*.letsjelly.dev")
104 reconcile(t, s, reg, nil)
105 if got := aliasMap(t, reg); len(got) != 0 {
106 t.Fatalf("aliases outlived the catch-all: %+v", got)
107 }
108}
109
110// An exact claim on a derived name wins, whoever holds it: a same-account
111// one (the Jelly manager's per-team claims, www, auth) and another
112// account's alike.
113func TestInstanceCatchallExactClaimWins(t *testing.T) {
114 s := jellyStore(t)
115 putClaim(t, s, Claim{Host: "acme.letsjelly.dev", Account: "bad", App: "dev-managed-jelly", Route: "acme.bad.carlos.exciting.io"})
116 putClaim(t, s, Claim{Host: "beta.letsjelly.dev", Account: "zzz", App: "other", Route: "x.zzz.carlos.exciting.io"})
117 reg := aliasRegWith(t)
118 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
119 addInstance(t, reg, "beta.bad.carlos.exciting.io", "beta", "bad", "dev-managed-jelly")
120 reconcile(t, s, reg, nil)
121 got := aliasMap(t, reg)
122 // acme is still served — by its exact claim's row, which says the same.
123 if got["acme.letsjelly.dev"].Host != "acme.bad.carlos.exciting.io" {
124 t.Fatalf("acme = %+v", got["acme.letsjelly.dev"])
125 }
126 // beta's exact claim belongs to another account whose route is not on
127 // this box: no row here at all, and certainly not the derived one.
128 if a, ok := got["beta.letsjelly.dev"]; ok {
129 t.Fatalf("derived alias shadowed another account's exact claim: %+v", a)
130 }
131 // Removing the exact claim hands the name back to the catch-all.
132 deleteClaim(t, s, "acme.letsjelly.dev")
133 reconcile(t, s, reg, nil)
134 if a := aliasMap(t, reg)["acme.letsjelly.dev"]; a.Host != "acme.bad.carlos.exciting.io" {
135 t.Fatalf("after the exact claim went, acme = %+v", a)
136 }
137}
138
139func TestInstanceCatchallRoutesWin(t *testing.T) {
140 s := jellyStore(t)
141 reg := aliasRegWith(t)
142 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
143 addInstance(t, reg, "beta.bad.carlos.exciting.io", "beta", "bad", "dev-managed-jelly")
144 addRoute(t, reg, "acme.letsjelly.dev", "bad", "something")
145 reconcile(t, s, reg, map[string]bool{"beta.letsjelly.dev": true})
146 got := aliasMap(t, reg)
147 if len(got) != 0 {
148 t.Fatalf("a derived name shadowed a real route: %+v", got)
149 }
150}
151
152// The catch-all is honoured only while its parent is held by the same
153// account — released, or held by someone else, it derives nothing.
154func TestInstanceCatchallNeedsTheParentInItsAccount(t *testing.T) {
155 for name, apex := range map[string]*Claim{
156 "released": nil,
157 "other account": {Host: "letsjelly.dev", Account: "zzz", App: "squat"},
158 } {
159 t.Run(name, func(t *testing.T) {
160 s := release.NewDirStore(t.TempDir())
161 putClaim(t, s, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "dev-managed-jelly", Instances: true})
162 if apex != nil {
163 putClaim(t, s, *apex)
164 }
165 reg := aliasRegWith(t)
166 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
167 reconcile(t, s, reg, nil)
168 if got := aliasMap(t, reg); len(got) != 0 {
169 t.Fatalf("derived without the parent: %+v", got)
170 }
171 })
172 }
173}
174
175// Only the claim's own account and app, and only provisioned instance rows
176// whose Slug is the host's first label.
177func TestInstanceCatchallOnlyItsOwnInstances(t *testing.T) {
178 s := jellyStore(t)
179 reg := aliasRegWith(t)
180 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
181 // A sibling app in the same account.
182 addInstance(t, reg, "sib.bad.carlos.exciting.io", "sib", "bad", "dev-jelly-manager")
183 // Another account's app of the same name.
184 addInstance(t, reg, "evil.zzz.carlos.exciting.io", "evil", "zzz", "dev-managed-jelly")
185 // A hand-added route (not provisioned), with a slug.
186 if err := reg.Upsert(registry.Route{Host: "hand.bad.carlos.exciting.io", Slug: "hand", App: "dev-managed-jelly", Account: "bad", Kind: "instance", Status: "ready"}); err != nil {
187 t.Fatal(err)
188 }
189 // A replica row: Slug is the whole host (provision.Replica) ...
190 addInstance(t, reg, "acme-r.bad.carlos.exciting.io", "acme-r.bad.carlos.exciting.io", "bad", "dev-managed-jelly")
191 // ... or a label, when the create door wrote it; the mark excludes it.
192 addInstance(t, reg, "rep.bad.carlos.exciting.io", "rep", "bad", "dev-managed-jelly")
193 if err := reg.SetReplica("rep.bad.carlos.exciting.io", true); err != nil {
194 t.Fatal(err)
195 }
196 // A sidecar of the app.
197 if err := reg.Upsert(registry.Route{Host: "side.sidecar.internal", Slug: "side", App: "dev-managed-jelly", Account: "bad", Kind: "sidecar", Status: "ready"}); err != nil {
198 t.Fatal(err)
199 }
200 // A slug that is not the host's first label.
201 addInstance(t, reg, "odd.bad.carlos.exciting.io", "other", "bad", "dev-managed-jelly")
202 reconcile(t, s, reg, nil)
203 got := aliasMap(t, reg)
204 if len(got) != 1 || got["acme.letsjelly.dev"].Host != "acme.bad.carlos.exciting.io" {
205 t.Fatalf("got %+v, want only acme.letsjelly.dev", got)
206 }
207}
208
209// A catch-all carrying fields no door writes on one is not honoured.
210func TestInstanceCatchallRefusesHandWrittenShapes(t *testing.T) {
211 for name, c := range map[string]Claim{
212 "route": {Route: "acme.bad.carlos.exciting.io"},
213 "wildcard": {Wildcard: true},
214 "paths": {Paths: map[string]string{"x": "acme.bad.carlos.exciting.io"}},
215 } {
216 t.Run(name, func(t *testing.T) {
217 s := release.NewDirStore(t.TempDir())
218 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
219 c.Host, c.Account, c.App, c.Instances = "*.letsjelly.dev", "bad", "dev-managed-jelly", true
220 putClaim(t, s, c)
221 reg := aliasRegWith(t)
222 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
223 reconcile(t, s, reg, nil)
224 for alias := range aliasMap(t, reg) {
225 if alias == "acme.letsjelly.dev" {
226 t.Fatalf("derived from a hand-written catch-all (%s)", name)
227 }
228 }
229 })
230 }
231}
232
233// The catch-all sits beside the apex's own one-route catch-all: the exact
234// derived names win at the edge (an exact key beats "*."), and neither pass
235// removes the other's row.
236func TestInstanceCatchallBesideARouteCatchall(t *testing.T) {
237 s := release.NewDirStore(t.TempDir())
238 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site", Wildcard: true, Catchall: true})
239 putClaim(t, s, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "dev-managed-jelly", Instances: true})
240 reg := aliasRegWith(t)
241 if err := reg.Upsert(registry.Route{Host: "letsjelly.dev", App: "dev-jelly-site", Account: "bad", Kind: "static", Status: "ready"}); err != nil {
242 t.Fatal(err)
243 }
244 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
245 reconcile(t, s, reg, nil)
246 got := aliasMap(t, reg)
247 if got["*.letsjelly.dev"].Host != "letsjelly.dev" || got["acme.letsjelly.dev"].Host != "acme.bad.carlos.exciting.io" {
248 t.Fatalf("got %+v", got)
249 }
250}
251
252func TestParseInstanceCatchallHost(t *testing.T) {
253 for raw, want := range map[string]string{
254 "*.letsjelly.dev": "letsjelly.dev",
255 "*.LetsJelly.dev.": "letsjelly.dev",
256 "*.a.b.example.com": "a.b.example.com",
257 "*.dev": "",
258 "letsjelly.dev": "",
259 "*letsjelly.dev": "",
260 "*.*.letsjelly.dev": "",
261 "x.*.letsjelly.dev": "",
262 "*.": "",
263 } {
264 got, ok := ParseInstanceCatchallHost(raw)
265 if ok != (want != "") || got != want {
266 t.Errorf("ParseInstanceCatchallHost(%q) = %q, %v; want %q", raw, got, ok, want)
267 }
268 }
269}
270
271func TestAttachInstanceCatchall(t *testing.T) {
272 ctx := context.Background()
273 s := release.NewDirStore(t.TempDir())
274 if _, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-managed-jelly", "t", "j"); !errors.Is(err, ErrParentNotHeld) {
275 t.Fatalf("unclaimed parent: err = %v, want ErrParentNotHeld", err)
276 }
277 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "zzz", App: "squat"})
278 if _, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-managed-jelly", "t", "j"); !errors.Is(err, ErrParentNotHeld) {
279 t.Fatalf("parent held elsewhere: err = %v, want ErrParentNotHeld", err)
280 }
281 deleteClaim(t, s, "letsjelly.dev")
282 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
283 created, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-managed-jelly", "t", "j")
284 if err != nil || !created {
285 t.Fatalf("first attach: created=%v err=%v", created, err)
286 }
287 c, err := GetClaim(ctx, s, "*.letsjelly.dev")
288 if err != nil || !c.Instances || c.Account != "bad" || c.App != "dev-managed-jelly" || c.Route != "" || c.Wildcard || c.Catchall {
289 t.Fatalf("stored = %+v, %v", c, err)
290 }
291 created, err = AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-managed-jelly", "t", "j")
292 if err != nil || created {
293 t.Fatalf("re-attach: created=%v err=%v, want idempotent", created, err)
294 }
295 // Another app of the same account does not take it over.
296 if _, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-jelly-site", "t", "j"); !errors.Is(err, ErrHostUnavailable) {
297 t.Fatalf("sibling app: err = %v, want ErrHostUnavailable", err)
298 }
299 // Capability and path writers refuse a catch-all even to its owner.
300 if _, err := SetClaimCapabilities(ctx, s, "*.letsjelly.dev", "bad", "dev-managed-jelly", true, false); !errors.Is(err, ErrHostUnavailable) {
301 t.Fatalf("capabilities on a catch-all: err = %v", err)
302 }
303 if _, err := SetClaimPaths(ctx, s, "*.letsjelly.dev", "bad", "dev-managed-jelly", map[string]string{"x": "a.b.c"}); !errors.Is(err, ErrHostUnavailable) {
304 t.Fatalf("paths on a catch-all: err = %v", err)
305 }
306 // The parent changes hands: the old catch-all is inert, and the new
307 // holder replaces it instead of meeting a squat.
308 deleteClaim(t, s, "letsjelly.dev")
309 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "zzz", App: "new"})
310 if created, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "zzz", "teams", "t", "j"); err != nil || !created {
311 t.Fatalf("new parent holder: created=%v err=%v", created, err)
312 }
313 if c, _ := GetClaim(ctx, s, "*.letsjelly.dev"); c.Account != "zzz" || c.App != "teams" || !c.Instances {
314 t.Fatalf("after replacement: %+v", c)
315 }
316 deleteClaim(t, s, "*.letsjelly.dev")
317 deleteClaim(t, s, "letsjelly.dev")
318 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
319 if _, err := AttachInstanceCatchall(ctx, s, "letsjelly.dev", "bad", "dev-managed-jelly", "t", "j"); err != nil {
320 t.Fatal(err)
321 }
322 // Detach is the ordinary owner-gated delete.
323 if removed, err := DetachClaim(ctx, s, "*.letsjelly.dev", "bad", "dev-jelly-site"); err != nil || removed {
324 t.Fatalf("sibling detach: removed=%v err=%v", removed, err)
325 }
326 if removed, err := DetachClaim(ctx, s, "*.letsjelly.dev", "bad", "dev-managed-jelly"); err != nil || !removed {
327 t.Fatalf("owner detach: removed=%v err=%v", removed, err)
328 }
329}
330
331func TestForeignInstanceCatchall(t *testing.T) {
332 ctx := context.Background()
333 s := jellyStore(t)
334 for host, want := range map[string]map[string]bool{
335 "acme.letsjelly.dev": {"bad": false, "zzz": true},
336 "x.acme.letsjelly.dev": {"bad": false, "zzz": false}, // deeper: not the catch-all's
337 "letsjelly.dev": {"bad": false, "zzz": false}, // the parent itself
338 "acme.example.com": {"bad": false, "zzz": false},
339 } {
340 for acct, w := range want {
341 c, err := ForeignInstanceCatchall(ctx, s, host, acct)
342 if got := c != nil; err != nil || got != w {
343 t.Errorf("ForeignInstanceCatchall(%s, %s) = %+v, %v; want %v", host, acct, c, err, w)
344 }
345 }
346 }
347 // Released parent: the catch-all serves nothing, so it holds nothing.
348 deleteClaim(t, s, "letsjelly.dev")
349 if c, err := ForeignInstanceCatchall(ctx, s, "acme.letsjelly.dev", "zzz"); err != nil || c != nil {
350 t.Errorf("after the parent went: %+v, %v; want nil", c, err)
351 }
352}
353
354// What a binary that predates Instances makes of a catch-all claim: the
355// shape it decodes (no Route, no Wildcard, no Paths) is one every pass
356// already skips. Simulated by clearing the field this binary knows.
357func TestInstanceCatchallIsInertWithoutTheField(t *testing.T) {
358 s := release.NewDirStore(t.TempDir())
359 putClaim(t, s, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
360 putClaim(t, s, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "dev-managed-jelly"})
361 reg := aliasRegWith(t)
362 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
363 reconcile(t, s, reg, nil)
364 if got := aliasMap(t, reg); len(got) != 0 {
365 t.Fatalf("got %+v", got)
366 }
367 wds, err := reg.ListWildcardDomains()
368 if err != nil || len(wds) != 0 {
369 t.Fatalf("wildcard domains %+v, %v", wds, err)
370 }
371}
372
373// The per-box cap holds, deterministically: the first names in host order
374// are kept, and nothing past the cap is written.
375func TestInstanceCatchallCap(t *testing.T) {
376 s := jellyStore(t)
377 reg := aliasRegWith(t)
378 for i := 0; i <= maxInstanceAliases; i++ {
379 label := "t" + strconv.Itoa(10000+i)
380 addInstance(t, reg, label+".bad.carlos.exciting.io", label, "bad", "dev-managed-jelly")
381 }
382 reconcile(t, s, reg, nil)
383 got := aliasMap(t, reg)
384 if len(got) != maxInstanceAliases {
385 t.Fatalf("got %d aliases, want the cap %d", len(got), maxInstanceAliases)
386 }
387 if _, ok := got["t"+strconv.Itoa(10000+maxInstanceAliases)+".letsjelly.dev"]; ok {
388 t.Fatal("the last name in host order was kept past the cap")
389 }
390}
391
392// unreadableKeyStore fails every read of one key, as a spurious store error would,
393// while the key still shows in listings.
394type unreadableKeyStore struct {
395 *release.DirStore
396 failKey string
397}
398
399func (f unreadableKeyStore) Get(ctx context.Context, key string) ([]byte, string, error) {
400 if key == f.failKey {
401 return nil, "", errors.New("synthetic 403")
402 }
403 return f.DirStore.Get(ctx, key)
404}
405
406// A claim whose body cannot be read this pass still beats the derived
407// name: precedence is decided from the listing, never from what decoded.
408func TestInstanceCatchallUnreadableClaimStillWins(t *testing.T) {
409 dir := release.NewDirStore(t.TempDir())
410 putClaim(t, dir, Claim{Host: "letsjelly.dev", Account: "bad", App: "dev-jelly-site"})
411 putClaim(t, dir, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "dev-managed-jelly", Instances: true})
412 putClaim(t, dir, Claim{Host: "acme.letsjelly.dev", Account: "zzz", App: "other", Route: "x.zzz.carlos.exciting.io"})
413 s := unreadableKeyStore{dir, ClaimKey("acme.letsjelly.dev")}
414 reg := aliasRegWith(t)
415 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
416 for _, cache := range []*ClaimCache{nil, {}} {
417 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, cache); err != nil {
418 t.Fatal(err)
419 }
420 if a, ok := aliasMap(t, reg)["acme.letsjelly.dev"]; ok {
421 t.Fatalf("derived over an unreadable claim (cache %v): %+v", cache != nil, a)
422 }
423 }
424}
425
426// replacingStore hands back the claim at key as it stood, then lets another
427// writer replace it before the caller acts — the old owner's detach racing
428// the parent's new holder.
429type replacingStore struct {
430 *release.DirStore
431 key string
432 next []byte
433 done bool
434}
435
436func (r *replacingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
437 body, etag, err := r.DirStore.Get(ctx, key)
438 if key == r.key && !r.done && err == nil {
439 r.done = true
440 if _, perr := r.DirStore.Put(ctx, key, r.next, etag); perr != nil {
441 return nil, "", perr
442 }
443 }
444 return body, etag, err
445}
446
447func TestDetachCatchallNeverDeletesAReplacement(t *testing.T) {
448 ctx := context.Background()
449 dir := release.NewDirStore(t.TempDir())
450 putClaim(t, dir, Claim{Host: "*.letsjelly.dev", Account: "bad", App: "old", Instances: true})
451 next, _ := json.Marshal(Claim{Schema: 1, Host: "*.letsjelly.dev", Account: "zzz", App: "new", Instances: true})
452 s := &replacingStore{DirStore: dir, key: ClaimKey("*.letsjelly.dev"), next: next}
453 removed, err := DetachClaim(ctx, s, "*.letsjelly.dev", "bad", "old")
454 if err != nil || removed {
455 t.Fatalf("detach over a replacement: removed=%v err=%v, want false and no error", removed, err)
456 }
457 c, err := GetClaim(ctx, dir, "*.letsjelly.dev")
458 if err != nil || c.Account != "zzz" || c.App != "new" {
459 t.Fatalf("the replacement was not kept: %+v, %v", c, err)
460 }
461}
462
463// putRecordKey drops an (empty) instance record at its key: the derivation's
464// duplicate check reads record KEYS only.
465func putRecordKey(t *testing.T, s release.Store, account, app, host string) {
466 t.Helper()
467 if _, err := s.Put(context.Background(), instanceRecordsPrefix(account, app)+host+".json", []byte(`{}`), ""); err != nil {
468 t.Fatal(err)
469 }
470}
471
472// A label two instances share (after an instance-domain change) derives
473// nothing, on every box alike — here, the box holding only one of them.
474func TestInstanceCatchallRefusesASharedLabel(t *testing.T) {
475 s := jellyStore(t)
476 putRecordKey(t, s, "bad", "dev-managed-jelly", "acme.bad.carlos.exciting.io")
477 putRecordKey(t, s, "bad", "dev-managed-jelly", "acme.teams.example.net")
478 putRecordKey(t, s, "bad", "dev-managed-jelly", "beta.bad.carlos.exciting.io")
479 reg := aliasRegWith(t)
480 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
481 addInstance(t, reg, "beta.bad.carlos.exciting.io", "beta", "bad", "dev-managed-jelly")
482 reconcile(t, s, reg, nil)
483 got := aliasMap(t, reg)
484 if _, ok := got["acme.letsjelly.dev"]; ok {
485 t.Fatalf("a shared label derived a name: %+v", got)
486 }
487 if got["beta.letsjelly.dev"].Host != "beta.bad.carlos.exciting.io" {
488 t.Fatalf("an unshared label lost its name: %+v", got)
489 }
490}
491
492// listFailStore fails listings under one prefix, and nothing else.
493type listFailStore struct {
494 *release.DirStore
495 prefix string
496}
497
498func (f listFailStore) List(ctx context.Context, prefix string) ([]string, error) {
499 if strings.HasPrefix(prefix, f.prefix) {
500 return nil, errors.New("synthetic list failure")
501 }
502 return f.DirStore.List(ctx, prefix)
503}
504
505// When the app's instances cannot be listed, the catch-all's names stay as
506// they were for the pass: a blip must not take every team's name down.
507func TestInstanceCatchallKeepsNamesWhenTheListingFails(t *testing.T) {
508 dir := jellyStore(t).(*release.DirStore)
509 reg := aliasRegWith(t)
510 addInstance(t, reg, "acme.bad.carlos.exciting.io", "acme", "bad", "dev-managed-jelly")
511 reconcile(t, dir, reg, nil)
512 if _, ok := aliasMap(t, reg)["acme.letsjelly.dev"]; !ok {
513 t.Fatal("precondition: the name derives")
514 }
515 failing := listFailStore{dir, instanceRecordsPrefix("bad", "dev-managed-jelly")}
516 if err := ReconcileAliases(context.Background(), failing, reg, aliasNow(), nil, nil); err != nil {
517 t.Fatal(err)
518 }
519 if a, ok := aliasMap(t, reg)["acme.letsjelly.dev"]; !ok || a.Host != "acme.bad.carlos.exciting.io" {
520 t.Fatalf("the name was dropped on a listing failure: %+v", aliasMap(t, reg))
521 }
522}
523
524func TestInstanceLabelCounts(t *testing.T) {
525 s := release.NewDirStore(t.TempDir())
526 putRecordKey(t, s, "bad", "app", "acme.bad.carlos.exciting.io")
527 putRecordKey(t, s, "bad", "app", "acme.old.example.net")
528 putRecordKey(t, s, "bad", "app", "beta.bad.carlos.exciting.io")
529 putRecordKey(t, s, "bad", "other", "gamma.bad.carlos.exciting.io")
530 got, err := InstanceLabelCounts(context.Background(), s, "bad", "app")
531 if err != nil || got["acme"] != 2 || got["beta"] != 1 || got["gamma"] != 0 {
532 t.Fatalf("counts = %v, %v", got, err)
533 }
534}
535