Merge pull request 'The uplink seat (ADR 0117), and the mesh's names written into the hosts file, not over it (hq 128)' (#79) from feat/the-uplink-seat-and-the-hosts-region into main
This commit was merged in pull request #79.
This commit is contained in:
@@ -923,12 +923,26 @@ func planCommand(ctx context.Context, args []string) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("\n--- %v %v ---\n%s", r["id"], r["path"], content)
|
fmt.Printf("\n--- %s ---\n%s", shownAs(r), content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shownAs is the heading `plan --show` puts over a resource's content.
|
||||||
|
//
|
||||||
|
// **A file written into says so.** Its content is the mesh's part of a file that is otherwise the
|
||||||
|
// machine's — the keys of a JSON document (novox/hq ADR 0102), the region of a hosts file (issue
|
||||||
|
// 128). Shown under a bare path it reads as the whole file, and a person checking what a take
|
||||||
|
// replaces would see a hosts file of a dozen lines where the machine keeps thirty.
|
||||||
|
func shownAs(r map[string]any) string {
|
||||||
|
heading := fmt.Sprintf("%v %v", r["id"], r["path"])
|
||||||
|
if into, ok := r["into"].(string); ok && into != "" {
|
||||||
|
heading += fmt.Sprintf(" (written into, %s)", into)
|
||||||
|
}
|
||||||
|
return heading
|
||||||
|
}
|
||||||
|
|
||||||
// licencesFor is what this node can be answered with by record, and what it was put on.
|
// licencesFor is what this node can be answered with by record, and what it was put on.
|
||||||
//
|
//
|
||||||
// A mesh with no licences at all is the ordinary case and must not be an error: every existing
|
// A mesh with no licences at all is the ordinary case and must not be an error: every existing
|
||||||
|
|||||||
@@ -35,3 +35,17 @@ func TestListensLinesAreEmptyForAModuleWithNothingToListenOn(t *testing.T) {
|
|||||||
t.Errorf("a module with no listens should print nothing, got %v", got)
|
t.Errorf("a module with no listens should print nothing, got %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `plan --show` says when a file is written into rather than over (novox/hq issue 128), or the
|
||||||
|
// mesh's region of a hosts file reads as the whole file.
|
||||||
|
func TestAFileWrittenIntoIsShownAsSuch(t *testing.T) {
|
||||||
|
region := shownAs(map[string]any{
|
||||||
|
"id": "mesh-wireguard.fact-node-names", "path": "/etc/hosts", "into": "block"})
|
||||||
|
if region != "mesh-wireguard.fact-node-names /etc/hosts (written into, block)" {
|
||||||
|
t.Errorf("the region is shown as %q", region)
|
||||||
|
}
|
||||||
|
whole := shownAs(map[string]any{"id": "dnsmasq.fact-node-zones", "path": "/etc/mesh-resolver/nodes.conf"})
|
||||||
|
if strings.Contains(whole, "written into") {
|
||||||
|
t.Errorf("a whole file is shown as written into: %q", whole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+74
-26
@@ -18,8 +18,10 @@ import (
|
|||||||
// module at all (novox/hq ADR 0040).
|
// module at all (novox/hq ADR 0040).
|
||||||
|
|
||||||
const (
|
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
|
// 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.
|
// is a wildcard, which a hosts file cannot express — that is FactNodeZones.
|
||||||
FactNodeNames = "node-names"
|
FactNodeNames = "node-names"
|
||||||
@@ -31,20 +33,44 @@ const (
|
|||||||
FactNodeZones = "node-zones"
|
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
|
// **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
|
// 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.
|
// 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
|
var facts = map[string]fact{
|
||||||
// and the names it was told to route; `machines` is only the machines. A fact takes the set it is
|
FactNodeNames: {
|
||||||
// true of, and the two must not be confused (novox/hq 04-ISSUES/111).
|
render: func(r Resolution, every, _ map[string]string, suffix string) string {
|
||||||
var facts = map[string]func(r Resolution, every, machines map[string]string, suffix string) string{
|
return nodeNames(r, every, suffix)
|
||||||
FactNodeNames: 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 {
|
FactNodeZones: {
|
||||||
return nodeZones(r, machines, suffix)
|
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))
|
out := make([]map[string]any, 0, len(names))
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
write, known := facts[name]
|
f, known := facts[name]
|
||||||
if !known {
|
if !known {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"%s asks the mesh for %q, which it does not compute. It has %s",
|
"%s asks the mesh for %q, which it does not compute. It has %s",
|
||||||
@@ -75,10 +101,27 @@ func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string,
|
|||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"%s asks for %q at %q, which is not an absolute path", m.Module, name, path)
|
"%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",
|
"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 — an order to roll out in, not a note.** A host older than
|
||||||
|
// the block mode refuses the whole declaration on an `into` it does not know: not just
|
||||||
|
// this file, everything the node was sent, so it stops converging on anything at all.
|
||||||
|
// Every node on the private network receives this fact, so every node's host —
|
||||||
|
// the controller's own machine included, which would otherwise stop taking the
|
||||||
|
// declaration that runs the controller — must run a block-aware host 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
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -93,7 +136,18 @@ func spokenFacts() string {
|
|||||||
return strings.Join(names, ", ")
|
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 —
|
// **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
|
// and does not yet know where it is, which is the ordinary state between adding a machine and it
|
||||||
@@ -101,21 +155,15 @@ func spokenFacts() string {
|
|||||||
// that hangs; leaving it out fails at once and says the name is unknown.
|
// 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 {
|
func nodeNames(r Resolution, addresses map[string]string, suffix string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
b.WriteString("# The mesh's names. This region is replaced whenever a machine joins or leaves.\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")
|
|
||||||
for _, name := range sortedNames(addresses) {
|
for _, name := range sortedNames(addresses) {
|
||||||
at := addresses[name]
|
at := addresses[name]
|
||||||
internal, bare := meshName(name, suffix)
|
internal, bare := meshName(name, suffix)
|
||||||
// Its mesh name resolves to its address on the private network rather than to loopback,
|
// 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)
|
fmt.Fprintf(&b, "%s\t%s\t%s", at, internal, bare)
|
||||||
if bare == r.Node {
|
if bare == r.Node {
|
||||||
b.WriteString("\t# this machine")
|
b.WriteString("\t# this machine")
|
||||||
|
|||||||
@@ -54,9 +54,60 @@ func TestAMachinesOwnNameIsItsMeshAddress(t *testing.T) {
|
|||||||
if !strings.HasPrefix(line, "10.42.0.1") {
|
if !strings.HasPrefix(line, "10.42.0.1") {
|
||||||
t.Fatalf("a machine's own mesh name is not its mesh address: %q", line)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package catalogue_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/overlay"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq issue 128, held where the host will see it: the shipped networking module's names,
|
||||||
|
// through the whole composition, and not only through FactsInto.
|
||||||
|
//
|
||||||
|
// Composition prefixes a fact's id with the module that asked for it and passes everything else
|
||||||
|
// through; a step that dropped `into` on the way would send the region as a whole file, and the
|
||||||
|
// host would write the machine's hosts file over again with every unit test above still green.
|
||||||
|
|
||||||
|
// onTheNetwork stands in for the overlay's generator: the node is part of the private network,
|
||||||
|
// and what the generator writes is not what is under test here.
|
||||||
|
type onTheNetwork struct{}
|
||||||
|
|
||||||
|
func (onTheNetwork) Resources(string) ([]map[string]any, bool, error) {
|
||||||
|
return []map[string]any{{"id": "overlay-config", "type": "file",
|
||||||
|
"path": "/etc/wireguard/mesh0.conf", "mode": "0600", "content": "[Interface]\n"}}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheHostsRegionArrivesAsTheHostWillReadIt(t *testing.T) {
|
||||||
|
shelf := provided(t)
|
||||||
|
// A resolver restarting on the names another module put on the machine, and one resource it
|
||||||
|
// only runs at start — neither of which composition has any business changing.
|
||||||
|
resolver, err := catalogue.ParseManifest([]byte(`{
|
||||||
|
"module": "resolver", "version": "1", "requires": ["mesh-addressing"],
|
||||||
|
"resources": [
|
||||||
|
{"id": "seed", "type": "file", "path": "/etc/resolver/seed", "mode": "0644",
|
||||||
|
"content": "seed\n", "at": "start"},
|
||||||
|
{"id": "daemon", "type": "service", "unit": "resolver.service", "state": "running",
|
||||||
|
"restart-on": ["seed", "mesh-wireguard.fact-node-names"]}
|
||||||
|
]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
shelf[resolver.Module] = resolver
|
||||||
|
|
||||||
|
got, err := catalogue.Resolve(shelf, []string{overlay.Domain, "resolver"},
|
||||||
|
catalogue.Node{Name: "homer", At: "homer.internal"}, catalogue.World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
names := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"}
|
||||||
|
out, err := got.Declaration(catalogue.Rendering{
|
||||||
|
Names: names, Machines: names, Suffix: "internal",
|
||||||
|
Generators: map[string]catalogue.Generator{overlay.Name: onTheNetwork{}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ids := map[string]map[string]any{}
|
||||||
|
for _, r := range out {
|
||||||
|
ids[r["id"].(string)] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := ids[overlay.Name+".fact-node-names"]
|
||||||
|
if hosts == nil {
|
||||||
|
t.Fatalf("no names reached the machine; the declaration has %v", keys(ids))
|
||||||
|
}
|
||||||
|
if hosts["path"] != "/etc/hosts" || hosts["into"] != "block" {
|
||||||
|
t.Fatalf("the hosts file is not written into as a region: %v", hosts)
|
||||||
|
}
|
||||||
|
content := hosts["content"].(string)
|
||||||
|
if !strings.Contains(content, "10.42.0.1\thomer.internal\thomer\t# this machine\n") {
|
||||||
|
t.Errorf("the region does not name the machine:\n%s", content)
|
||||||
|
}
|
||||||
|
for _, floor := range []string{"Generated by the mesh", "localhost", "127.0.1.1"} {
|
||||||
|
if strings.Contains(content, floor) {
|
||||||
|
t.Errorf("the region carries %q, which is the machine's:\n%s", floor, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver's reference to it still names a resource the host will be sent.
|
||||||
|
daemon := ids["resolver.daemon"]
|
||||||
|
if daemon == nil {
|
||||||
|
t.Fatalf("the resolver's service was not composed: %v", keys(ids))
|
||||||
|
}
|
||||||
|
for _, named := range daemon["restart-on"].([]any) {
|
||||||
|
if ids[named.(string)] == nil {
|
||||||
|
t.Errorf("the resolver restarts on %v, which is nothing the host is sent", named)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(daemon["restart-on"], []any{"resolver.seed", overlay.Name + ".fact-node-names"}) {
|
||||||
|
t.Errorf("restart-on is %v", daemon["restart-on"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a resource's own `at` passes through as the manifest wrote it.
|
||||||
|
if seed := ids["resolver.seed"]; seed == nil || seed["at"] != "start" {
|
||||||
|
t.Errorf("a resource's at did not survive composition: %v", seed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(m map[string]map[string]any) []string {
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -47,6 +47,13 @@ var seats = []Seat{
|
|||||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||||
{Name: "the-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
{Name: "the-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||||
{Name: "the-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
{Name: "the-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||||
|
// The program that manages the machine's own network. It delivers nothing: its holder only
|
||||||
|
// keeps the manager and the mesh from contradicting each other — the resolver file left to the
|
||||||
|
// mesh, the private network's interface left alone — and never declares a link, an address or
|
||||||
|
// a wireless network, because the link is the only channel a fix could arrive on. A seat
|
||||||
|
// rather than a condition in the resolver's module, so a machine running two managers is
|
||||||
|
// refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117).
|
||||||
|
{Name: "the-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seats is every seat the mesh defines, in reading order.
|
// Seats is every seat the mesh defines, in reading order.
|
||||||
|
|||||||
@@ -44,12 +44,32 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|||||||
delivered[s.Delivers] = s.Name
|
delivered[s.Delivers] = s.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(Seats()) != 14 {
|
if len(Seats()) != 15 {
|
||||||
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
t.Errorf("the mesh defines %d seats rather than 15; the set is closed, so a change here is "+
|
||||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq ADR 0117: a machine's uplink is a seat, held per machine, and delivers nothing.
|
||||||
|
//
|
||||||
|
// **Nothing, because nothing may be required of it.** A holder only keeps its network manager from
|
||||||
|
// contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer
|
||||||
|
// for something, and the manager's link is the one thing the mesh must never be able to break.
|
||||||
|
func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
|
||||||
|
seat, known := SeatNamed("the-uplink")
|
||||||
|
if !known {
|
||||||
|
t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames())
|
||||||
|
}
|
||||||
|
if seat.Scope != ScopeNode || seat.Delivers != "" || seat.Decision != "novox/hq ADR 0117" {
|
||||||
|
t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat)
|
||||||
|
}
|
||||||
|
// And a manager's module can hold it without providing anything.
|
||||||
|
raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"the-uplink","scope":"node"}]}`)
|
||||||
|
if _, err := ParseManifest(raw); err != nil {
|
||||||
|
t.Fatalf("a network manager's module could not hold the uplink: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func claimed(claims string) []byte {
|
func claimed(claims string) []byte {
|
||||||
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`)
|
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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,
|
// 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
|
// (novox/hq ADR 0011). Written as a marked region *into* the file rather than as the file: the
|
||||||
// underneath something else.
|
// 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
|
// 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
|
// package, and has no failure mode of its own. A daemon becomes necessary when names are wanted
|
||||||
|
|||||||
Reference in New Issue
Block a user