Every cutover done on novox tonight (drive, files, files-api, git, keycloak, umami) dropped the <label>.<node>.internal alias HAL always paired with the public hostname — found only when the operator tested it by hand. Not a security boundary (a predecessor proxy served both as a convenience, reaching a service over the VPN without a public TLS round trip, not as access control), so restoring it is composing the same convenience the same way the public name already is: <label> joined to the node's own private address (r.At), independently of whether a public domain exists to join the other half to. composeName's signature changes (publicDomain, internalDomain) but its shape does not — additive, label-gated, apex-aware, exactly mirroring the public half it already did. A contribution the mesh writes both names into is the entire fix; route-adapter and route-proxy pick up internal- name whenever they're updated to serve it, not before, so this alone changes nothing about what is live on any node yet.
235 lines
9.1 KiB
Go
235 lines
9.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// novox/hq ADR 0066 — public routing is name-agnostic.
|
|
//
|
|
// A route contribution carries a label (the subdomain the operator chose); the node carries its
|
|
// public domain; the mesh composes <label>.<public-domain> and interprets neither half. The
|
|
// checks here are the ADR's own: the same catalogue resolves against two domains by changing one
|
|
// node setting and nothing else, and a legacy full-name contribution passes through untouched so
|
|
// the catalogue can migrate module by module.
|
|
|
|
// labelled is a module that asks to be published under a subdomain rather than a full hostname.
|
|
func labelled(module, label string, port int) Manifest {
|
|
return Manifest{Module: module, Version: "1",
|
|
Contributes: map[string]map[string]any{
|
|
"reverse-proxy": {"label": label, "port": port},
|
|
}}
|
|
}
|
|
|
|
// withDomain is a workstation that composes its routed names under one public domain.
|
|
func withDomain(domain string) Node {
|
|
n := workstation()
|
|
n.PublicDomain = domain
|
|
return n
|
|
}
|
|
|
|
func TestALabelComposesWithTheNodesPublicDomain(t *testing.T) {
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if len(given) != 1 {
|
|
t.Fatalf("the proxy was told about %d of 1: %v", len(given), given)
|
|
}
|
|
if given[0].Values["name"] != "git.example.tld" {
|
|
t.Fatalf("the label did not compose with the domain: %v", given[0].Values)
|
|
}
|
|
// The port the module gave is still there — composing the name must not drop what the proxy
|
|
// needs to reach the workload.
|
|
if given[0].Values["port"] != float64(8080) {
|
|
t.Fatalf("composing the name dropped the port: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestTheApexLabelComposesToTheBareDomain(t *testing.T) {
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "@", 4000)),
|
|
[]string{"board"}, withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if len(given) != 1 {
|
|
t.Fatalf("the proxy was told about %d of 1: %v", len(given), given)
|
|
}
|
|
// `@` is the zone-file apex: served at the bare public domain, no subdomain, no leading dot.
|
|
if given[0].Values["name"] != "example.tld" {
|
|
t.Fatalf("the apex label did not compose to the bare domain: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestOneNodeSettingMovesTheCatalogueBetweenDomains(t *testing.T) {
|
|
// The ADR's name-agnostic check: the same catalogue resolves against two different public
|
|
// domains by changing one node setting and nothing else.
|
|
one, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
two, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, withDomain("other.example"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
first := received(t, mustDeclare(t, one))[0].Values
|
|
second := received(t, mustDeclare(t, two))[0].Values
|
|
|
|
if first["name"] != "git.example.tld" || second["name"] != "git.other.example" {
|
|
t.Fatalf("the name did not track the domain: %v then %v", first["name"], second["name"])
|
|
}
|
|
// And nothing else moved: same label, same port. The domain is the one fact that changed.
|
|
if first["label"] != second["label"] || first["label"] != "git" {
|
|
t.Fatalf("the label changed with the domain: %v then %v", first["label"], second["label"])
|
|
}
|
|
if first["port"] != second["port"] {
|
|
t.Fatalf("the port changed with the domain: %v then %v", first["port"], second["port"])
|
|
}
|
|
}
|
|
|
|
func TestALegacyFullNameContributionPassesThroughUnchanged(t *testing.T) {
|
|
// Backward compatibility: a contribution that still carries a full `name` and no `label` is
|
|
// granted that name as-is, even on a node that has a public domain. This is what lets the
|
|
// catalogue migrate one module at a time while the running mesh keeps working.
|
|
legacy := Manifest{Module: "board", Version: "1",
|
|
Contributes: map[string]map[string]any{
|
|
"reverse-proxy": {"name": "git.pinned.example", "port": 8080},
|
|
}}
|
|
got, err := Resolve(shelf(proxy(), legacy), []string{"board"}, withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if given[0].Values["name"] != "git.pinned.example" {
|
|
t.Fatalf("a full name was rewritten: %v", given[0].Values)
|
|
}
|
|
if _, grewLabel := given[0].Values["label"]; grewLabel {
|
|
t.Fatalf("a legacy contribution grew a label: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestALabelWithNoPublicDomainComposesNothing(t *testing.T) {
|
|
// A routed module on a node with no public domain has nothing to join its label to. It composes
|
|
// no name — which reads downstream exactly as a route that named no host, the same as before
|
|
// this existed — rather than an fabricated `git.` with a dangling dot.
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if _, named := given[0].Values["name"]; named {
|
|
t.Fatalf("a name was composed with no domain to compose it from: %v", given[0].Values)
|
|
}
|
|
if given[0].Values["label"] != "git" {
|
|
t.Fatalf("the label was lost: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
// withPrivateAddress is a workstation on the private network, at the given internal name — the
|
|
// same fact a route's own consumers already receive as `${bound:...:at}`.
|
|
func withPrivateAddress(at string) Node {
|
|
n := workstation()
|
|
n.At = at
|
|
return n
|
|
}
|
|
|
|
func TestALabelComposesWithTheNodesPrivateAddressToo(t *testing.T) {
|
|
// A predecessor proxy answered a route on both a public and a private-network hostname for the
|
|
// same convenience the mesh restores here: reaching a service over the VPN without a public TLS
|
|
// round trip. Composed independently of the public name, from the node's own `At`.
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, withPrivateAddress("anchor.internal"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if given[0].Values["internal-name"] != "git.anchor.internal" {
|
|
t.Fatalf("the label did not compose with the private address: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestThePublicAndInternalNamesComposeIndependently(t *testing.T) {
|
|
// A node with both a public domain and a private address gets both names from one label; a
|
|
// node with only one of the two gets only the matching one — neither composition depends on
|
|
// the other being possible.
|
|
both := withPrivateAddress("anchor.internal")
|
|
both.PublicDomain = "example.tld"
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, both, World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if given[0].Values["name"] != "git.example.tld" {
|
|
t.Fatalf("the public name did not compose alongside the internal one: %v", given[0].Values)
|
|
}
|
|
if given[0].Values["internal-name"] != "git.anchor.internal" {
|
|
t.Fatalf("the internal name did not compose alongside the public one: %v", given[0].Values)
|
|
}
|
|
|
|
publicOnly, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
|
[]string{"board"}, withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
givenPublicOnly := received(t, mustDeclare(t, publicOnly))
|
|
if _, has := givenPublicOnly[0].Values["internal-name"]; has {
|
|
t.Fatalf("an internal name was composed with no private address to compose it from: %v",
|
|
givenPublicOnly[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestTheApexLabelComposesToTheBarePrivateAddress(t *testing.T) {
|
|
got, err := Resolve(shelf(proxy(), labelled("board", "@", 4000)),
|
|
[]string{"board"}, withPrivateAddress("anchor.internal"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
given := received(t, mustDeclare(t, got))
|
|
if given[0].Values["internal-name"] != "anchor.internal" {
|
|
t.Fatalf("the apex label did not compose to the bare private address: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestARoutedNameResolvesToTheServingNode(t *testing.T) {
|
|
// novox/hq ADR 0066 propagate: a granted route name is published into internal resolution,
|
|
// mapped to the node that serves it, alongside the `<node>.internal` names — so every
|
|
// container, and an in-mesh ACME validator, resolves a routed name to the proxy that serves it.
|
|
// The names map is what withMeshNames writes into every container as `--add-host`; a route name
|
|
// mapped to the serving node's address rides the same mechanism.
|
|
got := containersOf(t, Resolution{Node: "anchor", Modules: []Manifest{{
|
|
Module: "app",
|
|
Resources: []map[string]any{{"id": "web", "type": "container", "name": "web",
|
|
"image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}},
|
|
}}}, Rendering{Names: map[string]string{
|
|
"anchor.internal": "10.42.0.1",
|
|
"git.example.tld": "10.42.0.1",
|
|
}})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected one container, got %d", len(got))
|
|
}
|
|
given := namesOf(got[0])
|
|
var sawNode, sawRoute bool
|
|
for _, h := range given {
|
|
if h == "anchor.internal:10.42.0.1" {
|
|
sawNode = true
|
|
}
|
|
if h == "git.example.tld:10.42.0.1" {
|
|
sawRoute = true
|
|
}
|
|
}
|
|
if !sawNode {
|
|
t.Fatalf("the container lost the mesh's node names: %v", given)
|
|
}
|
|
if !sawRoute {
|
|
t.Fatalf("the routed name was not published to the serving node: %v", given)
|
|
}
|
|
}
|