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)
|
return nil, fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
||||||
}
|
}
|
||||||
portOfEndpoint(values, endpointPorts(m))
|
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})
|
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||||
}
|
}
|
||||||
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
// 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)
|
return nil, fmt.Errorf("%s contributing %s to %s: %w", m.Module, local, to, err)
|
||||||
}
|
}
|
||||||
portOfEndpoint(values, endpointPorts(m))
|
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})
|
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.
|
// receivedFile is the file a provider is given its consumers' contributions in.
|
||||||
func receivedFile(requirement, path string, given []Contribution) (map[string]any, error) {
|
func receivedFile(requirement, path string, given []Contribution) (map[string]any, error) {
|
||||||
if given == nil {
|
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 {
|
func entriesFrom(addresses, accounts map[string]string, suffix string) []rosterEntry {
|
||||||
out := make([]rosterEntry, 0, len(addresses))
|
out := make([]rosterEntry, 0, len(addresses))
|
||||||
for _, name := range sortedNames(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)
|
internal, bare := meshName(name, suffix)
|
||||||
// The account is looked up by whichever key the caller keys accounts on — the internal name
|
// 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.
|
// 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
|
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.
|
// 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.
|
// The one place the default is written, so a fact and a name cannot disagree about it.
|
||||||
func suffixOr(suffix string) string {
|
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)
|
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")
|
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.
|
// - 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.
|
// - 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
|
// - `.Names` is every name the mesh serves (issue 111), so anything on the machine reaching a
|
||||||
// finds the machine serving it; machines with no address yet are already left out of the set.
|
// 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" +
|
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.
|
// Manifest is the module the mesh provides for itself.
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user