A route's internal name says where the request arrives #163
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,10 +169,12 @@ func (g *Generator) Graph() Graph { return g.graph }
|
||||
//
|
||||
// - No floor: no header, no localhost, no `127.0.1.1` — those are the machine's, above the region.
|
||||
// - A machine's own line is marked, and its mesh name resolves to its mesh address, not loopback.
|
||||
// - `.Names` is every name the mesh serves (issue 111), so a container reaching a routed name
|
||||
// finds the machine serving it; machines with no address yet are already left out of the set.
|
||||
// - `.Names` is every name the mesh serves (issue 111), so anything on the machine reaching a
|
||||
// routed name through its resolver finds the machine serving it; machines with no address yet
|
||||
// are already left out of the set. A routed name is one alias, itself — a machine has a bare
|
||||
// name beside its full one, a routed name has nothing beside it (issue 157).
|
||||
const hostsTemplate = "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
|
||||
"{{range .Names}}{{.Address}}\t{{.FQDN}}\t{{.Name}}{{if eq .Name $.Node}}\t# this machine{{end}}\n{{end}}"
|
||||
"{{range .Names}}{{.Address}}\t{{.FQDN}}{{if ne .Name .FQDN}}\t{{.Name}}{{end}}{{if eq .Name $.Node}}\t# this machine{{end}}\n{{end}}"
|
||||
|
||||
// Manifest is the module the mesh provides for itself.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user