Enforce the reserved sidecar suffix at every route-minting door
6 files changed,
+377
−2
cli.go+53 −0cli_test.go+172 −0internal/console/domainattach.go+10 −0internal/console/domainattach_test.go+19 −0internal/domains/aliases.go+33 −2internal/domains/aliases_test.go+90 −0
diff --git a/cli.go b/cli.go| index 102ffb7df..186c87d30 100644 |
| --- a/cli.go |
| +++ b/cli.go |
| @@ -86,6 +86,9 @@ func runAdd(args []string) { |
| log.Fatal("usage: carlos add [flags] <host>") |
| } |
| host := fs.Arg(0) |
| + if err := addRail(host, *kind); err != nil { |
| + log.Fatalf("add %s: %v", host, err) |
| + } |
| if reservedSlugs()[*slug] { |
| log.Fatalf("slug %q is reserved", *slug) |
| } |
| @@ -655,6 +658,9 @@ func runRoute(args []string) { |
| if rt == nil { |
| log.Fatalf("route: no route for host %q", *host) |
| } |
| + if err := suffixRail(*rt); err != nil { |
| + log.Fatalf("route: %v", err) |
| + } |
| switch { |
| case grantAxis: |
| runRouteGrant(r, *rt, *grant, *revoke, *dryRun) |
| @@ -677,6 +683,9 @@ func runRoute(args []string) { |
| if rt == nil { |
| log.Fatalf("route: no route for host %q", *host) |
| } |
| + if err := suffixRail(*rt); err != nil { |
| + log.Fatalf("route: %v", err) |
| + } |
| candidates = []registry.Route{*rt} |
| } else { |
| routes, err := r.List() |
| @@ -712,6 +721,15 @@ func runRoute(args []string) { |
| warnf(os.Stdout, "%s: skipped — platform-minted route; use -host %s to change it explicitly\n", rt.Host, rt.Host) |
| continue |
| } |
| + if err := suffixRail(rt); err != nil { |
| + // A real provisioned sidecar's App equals its owning app's, so |
| + // this is not hypothetical: an -app sweep legitimately meets |
| + // these rows. Skipped like a Platform route, not fatal — one |
| + // row under the reserved suffix must not abort the rest of |
| + // the batch. |
| + warnf(os.Stdout, "%s: skipped — %v; use -host %s to see the refusal explicitly\n", rt.Host, err, rt.Host) |
| + continue |
| + } |
| candidates = append(candidates, rt) |
| } |
| if len(candidates) == 0 { |
| @@ -1175,6 +1193,41 @@ func capsLabel(col string) string { |
| return col |
| } |
| +// addRail refuses the two things `carlos add` must never mint: a host under |
| +// the reserved sidecar suffix, and a sidecar row itself. Sidecars are |
| +// provisioned through their own record family (carlos sidecars create), so |
| +// an operator minting one by hand would produce a row no reconcile pass |
| +// owns and no record explains. Defence in depth behind Task 3's structural |
| +// refusal to SERVE a Kind=="sidecar" row (edge.go's Reload): this rail |
| +// guards the OTHER direction — an ordinary-kind row planted under a |
| +// sidecar's reserved name, which edge would serve without a second thought. |
| +func addRail(host, kind string) error { |
| + if strings.HasSuffix(host, provision.SidecarSuffix) { |
| + return fmt.Errorf("%s is under the reserved suffix %s — sidecar hosts are minted by `carlos sidecars create`, never by hand", host, provision.SidecarSuffix) |
| + } |
| + if kind == "sidecar" { |
| + return errors.New("carlos add does not mint sidecars — use `carlos sidecars create -app <app> <name>`") |
| + } |
| + return nil |
| +} |
| + |
| +// suffixRail is `carlos route`'s side of the same guarantee addRail closes |
| +// at `carlos add`: this verb cannot MINT a row under the reserved sidecar |
| +// suffix (r.Get returns nil for one, and every axis below refuses before |
| +// opening the registry for writing) — but it CAN keep an existing one alive, |
| +// repoint its channel, or grant it capabilities, and that is the door this |
| +// rail closes. It applies to every axis this verb writes (-channel, |
| +// -environment, -tunnel, -grant/-revoke, -hibernate, -backing, -unit): the |
| +// row it guards against is not a genuine Kind=="sidecar" row (edge.go's |
| +// Reload never serves those regardless) but an ordinary-kind row wearing a |
| +// sidecar's reserved name, which this verb must never keep converging. |
| +func suffixRail(rt registry.Route) error { |
| + if strings.HasSuffix(rt.Host, provision.SidecarSuffix) { |
| + return fmt.Errorf("%s is under the reserved suffix %s — this verb does not touch routes under it; sidecar hosts are owned by their own record family, never this one", rt.Host, provision.SidecarSuffix) |
| + } |
| + return nil |
| +} |
| + |
| // grantRail is everything a -grant/-revoke can be refused for from the ROW |
| // alone: the token's own validity, plus the two structural refusals. Pure, so |
| // every refusal is table-testable without a registry. |
diff --git a/cli_test.go b/cli_test.go| index b86b37ef7..0a06d4439 100644 |
| --- a/cli_test.go |
| +++ b/cli_test.go |
| @@ -15,6 +15,7 @@ import ( |
| "testing" |
| "github.com/carlosframework/platform/internal/caps" |
| + "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -1049,3 +1050,174 @@ func TestNotesFileAndNotesConflict(t *testing.T) { |
| t.Fatalf("refusal does not name -notes-file:\n%s", out) |
| } |
| } |
| + |
| +// --- Task 4: the reserved sidecar suffix, enforced at every minting door --- |
| +// |
| +// Task 3 already refuses to SERVE a Kind=="sidecar" row structurally |
| +// (edge.go's Reload skips it at the top of the loop). These rails are |
| +// defence in depth behind that guarantee, and they close the OTHER |
| +// direction: a row of a DIFFERENT kind minted (by hand, or before this |
| +// suffix was reserved) with a Host under provision.SidecarSuffix, which |
| +// would be served — undermining the very reservation Task 3 depends on. |
| + |
| +// TestAddRefusesSidecarSuffixAndKind pins addRail's predicate exactly as the |
| +// task-4 brief specifies it. |
| +func TestAddRefusesSidecarSuffixAndKind(t *testing.T) { |
| + if err := addRail("bot.eleven.bdm.sidecar.internal", "instance"); err == nil { |
| + t.Fatal("a host under the reserved suffix must be refused") |
| + } |
| + if err := addRail("app.example.com", "sidecar"); err == nil { |
| + t.Fatal("carlos add must not mint a sidecar row — sidecars are provisioned, not added") |
| + } |
| + if err := addRail("app.example.com", "instance"); err != nil { |
| + t.Fatalf("an ordinary add must still work: %v", err) |
| + } |
| +} |
| + |
| +// TestAddRefusesSidecarSuffixWiredIntoRunAdd drives `carlos add` as a |
| +// subprocess: addRail's own test proves the predicate, this proves runAdd |
| +// actually calls it, before any registry write. |
| +func TestAddRefusesSidecarSuffixWiredIntoRunAdd(t *testing.T) { |
| + bin := buildCarlos(t) |
| + tmp := t.TempDir() |
| + scratch := t.TempDir() |
| + regPath := filepath.Join(tmp, "registry.db") |
| + |
| + run := func(args ...string) (string, error) { |
| + t.Helper() |
| + cmd := exec.Command(bin, args...) |
| + cmd.Env = carlosTestEnv(t) |
| + cmd.Dir = scratch |
| + out, err := cmd.CombinedOutput() |
| + return string(out), err |
| + } |
| + |
| + host := "bot.eleven.bdm" + provision.SidecarSuffix |
| + out, err := run("add", "-registry", regPath, "-app", "eleven", host) |
| + if err == nil { |
| + t.Fatalf("carlos add %s: want non-zero exit, got 0\n%s", host, out) |
| + } |
| + if !strings.Contains(out, provision.SidecarSuffix) { |
| + t.Fatalf("refusal does not name the reserved suffix:\n%s", out) |
| + } |
| + reg, rerr := registry.Open(regPath) |
| + if rerr != nil { |
| + t.Fatalf("open registry: %v", rerr) |
| + } |
| + defer reg.Close() |
| + if rt, gerr := reg.Get(host); gerr != nil || rt != nil { |
| + t.Fatalf("a refused add still wrote a row: %+v, %v", rt, gerr) |
| + } |
| + |
| + out, err = run("add", "-registry", regPath, "-app", "eleven", "-kind", "sidecar", "app.example.com") |
| + if err == nil { |
| + t.Fatalf("carlos add -kind sidecar: want non-zero exit, got 0\n%s", out) |
| + } |
| + if !strings.Contains(out, "sidecar") { |
| + t.Fatalf("refusal does not mention sidecar:\n%s", out) |
| + } |
| + if rt, gerr := reg.Get("app.example.com"); gerr != nil || rt != nil { |
| + t.Fatalf("a refused -kind sidecar add still wrote a row: %+v, %v", rt, gerr) |
| + } |
| +} |
| + |
| +// TestSuffixRailRefusesReservedSuffix: `carlos route` cannot MINT a row |
| +// under the reserved suffix (r.Get returns nil and the verb refuses before |
| +// any rail runs) — but it CAN keep an existing one alive or grant it |
| +// capabilities, which is the door this rail closes. Pure and table-tested |
| +// for the same reason grantRail/backingRail are: a rail nobody can exercise |
| +// without running the whole process is a rail nobody tests. |
| +func TestSuffixRailRefusesReservedSuffix(t *testing.T) { |
| + under := registry.Route{Host: "bot.eleven.bdm" + provision.SidecarSuffix, Kind: "instance", App: "eleven", Account: "bdm"} |
| + err := suffixRail(under) |
| + if err == nil { |
| + t.Fatalf("suffixRail(%+v) accepted; want a refusal", under) |
| + } |
| + if !strings.Contains(err.Error(), provision.SidecarSuffix) { |
| + t.Fatalf("refusal %q does not name the reserved suffix", err) |
| + } |
| + ordinary := registry.Route{Host: "app.example.com", Kind: "instance", App: "eleven", Account: "bdm"} |
| + if err := suffixRail(ordinary); err != nil { |
| + t.Fatalf("suffixRail(%+v) = %v, want accepted", ordinary, err) |
| + } |
| +} |
| + |
| +// TestRouteRefusesReservedSuffixWiredIn drives `carlos route` as a |
| +// subprocess against a row planted directly in the registry — the only way |
| +// such a row can exist post-Task-4, since `carlos add` now refuses to mint |
| +// one — proving suffixRail is wired into every axis this verb writes, not |
| +// only provable in isolation. |
| +func TestRouteRefusesReservedSuffixWiredIn(t *testing.T) { |
| + bin := buildCarlos(t) |
| + tmp := t.TempDir() |
| + scratch := t.TempDir() |
| + regPath := filepath.Join(tmp, "registry.db") |
| + host := "bot.eleven.bdm" + provision.SidecarSuffix |
| + |
| + reg, err := registry.Open(regPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.Upsert(registry.Route{ |
| + Host: host, Kind: "instance", App: "eleven", Account: "bdm", Status: "ready", |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + reg.Close() |
| + |
| + run := func(args ...string) (string, error) { |
| + t.Helper() |
| + cmd := exec.Command(bin, args...) |
| + cmd.Env = carlosTestEnv(t) |
| + cmd.Dir = scratch |
| + out, err := cmd.CombinedOutput() |
| + return string(out), err |
| + } |
| + |
| + for _, args := range [][]string{ |
| + {"route", "-registry", regPath, "-host", host, "-channel", "canary/x"}, |
| + {"route", "-registry", regPath, "-host", host, "-grant", "port:25"}, |
| + {"route", "-registry", regPath, "-host", host, "-hibernate"}, |
| + {"route", "-registry", regPath, "-host", host, "-backing", registry.BackingUnit}, |
| + {"route", "-registry", regPath, "-host", host, "-unit", "console.service"}, |
| + } { |
| + out, err := run(args...) |
| + if err == nil { |
| + t.Fatalf("route %v: want non-zero exit, got 0\n%s", args[3:], out) |
| + } |
| + if !strings.Contains(out, provision.SidecarSuffix) { |
| + t.Fatalf("route %v: refusal does not name the reserved suffix:\n%s", args[3:], out) |
| + } |
| + } |
| + |
| + // The -app bulk sweep must not abort on a suffix row swept in alongside |
| + // ordinary ones (a real provisioned sidecar's App equals its owning |
| + // app's, so this is not hypothetical) — it is skipped, like a Platform |
| + // route, while its ordinary sibling still moves. |
| + reg2, err := registry.Open(regPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg2.Upsert(registry.Route{ |
| + Host: "eleven.bdm.oncarlos.com", Kind: "instance", App: "eleven", Account: "bdm", Status: "ready", |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + reg2.Close() |
| + // -environment "-" (clear) is a no-op for a fresh route (Environment is |
| + // already "") and needs no bucket/pubkey setup — chosen so this exercises |
| + // the -app sweep's suffix skip without dragging in the channel axis's |
| + // store/signature machinery, which is not what this test is about. |
| + out, err := run("route", "-registry", regPath, "-app", "eleven", "-account", "bdm", "-environment", "-") |
| + if err != nil { |
| + t.Fatalf("route -app eleven -environment -: %v\n%s", err, out) |
| + } |
| + if !strings.Contains(out, "eleven.bdm.oncarlos.com") { |
| + t.Fatalf("the ordinary sibling route was not moved:\n%s", out) |
| + } |
| + // The suffix row is named in a SKIP line (like a Platform route's own |
| + // skip), not silently dropped — but it must never appear as WRITTEN. |
| + if !strings.Contains(out, host+": skipped") { |
| + t.Fatalf("the suffix row must be named in a skip line, not silently dropped:\n%s", out) |
| + } |
| +} |
diff --git a/internal/console/domainattach.go b/internal/console/domainattach.go| index 40dce96a9..5914cb816 100644 |
| --- a/internal/console/domainattach.go |
| +++ b/internal/console/domainattach.go |
| @@ -93,6 +93,16 @@ func (s *Server) attachDomain(ctx context.Context, acct *Account, app, host, kin |
| return attachResult{}, &doorRefusal{http.StatusUnprocessableEntity, |
| "that hostname is platform-managed"} |
| } |
| + // The reserved sidecar suffix, refused at the same policy stage and with |
| + // the same status as the two checks above: this door must never mint a |
| + // claim (let alone a route record) under a name provision.SidecarSuffix |
| + // reserves. Defence in depth behind Task 3's structural refusal to SERVE |
| + // a Kind=="sidecar" row (edge.go's Reload) — this closes the other |
| + // direction, a member attaching an unrelated app's domain under it. |
| + if strings.HasSuffix(host, provision.SidecarSuffix) { |
| + return attachResult{}, &doorRefusal{http.StatusUnprocessableEntity, |
| + "that hostname is reserved for sidecar processes"} |
| + } |
| route, selfChannel, err := s.readyRouteHost(acct.Sqid, app, host) |
| if err != nil { |
| log.Printf("console: attach %s: registry: %v", host, err) |
diff --git a/internal/console/domainattach_test.go b/internal/console/domainattach_test.go| index 93b90940c..6ad039f83 100644 |
| --- a/internal/console/domainattach_test.go |
| +++ b/internal/console/domainattach_test.go |
| @@ -632,6 +632,25 @@ func TestAttachPlatformSpaceRefused(t *testing.T) { |
| } |
| } |
| +// TestAttachRefusesReservedSidecarSuffix: the attach door must refuse a |
| +// custom domain under provision.SidecarSuffix — never a panic, the family's |
| +// ordinary 422 (the same status HostDenied's platform-space refusal above |
| +// uses), and no claim left behind either. Defence in depth behind Task 3's |
| +// structural refusal to SERVE a Kind=="sidecar" row: this stops the OTHER |
| +// direction, a member attaching an unrelated app under a reserved name. |
| +func TestAttachRefusesReservedSidecarSuffix(t *testing.T) { |
| + fx, acct := attachFixture(t) |
| + shipVersion(t, fx, "story", "v1") |
| + seedChannel(t, fx, "story", "canary/live", "v1") |
| + |
| + host := "bot.eleven.bdm" + provision.SidecarSuffix |
| + _, ref := fx.srv.attachDomain(context.Background(), acct, "story", host, "", "", "paul@keymail.dev") |
| + if ref == nil || ref.Status != 422 { |
| + t.Fatalf("attach %q: refusal = %+v, want 422", host, ref) |
| + } |
| + claimAbsent(t, fx, host) |
| +} |
| + |
| func TestDetachFirstRouteDeletesRecordThenClaim(t *testing.T) { |
| fx, acct := attachFixture(t) |
| shipVersion(t, fx, "story", "v1") |
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go| index 27e0e7c61..9ff0e344b 100644 |
| --- a/internal/domains/aliases.go |
| +++ b/internal/domains/aliases.go |
| @@ -12,6 +12,7 @@ package domains |
| import ( |
| "context" |
| "log" |
| + "strings" |
| "time" |
| "github.com/carlosframework/platform/internal/registry" |
| @@ -24,6 +25,21 @@ import ( |
| // order); drops are logged, never silent. |
| const maxWildcardDomains = 32 |
| +// sidecarSuffix mirrors provision.SidecarSuffix, pinned here rather than |
| +// imported — the same reason claims.go's hostLabelRe gives: internal/provision |
| +// (its reconcile.go) already imports internal/domains for account/route |
| +// materialization, so importing provision back would be a cycle. |
| +// |
| +// A sidecar host is refused a route at the structural level (edge.go's |
| +// Reload skips Kind=="sidecar"), but that guarantee protects only the |
| +// SIDECAR row itself. This constant guards the other direction, at the ONE |
| +// place that can mint a route_aliases row from nothing: a claim naming a |
| +// reserved-suffix host as either end of an alias would insert that host |
| +// into the edge's routing map serving an ORDINARY route's upstream — which |
| +// Reload's Kind check never sees, because the row it reads is the alias's |
| +// primary, not a Kind=="sidecar" row at all. |
| +const sidecarSuffix = ".sidecar.internal" |
| + |
| // ReconcileAliases derives route_aliases and wildcard_domains from the |
| // global claim records. Both tables are wholly pass-owned: every row traces |
| // to a claim, so removal needs no ownership flag. The authoritative tenancy |
| @@ -58,7 +74,15 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist |
| // derivation below. |
| if c.Route != "" { |
| rt, ok := byHost[c.Route] |
| - if !ok { |
| + if strings.HasSuffix(c.Route, sidecarSuffix) || strings.HasSuffix(c.Host, sidecarSuffix) { |
| + // Refused on EITHER end: a reserved-suffix primary must |
| + // never be given a routable name, and an alias whose OWN |
| + // name is under the suffix would insert a sidecar |
| + // hostname into the routing map serving an ordinary |
| + // route's upstream — the direction edge.go's Kind check |
| + // cannot see, because this row's Kind is never "sidecar". |
| + log.Printf("domains: alias %s refused: reserved sidecar suffix (primary %s)", c.Host, c.Route) |
| + } else if !ok { |
| // primary not on this box — another box's claim |
| } else if rt.App != c.App || release.RouteAccount(rt) != c.Account { |
| log.Printf("domains: alias %s refused: route %s belongs to %s/%s, claim says %s/%s", |
| @@ -129,7 +153,14 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist |
| } |
| if c.Catchall { |
| star := "*." + c.Host |
| - if _, taken := byHost[star]; taken { |
| + if strings.HasSuffix(star, sidecarSuffix) || strings.HasSuffix(primaryHost, sidecarSuffix) { |
| + // Same both-ends guard as the plain-alias derivation above, |
| + // through this route_aliases writer's OTHER call site: a |
| + // catch-all's alias name is star, and star inherits c.Host's |
| + // suffix, so this also catches a wildcard host minted under |
| + // the reserved suffix. |
| + log.Printf("domains: catch-all %s refused: reserved sidecar suffix (primary %s)", star, primaryHost) |
| + } else if _, taken := byHost[star]; taken { |
| log.Printf("domains: catch-all %s skipped: a real route owns that host", star) |
| } else { |
| desired[star] = true |
diff --git a/internal/domains/aliases_test.go b/internal/domains/aliases_test.go| index 58f39f12d..b4762ecb3 100644 |
| --- a/internal/domains/aliases_test.go |
| +++ b/internal/domains/aliases_test.go |
| @@ -472,3 +472,93 @@ func TestReconcileWildcardEmptyRouteClaim(t *testing.T) { |
| t.Fatalf("want *.story.example -> story.example, got %+v", aliases) |
| } |
| } |
| + |
| +// --- Task 4: the reserved sidecar suffix, enforced at the alias door ----- |
| +// |
| +// Task 3 already refuses to SERVE a Kind=="sidecar" row (edge.go's Reload). |
| +// ReconcileAliases must never mint a route_aliases row that names a |
| +// reserved-suffix host on EITHER end: as the primary (Host), which would |
| +// keep an unroutable sidecar name "alive" as somebody's alias target, and — |
| +// the direction the Task 3 review caught, and the one most likely to be |
| +// missed — as the alias's OWN name (Alias), which would insert a sidecar |
| +// hostname into the edge's routing map serving an ORDINARY route's upstream, |
| +// making Allowed() return true for it (an ACME order on a sidecar hostname). |
| + |
| +// TestReconcileAliasesRefusesReservedSuffixPrimary: the claim's Route names |
| +// a host under the reserved suffix. Even though such a route can exist in |
| +// the registry (a real provisioned sidecar's own row, Kind=="sidecar"), |
| +// ReconcileAliases must never let anything alias it. |
| +func TestReconcileAliasesRefusesReservedSuffixPrimary(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + reg := aliasRegWith(t, registry.Route{ |
| + Host: "bot.eleven.bdm.sidecar.internal", App: "eleven", Account: "bdm", Kind: "sidecar", Status: "ready", |
| + }) |
| + putClaim(t, s, Claim{ |
| + Host: "tickets.example.com", Account: "bdm", App: "eleven", Route: "bot.eleven.bdm.sidecar.internal", |
| + }) |
| + if err := ReconcileAliases(context.Background(), s, reg, aliasNow()); err != nil { |
| + t.Fatal(err) |
| + } |
| + aliases, err := reg.ListAliases() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(aliases) != 0 { |
| + t.Fatalf("got %d aliases, want 0 (a reserved-suffix primary must never be aliased): %+v", len(aliases), aliases) |
| + } |
| +} |
| + |
| +// TestReconcileAliasesRefusesReservedSuffixAliasName is the end the brief's |
| +// wording alone would miss: the PRIMARY is an ordinary route, but the |
| +// ALIAS's own name is under the reserved suffix. Nothing about tenancy or |
| +// route existence would refuse this — only a check on the alias name |
| +// itself does, and this is the exact row the Task 3 review named: inserted |
| +// into edge's routing map, it would serve the primary's upstream under a |
| +// sidecar hostname and make Allowed() true for it. |
| +func TestReconcileAliasesRefusesReservedSuffixAliasName(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + reg := aliasRegWith(t, registry.Route{ |
| + Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready", |
| + }) |
| + putClaim(t, s, Claim{ |
| + Host: "bot.eleven.bdm.sidecar.internal", Account: "bab", App: "kass", Route: "canary/live", |
| + }) |
| + if err := ReconcileAliases(context.Background(), s, reg, aliasNow()); err != nil { |
| + t.Fatal(err) |
| + } |
| + aliases, err := reg.ListAliases() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(aliases) != 0 { |
| + t.Fatalf("got %d aliases, want 0 (an alias name under the reserved suffix must never be minted): %+v", len(aliases), aliases) |
| + } |
| +} |
| + |
| +// TestReconcileAliasesRefusesReservedSuffixCatchall: the same alias-name-end |
| +// danger, through the OTHER route_aliases writer (the catch-all "*.<host>" |
| +// derivation off a Wildcard+Catchall claim) — a second call site, so it |
| +// needs its own guard, not just the plain-alias one above. |
| +func TestReconcileAliasesRefusesReservedSuffixCatchall(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + reg := aliasRegWith(t) |
| + addRoute(t, reg, "app.bab.oncarlos.com", "bab", "app") |
| + putClaim(t, s, Claim{ |
| + Host: "sub.eleven.bdm.sidecar.internal", Account: "bab", App: "app", |
| + Route: "app.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z", |
| + Wildcard: true, Catchall: true, |
| + }) |
| + if err := ReconcileAliases(ctx, s, reg, aliasNow()); err != nil { |
| + t.Fatal(err) |
| + } |
| + aliases, err := reg.ListAliases() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, a := range aliases { |
| + if strings.HasPrefix(a.Alias, "*.") { |
| + t.Fatalf("a catch-all alias under the reserved suffix must never be minted: %+v", a) |
| + } |
| + } |
| +} |