The uplink seat (ADR 0117), and the mesh's names written into the hosts file, not over it (hq 128) #79
+70
-26
@@ -18,8 +18,10 @@ import (
|
||||
// module at all (novox/hq ADR 0040).
|
||||
|
||||
const (
|
||||
// FactNodeNames is every machine's name and address, as a hosts file.
|
||||
// FactNodeNames is every machine's name and address, as lines of a hosts file.
|
||||
//
|
||||
// Written *into* the machine's hosts file as a region of its own, never as the file: the rest
|
||||
// of that file is the distribution's, the operator's and other tools' (novox/hq issue 128).
|
||||
// Exact names only: `homer` and `homer.internal` resolve to homer. Anything *under* a machine
|
||||
// is a wildcard, which a hosts file cannot express — that is FactNodeZones.
|
||||
FactNodeNames = "node-names"
|
||||
@@ -31,20 +33,44 @@ const (
|
||||
FactNodeZones = "node-zones"
|
||||
)
|
||||
|
||||
// facts is every fact the mesh computes, and what writes it.
|
||||
// fact is one thing the mesh computes, and how it is written.
|
||||
type fact struct {
|
||||
// render is the fact's content. A fact is written from the names it is about. `every` is every
|
||||
// name the mesh serves — machines and the names it was told to route; `machines` is only the
|
||||
// machines. A fact takes the set it is true of, and the two must not be confused
|
||||
// (novox/hq 04-ISSUES/111).
|
||||
render func(r Resolution, every, machines map[string]string, suffix string) string
|
||||
// shared is whether the file the fact goes to belongs to the machine rather than to the mesh.
|
||||
//
|
||||
// **A property of the fact, not of the path a module asked for.** A hosts file is the
|
||||
// machine's wherever it lives: the distribution put `localhost` in it, the operator added
|
||||
// their own lines, and a local development tool keeps marked blocks of its own there. The
|
||||
// mesh writing it whole replaced all of that the moment the private network was taken, and
|
||||
// every later write by the other tool was lost at the next machine joining — silently, with
|
||||
// both sides believing they owned the file (novox/hq issue 128). That is ADR 0102's failure in
|
||||
// a file 0102 did not name, because its merge is structured and a hosts file is not; the
|
||||
// answer is the same idea for text — a marked region the host owns, everything outside it
|
||||
// kept byte for byte. A resolver's zones file is the other way about: the mesh owns it, and
|
||||
// nothing else writes there.
|
||||
shared bool
|
||||
}
|
||||
|
||||
// facts is every fact the mesh computes, and how each is written.
|
||||
//
|
||||
// **A closed list.** A module asking for a fact the mesh does not have is asking for a file nobody
|
||||
// will write, and finding that out on a machine — as a daemon that starts, reads nothing, and
|
||||
// answers no queries — is worse than being told where the manifest is.
|
||||
// A fact is written from the names it is about. `every` is every name the mesh serves — machines
|
||||
// and the names it was told to route; `machines` is only the machines. A fact takes the set it is
|
||||
// true of, and the two must not be confused (novox/hq 04-ISSUES/111).
|
||||
var facts = map[string]func(r Resolution, every, machines map[string]string, suffix string) string{
|
||||
FactNodeNames: func(r Resolution, every, _ map[string]string, suffix string) string {
|
||||
return nodeNames(r, every, suffix)
|
||||
var facts = map[string]fact{
|
||||
FactNodeNames: {
|
||||
render: func(r Resolution, every, _ map[string]string, suffix string) string {
|
||||
return nodeNames(r, every, suffix)
|
||||
},
|
||||
shared: true,
|
||||
},
|
||||
FactNodeZones: func(r Resolution, _, machines map[string]string, suffix string) string {
|
||||
return nodeZones(r, machines, suffix)
|
||||
FactNodeZones: {
|
||||
render: func(r Resolution, _, machines map[string]string, suffix string) string {
|
||||
return nodeZones(r, machines, suffix)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,7 +90,7 @@ func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string,
|
||||
|
||||
out := make([]map[string]any, 0, len(names))
|
||||
for _, name := range names {
|
||||
write, known := facts[name]
|
||||
f, known := facts[name]
|
||||
if !known {
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks the mesh for %q, which it does not compute. It has %s",
|
||||
@@ -75,10 +101,23 @@ func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string,
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks for %q at %q, which is not an absolute path", m.Module, name, path)
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
file := map[string]any{
|
||||
"id": "fact-" + name, "type": "file", "path": path, "mode": "0644",
|
||||
"content": write(r, addresses, machines, suffix),
|
||||
})
|
||||
"content": f.render(r, addresses, machines, suffix),
|
||||
}
|
||||
if f.shared {
|
||||
// The host owns only the lines between `# BEGIN mesh <id>` and `# END mesh <id>` and
|
||||
// keeps the rest of the file byte for byte; undeclared, the region goes and nothing
|
||||
// else does. Replacing nothing, it is written on an adopted node without being held,
|
||||
// so a machine is named on the private network before its module is taken.
|
||||
//
|
||||
// **Hosts first, then this.** A host older than the block mode refuses the whole
|
||||
// declaration on an `into` it does not know — not just this file, everything — so
|
||||
// every host is upgraded before a controller emitting it is rolled out, the same
|
||||
// order ADR 0102 set for `into: json` (novox/hq issue 128).
|
||||
file["into"] = "block"
|
||||
}
|
||||
out = append(out, file)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -93,7 +132,18 @@ func spokenFacts() string {
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// nodeNames is every machine's name and address, as a hosts file.
|
||||
// nodeNames is every machine's name and address, as the mesh's region of a hosts file.
|
||||
//
|
||||
// **Only the mesh's names.** No header claiming the file, no `localhost`, no `127.0.1.1` line for
|
||||
// the machine itself. Those used to be written here as the floor every Linux expects, because the
|
||||
// mesh wrote the whole file and removing them would have broken things with nothing to do with
|
||||
// the mesh. They were never the mesh's: the distribution wrote them before the mesh arrived and
|
||||
// will want them after it leaves, and a machine's own name belongs to whoever named the machine.
|
||||
// Now the host writes this into a marked region (novox/hq issue 128) and leaves the rest of the
|
||||
// file as it found it, so the floor stays where it always was — the machine's — and the mesh
|
||||
// writing a second `localhost` beside it would be one more line nobody could say the owner of.
|
||||
// The one comment line is for a person reading the file: which lines are the mesh's, and that
|
||||
// editing them is pointless.
|
||||
//
|
||||
// **A machine with no address is left out.** The mesh has a record for it — somebody added it —
|
||||
// and does not yet know where it is, which is the ordinary state between adding a machine and it
|
||||
@@ -101,21 +151,15 @@ func spokenFacts() string {
|
||||
// that hangs; leaving it out fails at once and says the name is unknown.
|
||||
func nodeNames(r Resolution, addresses map[string]string, suffix string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
||||
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
||||
// The floor every Linux expects, and which removing would break things that have nothing to do
|
||||
// with the mesh.
|
||||
b.WriteString("127.0.0.1\tlocalhost\n")
|
||||
b.WriteString("::1\t\tlocalhost ip6-localhost ip6-loopback\n")
|
||||
if r.Node != "" {
|
||||
fmt.Fprintf(&b, "127.0.1.1\t%s\n", r.Node)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString("# The mesh's names. This region is replaced whenever a machine joins or leaves.\n")
|
||||
for _, name := range sortedNames(addresses) {
|
||||
at := addresses[name]
|
||||
internal, bare := meshName(name, suffix)
|
||||
// Its mesh name resolves to its address on the private network rather than to loopback,
|
||||
// so a service binding the name it was given stays reachable from everywhere else.
|
||||
// so a service binding the name it was given stays reachable from everywhere else. The
|
||||
// bare name may be answered first by a line of the machine's own — `127.0.1.1 homer`,
|
||||
// above the region — and that is the machine's choice to have made; the mesh name is the
|
||||
// one nothing else in the file writes.
|
||||
fmt.Fprintf(&b, "%s\t%s\t%s", at, internal, bare)
|
||||
if bare == r.Node {
|
||||
b.WriteString("\t# this machine")
|
||||
|
||||
@@ -54,9 +54,60 @@ func TestAMachinesOwnNameIsItsMeshAddress(t *testing.T) {
|
||||
if !strings.HasPrefix(line, "10.42.0.1") {
|
||||
t.Fatalf("a machine's own mesh name is not its mesh address: %q", line)
|
||||
}
|
||||
// And the loopback floor is still there, or things with nothing to do with the mesh break.
|
||||
if !strings.Contains(out, "127.0.0.1\tlocalhost") {
|
||||
t.Fatalf("the loopback floor was removed:\n%s", out)
|
||||
}
|
||||
|
||||
// novox/hq issue 128: the names are the mesh's region of the machine's hosts file, and only that.
|
||||
//
|
||||
// **The loopback floor is the machine's now, not the mesh's.** It was written here while the mesh
|
||||
// wrote the whole file. Written into a region, a `localhost` or a `127.0.1.1 homer` of the mesh's
|
||||
// would stand beside the distribution's own — a second answer nobody could say the owner of, and
|
||||
// one that goes when the mesh leaves. A header claiming the file would be a lie about the rest of
|
||||
// it. And no line may look like the host's own markers, or the host would refuse the region.
|
||||
func TestTheNamesAreOnlyTheMeshsRegionOfTheFile(t *testing.T) {
|
||||
out := nodeNames(Resolution{Node: "homer"}, threeMachines, "")
|
||||
want := "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
|
||||
"10.42.0.1\thomer.internal\thomer\t# this machine\n" +
|
||||
"10.42.0.2\tmarge.internal\tmarge\n"
|
||||
if out != want {
|
||||
t.Fatalf("the region is not exactly the mesh's names:\n%s\n--- want ---\n%s", out, want)
|
||||
}
|
||||
for _, floor := range []string{"localhost", "127.0.", "::1", "Generated by the mesh", "# BEGIN mesh ", "# END mesh "} {
|
||||
if strings.Contains(out, floor) {
|
||||
t.Fatalf("the region carries %q, which is not the mesh's to write:\n%s", floor, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hosts file is written into; the resolver's zones are written whole. A property of each fact,
|
||||
// so a module asking for node-names at another path still does not get a file of the mesh's.
|
||||
func TestOnlyTheNamesAreWrittenIntoASharedFile(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]string{
|
||||
FactNodeZones: "/etc/mesh-resolver/nodes.conf", FactNodeNames: "/etc/hosts",
|
||||
}}
|
||||
given, err := FactsInto(m, Resolution{Node: "homer"}, threeMachines, threeMachines, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(given) != 2 {
|
||||
t.Fatalf("expected two files, got %v", given)
|
||||
}
|
||||
for _, f := range given {
|
||||
switch f["path"] {
|
||||
case "/etc/hosts":
|
||||
if f["into"] != "block" {
|
||||
t.Errorf("the hosts file is written over rather than into: %v", f)
|
||||
}
|
||||
case "/etc/mesh-resolver/nodes.conf":
|
||||
// The mesh owns the resolver's zones; nothing else writes there.
|
||||
if into, set := f["into"]; set {
|
||||
t.Errorf("the zones file is written into (%v), and it is the mesh's whole", into)
|
||||
}
|
||||
if !strings.HasPrefix(f["content"].(string), "# Generated by the mesh.") {
|
||||
t.Errorf("the zones file lost its header: %v", f["content"])
|
||||
}
|
||||
default:
|
||||
t.Errorf("a file nobody asked for: %v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,10 @@ const DefaultSuffix = "internal"
|
||||
//
|
||||
// This is not the `/etc/hosts` floor the design removes. That floor existed because a node had to
|
||||
// reach the mesh's database before its own DNS worked — a fallback for a circularity, and the
|
||||
// circularity is gone. This is the mechanism itself: the complete set of names in this mesh,
|
||||
// generated whole and owned by the mesh (novox/hq ADR 0011), rather than a patch written
|
||||
// underneath something else.
|
||||
// circularity is gone. This is the mechanism itself: the complete set of names in this mesh
|
||||
// (novox/hq ADR 0011). Written as a marked region *into* the file rather than as the file: the
|
||||
// rest of it — `localhost`, the machine's own name, other tools' blocks — is the machine's, and
|
||||
// writing it whole replaced all of that (novox/hq issue 128).
|
||||
//
|
||||
// A file rather than a resolver daemon, deliberately, for now: it works on every Linux, needs no
|
||||
// package, and has no failure mode of its own. A daemon becomes necessary when names are wanted
|
||||
|
||||
Reference in New Issue
Block a user