A route's internal name says where the request arrives
novox/hq ADR 0151 (issues 139, 157). <label>.<node>.internal is answered by every resolver as 'anything under that node goes to that node', so the node in a route's internal name must be the one whose proxy answers it; composed under the consumer's own name it sent a client to a machine with nothing listening whenever the proxy ran elsewhere. Composed under the serving node now — the same machine wherever the proxy runs beside the module, so nothing changes on a mesh with one hub. A routed public name gets no .internal alias any more: the roster publishes it as itself, once. The alias resolved and nothing served it.
This commit is contained in:
@@ -1101,7 +1101,7 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
||||
return nil, fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
||||
}
|
||||
portOfEndpoint(values, endpointPorts(m))
|
||||
composeName(values, r.PublicDomain, r.At, reaches, endpointPorts(m), blocks)
|
||||
composeName(values, r.PublicDomain, servingAt(r, to), reaches, endpointPorts(m), blocks)
|
||||
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||
}
|
||||
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
||||
@@ -1124,7 +1124,7 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
||||
return nil, fmt.Errorf("%s contributing %s to %s: %w", m.Module, local, to, err)
|
||||
}
|
||||
portOfEndpoint(values, endpointPorts(m))
|
||||
composeName(values, r.PublicDomain, r.At, reaches, endpointPorts(m), blocks)
|
||||
composeName(values, r.PublicDomain, servingAt(r, to), reaches, endpointPorts(m), blocks)
|
||||
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||
}
|
||||
}
|
||||
@@ -1224,6 +1224,24 @@ func composeName(values map[string]any, publicDomain, internalDomain string, rea
|
||||
}
|
||||
}
|
||||
|
||||
// servingAt is the private-network name of the node a contribution to `to` arrives at: the
|
||||
// provider's, when the provision is answered elsewhere, and this machine's own when it is answered
|
||||
// here or not yet settled.
|
||||
//
|
||||
// A route's internal name is composed under it (novox/hq ADR 0151, issue 139). `<label>.<node>.internal`
|
||||
// is answered by every machine's resolver as *anything under that node's name goes to that node* —
|
||||
// so the node in the name has to be the one whose proxy answers, or the name sends a client to a
|
||||
// machine with nothing listening while the public name, published at the serving node's address,
|
||||
// works. Where the proxy runs beside the module the two are the same machine and nothing changes.
|
||||
func servingAt(r Resolution, to string) string {
|
||||
for _, n := range r.Needs {
|
||||
if n.Name == to && n.At != "" {
|
||||
return n.At
|
||||
}
|
||||
}
|
||||
return r.At
|
||||
}
|
||||
|
||||
// receivedFile is the file a provider is given its consumers' contributions in.
|
||||
func receivedFile(requirement, path string, given []Contribution) (map[string]any, error) {
|
||||
if given == nil {
|
||||
|
||||
@@ -162,6 +162,13 @@ func renderRoster(tmpl string, view rosterView) (string, error) {
|
||||
func entriesFrom(addresses, accounts map[string]string, suffix string) []rosterEntry {
|
||||
out := make([]rosterEntry, 0, len(addresses))
|
||||
for _, name := range sortedNames(addresses) {
|
||||
if routed(name, suffix) {
|
||||
// A routed name is already a full name under a public domain, and it has no mesh
|
||||
// form: appending the suffix made `<name>.<suffix>`, which every machine's hosts file
|
||||
// carried and nothing served (novox/hq issue 157). It is published as itself, once.
|
||||
out = append(out, rosterEntry{Name: name, FQDN: name, Address: addresses[name], Account: accounts[name]})
|
||||
continue
|
||||
}
|
||||
internal, bare := meshName(name, suffix)
|
||||
// The account is looked up by whichever key the caller keys accounts on — the internal name
|
||||
// or the bare one — so a template gets the right login however the maps were built.
|
||||
@@ -187,6 +194,14 @@ func meshName(name, suffix string) (internal, bare string) {
|
||||
return name + dotted, name
|
||||
}
|
||||
|
||||
// routed says whether a name the mesh serves is a routed public name rather than a machine's: it
|
||||
// carries a domain of its own and not the mesh's suffix. A machine's name is bare (`homer`) or
|
||||
// internal (`homer.internal`); anything else with a dot in it was composed under a public domain.
|
||||
func routed(name, suffix string) bool {
|
||||
dotted := "." + strings.TrimPrefix(suffixOr(suffix), ".")
|
||||
return strings.Contains(name, ".") && !strings.HasSuffix(name, dotted)
|
||||
}
|
||||
|
||||
// suffixOr is the suffix given, or the one the mesh composes names with when none was handed down.
|
||||
// The one place the default is written, so a fact and a name cannot disagree about it.
|
||||
func suffixOr(suffix string) string {
|
||||
|
||||
@@ -258,3 +258,24 @@ func TestAHomeFactIsSkippedWhereThereIsNoAccount(t *testing.T) {
|
||||
t.Fatalf("a home fact was placed on a machine with no operator account: %v", given)
|
||||
}
|
||||
}
|
||||
|
||||
// A routed name is already a full name under a public domain and has no mesh form. Appending the
|
||||
// suffix to it made `git.example.tld.internal` — carried by every machine's hosts file, served by
|
||||
// nothing, and refused by the proxy at the handshake (novox/hq issue 157). It is published as
|
||||
// itself, and only a machine has a bare name beside its full one.
|
||||
func TestARoutedNameIsPublishedAsItselfAndNotSuffixed(t *testing.T) {
|
||||
names := map[string]string{"homer.internal": "10.42.0.1", "git.example.tld": "10.42.0.1"}
|
||||
tmpl := RosterFile{Path: "/f", Template: "{{range .Names}}{{.FQDN}} {{.Name}}\n{{end}}"}
|
||||
out, err := FactsInto(Manifest{Module: "a", Facts: map[string]RosterFile{"f": tmpl}},
|
||||
Resolution{Node: "homer"}, names, map[string]string{"homer.internal": "10.42.0.1"}, nil, "internal")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out[0]["content"].(string)
|
||||
if strings.Contains(got, "tld.internal") {
|
||||
t.Fatalf("the routed name was given a suffixed alias that nothing serves:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "git.example.tld git.example.tld\n") || !strings.Contains(got, "homer.internal homer\n") {
|
||||
t.Fatalf("the roster does not carry the routed name as itself beside the machine's two forms:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,3 +228,29 @@ func TestARoutedNameResolvesToTheServingNode(t *testing.T) {
|
||||
t.Fatalf("the routed name is not in the roster the machine's resolver answers from")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 139, ADR 0151: `<label>.<node>.internal` is answered by every machine's resolver as
|
||||
// "anything under that node's name goes to that node", so the node in a route's internal name must
|
||||
// be the one whose proxy answers it. Composed under the consumer's own name, a route served from
|
||||
// another machine got an internal name that resolved to a machine with nothing listening, while the
|
||||
// public name — published at the serving node's address — worked.
|
||||
func TestARoutesInternalNameIsComposedUnderTheNodeThatServesIt(t *testing.T) {
|
||||
hub := proxy()
|
||||
hub.Provides = FromAnywhere("reverse-proxy")
|
||||
got, err := Resolve(shelf(hub, labelled("board", "git", 8080)), []string{"board"},
|
||||
withPrivateAddress("laptop.internal"), World{Offered: map[string][]Provider{
|
||||
"reverse-proxy": {{Node: "anchor", At: "anchor.internal", Module: "traefik"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Gathered the way the control plane gathers a consumer's contribution for a provider on
|
||||
// another machine.
|
||||
values, asks, err := got.ContributionsFrom("reverse-proxy", "board", nil)
|
||||
if err != nil || !asks {
|
||||
t.Fatalf("the route was not contributed: %v %v", asks, err)
|
||||
}
|
||||
if values["internal-name"] != "git.anchor.internal" {
|
||||
t.Fatalf("the internal name does not say where the request arrives: %v", values)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user