diff --git a/go/catalogue/adoption.go b/go/catalogue/adoption.go index 72bc564..4420af4 100644 --- a/go/catalogue/adoption.go +++ b/go/catalogue/adoption.go @@ -7,13 +7,6 @@ import ( "strings" ) -// What an adopted node is declared in place of a filter (novox/hq ADR 0100). -// -// On an adopted node the firewall found on the machine stays in force: the mesh loads no table -// that drops by default or holds an accept. What the mesh needs reachable is declared as -// openings, which the host converges through the found firewall in its own terms; and the mesh -// guards its own foundation ports itself, in a table that only refuses. - // AdoptionPrefix is the id prefix of what the mesh declares of its own on an adopted node. It is // never a module's, so none of it is ever held as found. const AdoptionPrefix = "adoption." diff --git a/go/catalogue/adoption_test.go b/go/catalogue/adoption_test.go index a812651..48b8ad9 100644 --- a/go/catalogue/adoption_test.go +++ b/go/catalogue/adoption_test.go @@ -298,38 +298,6 @@ func TestAGivenPortIsUsedEverywhereThePortIs(t *testing.T) { } } -func TestAGivenPortIsTheNodesAndReachesSomething(t *testing.T) { - store := anAdoptedAnchor().Modules[1] - node := func(v any) []Layer { - return []Layer{{From: "anchor", Placement: map[string]any{PortsSetting: v}}} - } - if got, err := GivenPorts(store, node(map[string]any{"5432": float64(5433)})); err != nil || - got[5432] != 5433 { - t.Fatalf("a node's given port was not read: %v %v", got, err) - } - if _, err := GivenPorts(store, []Layer{{From: MeshWideLayer, - Placement: map[string]any{PortsSetting: map[string]any{"5432": float64(5433)}}}}); err == nil { - t.Fatal("a port given for the whole mesh was accepted") - } - if _, err := GivenPorts(store, node(map[string]any{"6000": float64(6001)})); err == nil { - t.Fatal("a port the module neither listens on, publishes nor guards was given") - } - if _, err := GivenPorts(store, node(map[string]any{"5432": float64(70000)})); err == nil { - t.Fatal("a machine port that is not a port was given") - } - if _, err := GivenPorts(store, node(map[string]any{"5432": float64(22)})); err == nil { - t.Fatal("ssh's port was given") - } - broker := anAdoptedAnchor().Modules[2] - if _, err := GivenPorts(broker, node(map[string]any{"5671": float64(5700), - "5672": float64(5700)})); err == nil { - t.Fatal("one machine port was given for two of the module's ports") - } - if stray := UnusedSettings(store, node(map[string]any{"5432": float64(5433)})); len(stray) != 0 { - t.Fatalf("a given port is called stray: %v", stray) - } -} - // novox/hq ADR 0103: the guard is derived, and from taken modules only — every machine port a taken // module publishes that the filter admits from the private network only, and the ports its // manifest guards. A module assigned but not taken is not guarded: its port may still be the diff --git a/go/catalogue/artifact_store_alias_test.go b/go/catalogue/artifact_store_alias_test.go deleted file mode 100644 index 761d476..0000000 --- a/go/catalogue/artifact_store_alias_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package catalogue - -import "testing" - -// A claim written before the rename still holds (novox/hq ADR 0122, ADR 0156): with the store's -// aliases loaded, the former name resolves to the seat. -func TestTheArtifactStoresFormerNameResolvesToIt(t *testing.T) { - was := aliases - t.Cleanup(func() { aliases = was }) - UseAliases(map[string]string{"the-artifact-store": "mesh-artifact-store"}) - seat, known := SeatNamed("the-artifact-store") - if !known || seat.Name != "mesh-artifact-store" || seat.Delivers != "artifact-store" { - t.Fatalf("the former name did not resolve: %+v %v", seat, known) - } - if _, known := SeatNamed("mesh-artifact-store"); !known { - t.Fatal("the seat is not in the set under its name") - } -} diff --git a/go/catalogue/artifact_sweep_seat.go b/go/catalogue/artifact_sweep_seat.go index cf8cefc..0add09f 100644 --- a/go/catalogue/artifact_sweep_seat.go +++ b/go/catalogue/artifact_sweep_seat.go @@ -1,11 +1,5 @@ package catalogue -// **The artifact sweep, out of the controller** (novox/hq ADR 0571 clause 2, issue 1955): what the artifact store keeps -// and lets go of, the seat `mesh-artifact-sweep` (named for its scope, ADR 0121 §1; the operator, 2026-10-29), held -// once for the mesh by artifact-store-tools, serves what the controller served of its store (artifacts, held, mirrors, images, collect) and says `artifacts-gone`. Its keep set is the store's own listing joined -// with every want's fingerprint and each module's five newest builds from the builder's log (ADR 0526 §1); it lets go -// of nothing until the controller's lease names a holder that is no controller. - // ArtifactSweepSeat is the seat artifact-store-tools holds. Not the seat mesh-artifact-store, the store itself held by // distribution, and not the provision artifact-store it delivers. const ArtifactSweepSeat = "mesh-artifact-sweep" diff --git a/go/catalogue/artifacts.go b/go/catalogue/artifacts.go index e9a010d..14b4f90 100644 --- a/go/catalogue/artifacts.go +++ b/go/catalogue/artifacts.go @@ -5,26 +5,6 @@ import ( "strings" ) -// What the mesh built, named by what it is rather than by where it was pushed. -// -// **An image is recorded by its digest and its path; the registry's address is a route to it** -// (novox/hq 04-ISSUES/102). A build used to be recorded as `://@sha256:…` -// — the reference the builder pushed to, kept whole — and every declaration carried that literal. -// Move the registry's port, or the registry, and every fresh pull of a mesh image fails: a new -// node, a recreate after eviction. The digest is the identity; the address is the node's setting -// for the module that serves the artifact store, and it is read from there when a reference is -// composed, never written into a record. -// -// So a kept reference has a scheme of the mesh's own, named after the provision that answers it: -// -// artifact-store:///@sha256: an image -// artifact-store:////blobs/sha256: an archive -// -// No runtime knows the scheme. That is the point of it being one: a reference that leaks to a -// machine uncomposed is refused by the runtime as malformed, in front of whoever sent it, rather -// than pulled from a public registry that happens to have a repository by that name — which is -// what an address-less `/@sha256:…` would be. - // ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without // the store's address. const ArtifactStoreScheme = ArtifactStoreProvision + "://" @@ -78,15 +58,6 @@ func Routed(reference, address string) string { return address + "/" + path } -// Rerouted is a reference the mesh recorded, whichever way it was recorded, as a machine fetches -// it now: a kept one composed with the store's address, and one recorded before references were -// kept without their address — the builder's own `/@sha256:…` — re-routed to where -// the store is now. Only for references that are the mesh's own: everything a build record -// holds is, by construction. -func Rerouted(reference, address string) string { - return Routed(Recorded(reference), address) -} - // artifactsInto composes the artifact store's address into a resource's `image` and `source`. // // **Composed here, at the last moment before a machine, and stored nowhere.** A kept reference is diff --git a/go/catalogue/artifacts_test.go b/go/catalogue/artifacts_test.go index 3d0ae70..c0ea515 100644 --- a/go/catalogue/artifacts_test.go +++ b/go/catalogue/artifacts_test.go @@ -28,22 +28,6 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { } } -func TestARecordedReferenceIsRoutedThroughTheStoreAsItIsNow(t *testing.T) { - if got := Routed(ArtifactStoreScheme+"gitea/server@"+digest, "anchor.internal:5101"); got != "anchor.internal:5101/gitea/server@"+digest { - t.Errorf("an image is fetched as %q", got) - } - if got := Routed(ArtifactStoreScheme+"gitea/config/blobs/"+digest, "anchor.internal:5101"); got != "http://anchor.internal:5101/v2/gitea/config/blobs/"+digest { - t.Errorf("an archive is fetched as %q", got) - } - if got := Routed("gitea/gitea@"+digest, "anchor.internal:5101"); got != "gitea/gitea@"+digest { - t.Errorf("a reference that is not the store's was routed: %q", got) - } - // One recorded before references were kept without their address follows the store too. - if got := Rerouted("anchor.internal:5100/gitea/server@"+digest, "anchor.internal:5101"); got != "anchor.internal:5101/gitea/server@"+digest { - t.Errorf("a reference recorded with the old address stays there: %q", got) - } -} - // **The address is composed into a declaration, and the record never carries it.** func TestAnImageTheMeshBuiltIsRoutedThroughTheStoreWhenDeclared(t *testing.T) { m := Manifest{Module: "gitea", Version: "1", Resources: []map[string]any{ diff --git a/go/catalogue/backup_placement.go b/go/catalogue/backup_placement.go index ddd3517..2e77dab 100644 --- a/go/catalogue/backup_placement.go +++ b/go/catalogue/backup_placement.go @@ -5,12 +5,6 @@ import ( "regexp" ) -// What the machine's modules back up is placement (design 57 L3, issue 794). The holder of node-backup names it as -// `${backup:lines}` (what to run and which paths to keep) and `${backup:data}` (every item with its class and -// protection): the text `${contribution:node-backup:backup}` and `${contribution:node-backup:data}` render, from the -// same function, derived from each placed module's declared data (novox/hq ADR 0233). A handed holder has the text in -// its plan's placement for its client to fill; one not handed has the controller fill it where it stands. - // BackupNamespace is the placeholder kind of the machine's backup lines and data. const BackupNamespace = "backup" diff --git a/go/catalogue/bound.go b/go/catalogue/bound.go index 12941a4..2f2a346 100644 --- a/go/catalogue/bound.go +++ b/go/catalogue/bound.go @@ -6,21 +6,6 @@ import ( "strings" ) -// A consumer of a provision that keeps its data stays bound where its data is (novox/hq ADR 0232). -// -// **What a resolution chooses is not where a consumer's data is.** Resolving answers "which provider -// would I pick now", from the seats' holders, the pins and what is assigned where, and every one of -// those can change under a consumer without anybody meaning to move it. For a resolver that is the -// point: any provider answers alike. For a database it is the consumer's whole state: on 2026-10-05 -// one change to how a seat's holder answers (issue 258) re-bound five database consumers on one -// machine to the store on another, each was given a fresh, empty database there, and nothing warned -// for twenty hours (issue 273). Nothing was lost only because the old provider kept everything. -// -// So the mesh records where each such consumer was last sent (the store's `binding` table), and a -// resolution that would answer it from anywhere else keeps the recorded provider instead and says so. -// **Only a pin moves it**, because a pin is a person: the one act that says "answer this machine's -// consumers from there", taken knowing the data must go first. - // KeptBinding is one consumer this resolution would have moved, and did not. type KeptBinding struct { // Machine is where the consumer runs, and Consumer the module there that is bound. diff --git a/go/catalogue/bound_into_files.go b/go/catalogue/bound_into_files.go index e04caad..70ff0b3 100644 --- a/go/catalogue/bound_into_files.go +++ b/go/catalogue/bound_into_files.go @@ -7,24 +7,6 @@ import ( "strings" ) -// The half of a connection that is not secret, put where the program reading it can find it. -// -// **The asymmetry this removes was backwards** (novox/hq 04-ISSUES/023). A sealed credential can -// be placed inside any configuration file a module writes: the module leaves a hole, the mesh -// delivers the value sealed beside it, and the host — the only thing that sees both — fills it in. -// The host and the port and the name to present are ordinary facts the mesh holds in the clear, -// and they were the ones stuck: readable only inside a JSON binding, which a program reading -// `KEY=value` cannot use. -// -// So the same shape, and simpler. These values are not secret, so **the control plane substitutes -// them itself** before the declaration is sent. Nothing new reaches the host, which learns no -// formats and gains no fields. -// -// **It stays name-agnostic** ([ADR 0027]). The mesh does not learn what a `postgres-database` is: -// `at`, `as`, `from` and `address` (the providing machine's private address, novox/hq ADR 0194) are -// facts about any provision at all, and everything else comes from what the provider said it -// serves — whose keys are agreed by the requirement's name, not by this file. - // bound is where a module says a value from one of its bindings belongs: // ${bound::}, and for an optional requirement the value it takes while absent, // ${bound::|default:} (novox/hq ADR 0330 §4). A default is never empty: an diff --git a/go/catalogue/build.go b/go/catalogue/build.go index 9dbce70..bc59665 100644 --- a/go/catalogue/build.go +++ b/go/catalogue/build.go @@ -7,17 +7,6 @@ import ( "strings" ) -// Turning a manifest that names artifacts into one that names digests. -// -// **Two documents, deliberately.** The manifest in a repository says *this resource uses the -// archive called `config`*; the manifest the mesh holds says *this resource is sha256:…*. A digest -// is not knowable until something is built, so a repository carrying one would be a repository -// whose file is wrong the moment anybody edits anything — and the mesh would be pinning a value -// nobody could have checked. -// -// So the built manifest is **derived**, and the record of which commit it was derived from is what -// makes "is this current?" answerable without building (novox/hq ADR 0009). - // Built is one artifact after it exists: where it is and what it hashes to. type Built struct { // Name is what the manifest called it. diff --git a/go/catalogue/busy.go b/go/catalogue/busy.go index bf56b3a..d7b81ab 100644 --- a/go/catalogue/busy.go +++ b/go/catalogue/busy.go @@ -7,18 +7,6 @@ import ( "time" ) -// A long-running container may say it is busy (novox/hq ADR 0335 §1). -// -// **Beside its health, on the container**: a command run inside it, by its shell, as an exec health check -// is, that exits 0 while the container holds work a replacement would lose and non-zero when it is idle; -// and how long one ask may take. Being asked is also its notice that it is about to be replaced: from the -// first ask it takes no new work. The node-engine asks before it replaces the container, leaves it running -// while it says busy, and replaces it on the first pass it says idle, or at 15 minutes — the engine's bound, -// never the module's. -// -// Sent only to a node-engine that reads it (link.BusyContract): an older, strict engine refuses a field it -// does not know, whole, and to it the container is replaced at once, as before. - // BusyField is the resource field. const BusyField = "busy" diff --git a/go/catalogue/checker_seat.go b/go/catalogue/checker_seat.go index e9a5821..a553dd5 100644 --- a/go/catalogue/checker_seat.go +++ b/go/catalogue/checker_seat.go @@ -1,10 +1,5 @@ package catalogue -// The delivery's checker (novox/hq ADR 0547 §10, issue 774): one holder for the mesh, the module -// mesh-delivery-checker, which judges a pull request's head and a pushed head the same way. The try the -// controller forwarded to it is a verb of this seat, answered by the checker with the verdict it gives a pull -// request, so the try outlives the controller. - // CheckerSeat is the seat mesh-delivery-checker holds. const CheckerSeat = "mesh-delivery-checker" diff --git a/go/catalogue/collections.go b/go/catalogue/collections.go index 3dffb3a..178bcfb 100644 --- a/go/catalogue/collections.go +++ b/go/catalogue/collections.go @@ -10,28 +10,6 @@ import ( "strings" ) -// Collections (novox/hq ADR 0349 clause 13, design 57 L1, issue 684): a provision with many providers. -// -// **A collector requires a collection; a contributor provides an item from a template.** A module taking additions -// from others names a collection in a file of its own, `${collected:}`, and declares it under -// `collections`, with the comment its tool writes a line in. Each contributor ships its item as a template of its -// own repository and declares it under `items`: `{"collection": …, "template": "files/"}`. Its build packs the -// template with the module's others, and the mesh holds its text as it holds a file's (template.go). -// -// **The node's provisioner client fills both.** An item is filled on the node by the client of the node its -// contributor is placed on, from the same owners as a file, and recorded in mesh-provisioner_collect; a collector's -// file is filled by the same client from every item of its collection recorded for that node, so an item goes when -// its contributor leaves. So a module contributing an item is a moved module (handed.go), and the controller renders -// no item: the plan of a contributor names its items and their templates, and the plan of a collector names each -// collection it reads and its comment. Neither carries a contributor's text. -// -// **The three old namespaces read as collections.** `${shell::}`, `${environment:}` and -// `${contribution::}` name, in a handed collector's file, the collection of the same name — -// `shell:zsh:normal` — with the comment the controller wrote before each module's piece. An item is contributed to -// one by that name, by a module the old form would have let contribute it. Each file is filled one way, never -// both (ADR 0349 clause 17): a handed collector is filled from items alone and an unhanded one from the old forms -// alone, and a piece of the other form on the same machine is said as not placed, never dropped quietly. - // Item is one item a module contributes to a collection: its template in the module's repository, or, in the // manifest the mesh holds, the template's text, read back from the build's templates archive like a file's. // @@ -222,21 +200,6 @@ func oldCollection(name string, forItem bool) (comment string, old bool, wrong e return r.Comment, true, nil } -// placerOfOld is the seat whose holder alone may collect an old namespace's collection (ADR 0203, 0204, 0208, -// 0211, 0212): the same seat that places it the old way. -func placerOfOld(name string) string { - namespace, key, _ := strings.Cut(name, ":") - switch namespace { - case "shell": - target, _, _ := strings.Cut(key, ":") - return placerOf(target) - case "environment": - return EnvironmentSeat - } - seat, _, _ := strings.Cut(key, ":") - return canonicalSeat(seat) -} - // itemTemplateProblem is what is wrong with an item's template path, or "". func itemTemplateProblem(m Manifest, name string) string { dir := m.TemplatesDir() @@ -299,13 +262,6 @@ func collectionComment(m Manifest, name string) (string, error) { return "", fmt.Errorf("%s names ${collected:%s} and does not collect it", m.Module, name) } -// itemsLeftOut says each item of a collection a file is filled with the old way that the controller could not render -// there (heldItemText): the collector is not handed to the provisioner on this machine (ADR 0349 clause 17). Said, -// never dropped quietly. -func itemsLeftOut(modules []Manifest, holder Manifest, collection string, facts map[string]string) []string { - return itemsLeftOutFor(modules, holder, collection, nil, facts, nil) -} - // itemsLeftOutFor is itemsLeftOut for a placeholder that selects by attributes (where), on a machine with these // capabilities: an item the selector does not take, or whose capability the machine lacks, is not left out but not // wanted here. @@ -358,22 +314,6 @@ func oldFormsLeftOut(modules []Manifest, holder Manifest, collections []PlanColl return out } -// heldItemText is a moved contributor's items of an old namespace's collection as the controller renders them into a -// collector not handed on this machine (design 57 L2, issue 681): the text the mesh holds, read back from the -// contributor's templates archive, with the machine's facts filled, each ending its line. As a moved module's file -// keeps its text for a machine that cannot take it handed (handed.go), an item keeps its place in a collector filled -// the old way, so the file stays what its old form made it. Only `shell:` and `contribution:` collections, which the -// controller renders as text; false for a module with no such item, or an item whose text the mesh does not hold or -// which names anything but a machine fact this machine has, which is then said as not placed. -func heldItemText(m Manifest, collection string, facts map[string]string) (string, bool) { - items := heldItems(m, collection, nil, facts, nil) - var b strings.Builder - for _, h := range items { - b.WriteString(h.text) - } - return b.String(), len(items) > 0 -} - // renderedOldWay is whether the controller renders a collection's items into a collector not handed. func renderedOldWay(collection string) bool { namespace, _, _ := strings.Cut(collection, ":") diff --git a/go/catalogue/collections_test.go b/go/catalogue/collections_test.go index f28c48c..c0172b1 100644 --- a/go/catalogue/collections_test.go +++ b/go/catalogue/collections_test.go @@ -94,50 +94,3 @@ func TestACollectorFilledTheOldWayHoldsTheOldFormsAndEachHeldItem(t *testing.T) t.Fatalf("unplaced: %v", c.Unplaced) } } - -// An item the controller cannot render the old way (it names a value but a machine fact) is said as not placed. -func TestAHeldItemNamingMoreThanAFactIsSaidNotPlacedTheOldWay(t *testing.T) { - zsh := held(t, collectorZsh, builtWith("zsh", map[string]string{"zshrc": zshrcText})) - if _, ok := heldItemText(Manifest{Module: "p", Items: []Item{{Collection: "shell:zsh:normal", - Content: "export THEME=${config:theme}\n"}}}, "shell:zsh:normal", map[string]string{}); ok { - t.Fatal("an item naming a config value was rendered by the controller") - } - if _, ok := heldItemText(Manifest{Module: "p", Items: []Item{{Collection: "shell:zsh:normal", - Content: "cd ${machine:account-home}\n"}}}, "shell:zsh:normal", map[string]string{}); ok { - t.Fatal("an item naming a fact this machine lacks was rendered") - } - if _, ok := heldItemText(Manifest{Module: "p", Items: []Item{{Collection: "widgets", Content: "x\n"}}}, - "widgets", nil); ok { - t.Fatal("an item of a collection of the new form was rendered the old way") - } - left := itemsLeftOut([]Manifest{zsh, {Module: "p", Items: []Item{{Collection: "shell:zsh:normal", - Content: "export THEME=${config:theme}\n"}}}}, zsh, "shell:zsh:normal", map[string]string{}) - if len(left) != 1 || !strings.Contains(left[0], "p's item of shell:zsh:normal is not placed") { - t.Fatalf("left out: %v", left) - } -} - -// Each old namespace L2 moves renders a moved contributor's item exactly as its old form did, where the collector is -// not handed: the same contributor written both ways composes the same bytes (design 57 L2, issue 681). -func TestAMovedContributorsItemRendersAsItsOldFormDid(t *testing.T) { - code := "# two lines\nalias a=b" - asOld := Manifest{Module: "laptop", Shell: []ShellCode{{For: "zsh", Slot: "normal", Code: code}}, - Contributions: []SeatContribution{{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_F21 1 touchpad\n"}}} - asItem := Manifest{Module: "laptop", Moved: true, Items: []Item{{Collection: "shell:zsh:normal", Content: code}, - {Collection: "contribution:node-hotkeys:trigger", Content: "KEY_F21 1 touchpad\n"}}} - other := Manifest{Module: "zz-other", Shell: []ShellCode{{For: "zsh", Slot: "normal", Code: "x=1\n"}}, - Contributions: []SeatContribution{{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_MUTE 1 mute\n"}}} - first := Manifest{Module: "aa-first", Shell: []ShellCode{{For: "zsh", Slot: "normal", Code: "y=2\n"}}} - for _, mods := range [][]Manifest{{other, first}, {}} { - was := append(append([]Manifest(nil), mods...), asOld) - is := append(append([]Manifest(nil), mods...), asItem) - if a, b := shellCodeOn(was, "zsh", "normal", nil), shellCodeOn(is, "zsh", "normal", nil); a != b || a == "" { - t.Errorf("shell: the old form made %q, the item %q", a, b) - } - a, _, errA := seatContributions(was, hotkeysHolder(), "node-hotkeys:trigger", Rendering{}, nil, nil) - b, left, errB := seatContributions(is, hotkeysHolder(), "node-hotkeys:trigger", Rendering{}, nil, nil) - if errA != nil || errB != nil || a != b || a == "" || len(left) != 0 { - t.Errorf("hotkeys: the old form made %q, the item %q (%v %v %v)", a, b, errA, errB, left) - } - } -} diff --git a/go/catalogue/config.go b/go/catalogue/config.go index 4d7c1fd..9341b4b 100644 --- a/go/catalogue/config.go +++ b/go/catalogue/config.go @@ -7,28 +7,6 @@ import ( "slices" ) -// A module declares its config (novox/hq ADR 0348 §1, §4, §5, §6, §8, §11; design 57 package K1, issue 660). -// -// **One concept, two spellings for a while.** "Config" replaces "settings": the manifest says `config` for -// `settings`, `takes-config` for `takes-settings` and `${config:}` for `${setting:}`. The old names stay -// accepted as aliases until every module has moved (ADR 0348 §1), and a module uses one form or the other, never -// both. The config form is read into the very fields the settings form fills — Settings, a mergeable file's -// takes-settings and `${setting:}` — so composition has one path and `${config:}` fills exactly as -// `${setting:}` does: a module rewritten from settings to config composes byte-identically. It is written -// back in the form it was read in, so what the mesh holds is what its author wrote. -// -// **A moved module is held to the whole declaration.** Under `config` every value has a type (number, string or -// boolean; an object or a list only when trusted), a description including its unit, its validation (enum, -// minimum and maximum, pattern), whether it is trusted, optionally the predefined key it falls back to, and either -// a default or `required`; it lands in a file or is read at runtime (`runtime: true`); every mergeable file names -// the keys it takes; a `reload-on` or `restart-on` names a file the module declares. A module not yet moved keeps -// today's form and today's rules (SettingProblems, TakenSettingsProblems) until the catalogue's list of such -// modules is empty (ADR 0348 §11). -// -// **A required value nothing sets** is the operator's own: it composes as an operator's setting nobody gave does -// today, refused by name at composition (UnsetSettingError). Who reports it once mesh-config answers values is the -// provisioner's (ADR 0349), not this file's. - // ConfigField is the manifest key a module declares its config values under. const ConfigField = "config" @@ -150,22 +128,6 @@ func (d SettingDeclaration) MarshalJSON() ([]byte, error) { Default: d.Default, Required: d.Required, Runtime: d.Runtime}) } -// renamedTaken is every value a mergeable file takes under a name of its own, with that name (issue 751). -func renamedTaken(m Manifest) map[string]string { - out := map[string]string{} - for _, r := range m.Resources { - if how, _ := r["merge"].(string); how == "" { - continue - } - for _, e := range takenEntries(r[TakesSettingsField]) { - if e.renamed { - out[e.key] = e.as - } - } - } - return out -} - // validationProblems is what is wrong with a declaration's validation, and its default against it. func validationProblems(d SettingDeclaration) []string { var problems []string diff --git a/go/catalogue/config_move.go b/go/catalogue/config_move.go index e902b91..75be4ea 100644 --- a/go/catalogue/config_move.go +++ b/go/catalogue/config_move.go @@ -2,29 +2,6 @@ package catalogue import "strings" -// What the move of config reads of a module's declaration (novox/hq ADR 0348 §9, design 57 K5, issue 664): where a -// layer's key goes, how two layers' values become one, and whether a value is one its declaration takes. - -// TakenAs is each name a mergeable file takes a config value under that is not the value's config key, with that -// key (issue 751): a layer set before the module moved may still say the file's name, and the move sets it under -// the config key. -func TakenAs(m Manifest) map[string]string { - out := map[string]string{} - for key, as := range renamedTaken(m) { - out[as] = key - } - return out -} - -// AskedKeys is every value a placeholder of the module asks for: a file, a unit, a directory's owner, a -// contribution or a served fact. Such a value is taken whole from the last layer that sets it, where a mergeable -// file lays one object over another key by key. -func AskedKeys(m Manifest) map[string]bool { return settingKeysUsedBy(m) } - -// LaidOver is a value laid over another as a later layer is laid over an earlier: two objects key by key, anything -// else replaced whole. -func LaidOver(under, over any) any { return mergeValue(under, over) } - // ValueProblems is what is wrong with a value for a declaration: its type, and its enum, bounds or pattern. func ValueProblems(d SettingDeclaration, v any) []string { if v == nil { diff --git a/go/catalogue/config_rename_test.go b/go/catalogue/config_rename_test.go index f97ec6d..6efb5a7 100644 --- a/go/catalogue/config_rename_test.go +++ b/go/catalogue/config_rename_test.go @@ -83,45 +83,6 @@ func TestAnUnsetRenamedValueLeavesTheFileAsItWas(t *testing.T) { } } -// A layer said before its module moved names the file's own spelling, such as nodered's timeZone on one machine. -// Until K5 moves the layer and renames the key, that spelling still reaches the file, so the move changes no -// machine's file; the config key wins where one layer says both. -func TestALayerStillSayingTheFilesNameReachesItUntilItsMove(t *testing.T) { - m, err := parseRaw(t, renaming()) - if err != nil { - t.Fatal(err) - } - for _, said := range []string{"url", "NOTIFIER_URL"} { - if stray := UnusedSettings(m, []Layer{{From: "ace", Values: map[string]any{said: "https://x.test"}}}); len(stray) > 0 { - t.Fatalf("%s, which the rules file takes, is said stray: %v", said, stray) - } - got := rulesComposed(t, m, Layer{From: "ace", Values: map[string]any{said: "https://x.test"}}) - var doc map[string]any - if err := json.Unmarshal([]byte(got), &doc); err != nil { - t.Fatal(err) - } - if doc["NOTIFIER_URL"] != "https://x.test" || doc["url"] != nil { - t.Fatalf("a layer saying %s made the file %s", said, got) - } - } - both := rulesComposed(t, m, Layer{From: "ace", Values: map[string]any{"url": "https://key.test", "NOTIFIER_URL": "https://old.test"}}) - if !strings.Contains(both, "https://key.test") || strings.Contains(both, "old.test") { - t.Fatalf("where one layer says both, the config key did not win: %s", both) - } - later := rulesComposed(t, m, Layer{From: "mesh", Values: map[string]any{"url": "https://mesh.test"}}, - Layer{From: "ace", Values: map[string]any{"NOTIFIER_URL": "https://ace.test"}}) - if !strings.Contains(later, "https://ace.test") { - t.Fatalf("a later layer's old spelling did not beat an earlier layer's key: %s", later) - } - // Who may set it is the same under either spelling: the rules file is trusted, so both are the terminal's. - terminal := TerminalKeys(m) - for _, k := range []string{"url", "NOTIFIER_URL"} { - if !listHolds(terminal, k) { - t.Fatalf("%s is not the terminal's in a trusted file: %v", k, terminal) - } - } -} - func listHolds(list []string, s string) bool { for _, x := range list { if x == s { diff --git a/go/catalogue/consumer_into_serves.go b/go/catalogue/consumer_into_serves.go index 441a77d..2923fa5 100644 --- a/go/catalogue/consumer_into_serves.go +++ b/go/catalogue/consumer_into_serves.go @@ -7,21 +7,6 @@ import ( "strings" ) -// What a provider derives for one consumer, said once in the provider's definition and delivered -// to both ends (novox/hq ADR 0202, issue 124). -// -// A `serves` block is otherwise literal: the same values for every consumer. Where the provider -// *names the resource* — a bucket, a database, a vhost — the name is derived from who is asking, -// and before this the mesh had no channel for it. The provider recomputed it in its own code and -// every consumer transcribed it into its own definition by hand, which is a copy of somebody -// else's rule kept in agreement by nobody. One of three transcriptions was wrong for months. -// -// **The mesh learns no protocol here; it spells its own name in an alphabet it already knows.** -// The only fact a served value may name is the identity the mesh itself minted for the consumer, -// in one of two alphabets: as it was minted, and as a DNS label. Everything a provider wants -// around it — a prefix, a suffix, a separator — it writes around the placeholder, because a -// served value is a string. - // consumerFact is `${consumer:}` or `${consumer::}`. var consumerFact = regexp.MustCompile(`\$\{consumer:([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\}`) @@ -124,15 +109,6 @@ func asDNSLabel(as string) string { return strings.ReplaceAll(as, "_", "-") } -func sortedServes(serves map[string]map[string]any) []string { - out := make([]string, 0, len(serves)) - for k := range serves { - out = append(out, k) - } - sort.Strings(out) - return out -} - func sortedAnyKeys(values map[string]any) []string { out := make([]string, 0, len(values)) for k := range values { diff --git a/go/catalogue/consumers.go b/go/catalogue/consumers.go index 46a64e0..6ad2e69 100644 --- a/go/catalogue/consumers.go +++ b/go/catalogue/consumers.go @@ -6,15 +6,6 @@ import ( "slices" ) -// A provider's consumers' credentials, in a file of the provider's own (novox/hq ADR 0373, issue 679). -// -// A provider in the tool runner may not ask the vault (ADR 0349 clause 11, ADR 0370 clause 2), so its template names -// `${consumers:}` and its machine's provisioner client fills it: one entry per consumer bound to the -// provider for that provision, each carrying the credential the vault keeps for the pair. Only the client fills it, -// so the placeholder is a provider's alone, for a provision it provides, in a file only the provider reads (mode 0600 -// or 0400, owned at runtime, so a change on disk is written back); and a module not handed to the client on a machine -// is refused there, never written the placeholder's text, nor an empty list the provider would read as no consumers. - // ConsumersNamespace is a provider's consumers' credentials' placeholder namespace, `${consumers:}`. const ConsumersNamespace = "consumers" diff --git a/go/catalogue/contribution_dependencies_test.go b/go/catalogue/contribution_dependencies_test.go index dc8cafd..39ad0a9 100644 --- a/go/catalogue/contribution_dependencies_test.go +++ b/go/catalogue/contribution_dependencies_test.go @@ -2,7 +2,6 @@ package catalogue import ( "reflect" - "strings" "testing" ) @@ -44,27 +43,6 @@ func TestAContributionDependsOnTheSeatThatPlacesIt(t *testing.T) { } } -func TestAContributionIsMetByAHolderOnTheNodeAndRefusedWithout(t *testing.T) { - holder := mod("node-env", nil, nil, nil, Claim{Name: EnvironmentSeat}) - contributor := mod("theme", nil, nil, nil) - contributor.Environment = &Environment{Variables: map[string]string{"GTK_THEME": "Adwaita:dark"}} - catalogue := map[string]Manifest{"node-env": holder, "theme": contributor} - - if _, err := AssignRefusal(catalogue, "laptop", []string{"node-env"}, []string{"theme"}); err != nil { - t.Fatalf("a contributor beside the holder is refused: %v", err) - } - _, err := AssignRefusal(catalogue, "laptop", nil, []string{"theme"}) - if err == nil { - t.Fatal("a contributor on a node without the holder was accepted, and its contribution would be written nowhere") - } - if !strings.Contains(err.Error(), EnvironmentSeat) || !strings.Contains(err.Error(), "node-env") { - t.Errorf("the refusal names neither the seat nor its holder: %v", err) - } - if _, err := AssignRefusal(catalogue, "laptop", nil, []string{"theme", "node-env"}); err != nil { - t.Errorf("the contributor and the holder assigned together are refused: %v", err) - } -} - func TestAHolderMeetsItsOwnContribution(t *testing.T) { // The display server's module contributes nothing to its own seat today, but the zsh module's // environment contributions do go to another seat: a claim meets only the seat it names. @@ -78,38 +56,3 @@ func TestAHolderMeetsItsOwnContribution(t *testing.T) { t.Errorf("zsh alone: unheld %v, want only %s (its shell code is its own seat's)", unheld, EnvironmentSeat) } } - -func TestTwoModulesMayDeclareOnePackage(t *testing.T) { - pacman := withResources(mod("pacman", nil, nil, nil, Claim{Name: PackageManagerSeat}), - res("package", "pacman-contrib")) - bar := withResources(mod("bar", nil, nil, nil), res("package", "bar"), res("package", "pacman-contrib")) - catalogue := map[string]Manifest{"pacman": pacman, "bar": bar} - if err := CollisionRefusal(catalogue, "laptop", []string{"pacman"}, []string{"bar"}); err != nil { - t.Fatalf("a second declaration of a package was refused (novox/hq ADR 0303): %v", err) - } -} - -func TestAPackageDeclaredPresentAndAbsentIsRefusedNamingBoth(t *testing.T) { - gone := res("package", "ufw") - gone["absent"] = true - filter := withResources(mod("nftables", nil, nil, nil), gone) - wants := withResources(mod("legacy", nil, nil, nil), res("package", "ufw")) - other := withResources(mod("other", nil, nil, nil), res("package", "other")) - catalogue := map[string]Manifest{"nftables": filter, "legacy": wants, "other": other} - - err := CollisionRefusal(catalogue, "laptop", []string{"nftables"}, []string{"legacy"}) - if err == nil || !strings.Contains(err.Error(), "ufw") || !strings.Contains(err.Error(), "nftables") || - !strings.Contains(err.Error(), "legacy") || !strings.Contains(err.Error(), "ADR 0303") { - t.Fatalf("a package declared present and absent was not refused naming both: %v", err) - } - if strings.Contains(err.Error(), "one owner per node") { - t.Errorf("the refusal gives the one-owner rule, which no longer holds for packages: %v", err) - } - if err := CollisionRefusal(catalogue, "laptop", []string{"nftables"}, []string{"other"}); err != nil { - t.Errorf("a module declaring nothing contradictory is refused: %v", err) - } - // A contradiction already on the node is status's, not a reason to refuse an unrelated assignment. - if err := CollisionRefusal(catalogue, "laptop", []string{"nftables", "legacy"}, []string{"other"}); err != nil { - t.Errorf("an unrelated assignment is refused for a contradiction already there: %v", err) - } -} diff --git a/go/catalogue/control_records_seats.go b/go/catalogue/control_records_seats.go index b82b6eb..67c754f 100644 --- a/go/catalogue/control_records_seats.go +++ b/go/catalogue/control_records_seats.go @@ -1,14 +1,5 @@ package catalogue -// **The control records, out of the controller** (novox/hq ADR 0565 clause 1, issue 1953): two seats at mesh scope, -// each held once, and the node seat's verb `packages`. -// -// - mesh-wants, held by the assigner (mesh-delivery-assigner): a person's upgrade policy and the bus step (clauses 7, -// 8, 9); -// - mesh-hand-acts, held by the module mesh-hand-acts: the hand-act log and the warrants recorded (clause 10). -// -// Their events are their holders' to say on the seat; their layouts are go/handacts' and go/delivery's. - // The two seats. const ( WantsSeat = "mesh-wants" diff --git a/go/catalogue/controller_dependence.go b/go/catalogue/controller_dependence.go index afcb9cf..8c598b0 100644 --- a/go/catalogue/controller_dependence.go +++ b/go/catalogue/controller_dependence.go @@ -7,12 +7,6 @@ import ( "strings" ) -// **Nothing may name the controller** (novox/hq ADR 0590 clause 5, issue 1126): the controller ended by its unassignment -// on 2026-11-02 and its code left the mesh, so a manifest that reads one of its states (its lease too), invokes one of its -// verbs or tools, consumes or waits on one of its events, uses, claims or requires it, or is it, depends on nothing that -// answers. ManifestRefusals runs the check over a repository's manifests, in the test each repository of manifests keeps -// (mesh-catalog, mesh-delivery, mesh-tools); a later decision that places a controller again must first lift it. - // EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it. const EndedControllerName = "mesh-controller" diff --git a/go/catalogue/core_credentials.go b/go/catalogue/core_credentials.go index f11b70c..1b43550 100644 --- a/go/catalogue/core_credentials.go +++ b/go/catalogue/core_credentials.go @@ -5,11 +5,6 @@ import ( "slices" ) -// The core's own credentials (novox/hq ADR 0349 §16, issue 755). The bus, the node-engine, the controller and -// mesh-provisioner need their files before any provisioner client runs, so their files stay placement and their -// credentials travel in the declaration as values the vault sealed to the machine they run on, which the controller -// carries and cannot open; the node-engine opens them there (mesh-host's VaultBoxContract). - // CoreCredentialModules are the modules whose own secrets are the core's credentials: the bus, mesh-provisioner and // its client. The node-engine and the controller hold no own secret of a module's. **Fixed here, in the controller's // code, as the clause says** — and the vault keeps the same list (mesh-vault's CoreModules), answering the controller diff --git a/go/catalogue/cron.go b/go/catalogue/cron.go deleted file mode 100644 index 9fa7ce1..0000000 --- a/go/catalogue/cron.go +++ /dev/null @@ -1,15 +0,0 @@ -package catalogue - -// A five-field cron validator, enough to refuse a malformed schedule near its author -// (novox/hq ADR 0053). -// -// **Validation only, and written rather than pulled in.** The control plane's part in a scheduled -// step is small and exact: carry the field to the host unchanged, and refuse a `schedule` that is -// not a well-formed cron here rather than on the machine — the same near-versus-far discipline the -// manifest keeps for an action a module may not declare and a run-once that is not a boolean. What -// *when to run it again* means is the host's to evaluate; the control plane only checks the string -// is one it could. A cron library would be a dependency carried for evaluation nobody does here. -// -// The ordinary five fields — minute, hour, day-of-month, month, day-of-week — with `*`, lists -// (`,`), ranges (`-`) and steps (`/`). No names (jan, mon): a numeric cron is what the host -// evaluates, and refusing a name here is refusing it near whoever wrote it. diff --git a/go/catalogue/data.go b/go/catalogue/data.go index 86ddf79..3d1719b 100644 --- a/go/catalogue/data.go +++ b/go/catalogue/data.go @@ -10,24 +10,6 @@ import ( "time" ) -// A module declares the data it holds, and the mesh protects and watches it from that declaration -// (novox/hq ADR 0233). -// -// **One section, `data`, for every kind of data a module keeps:** its own, by directory — a store's -// files, a mail spool, an application's uploads — or by an operator's path it was given; what it keeps -// for its consumers, by the provision they reach it through; and what of its own lives with a -// provider, by the provision it requires. Each entry has a class — how precious it is — and, for its -// own data, how it is protected; everything the mesh does is derived from those, never written per -// module: -// -// - a binding to a consumer's data does not move (ADR 0232) where the provision's class keeps data; -// - the backup holder's lines are composed from it — a module no longer writes them by hand; -// - what an unassignment leaves behind of an irreplaceable or valuable item is retired, listed by -// `cleanup list` and deleted only by `cleanup delete` (ADR 0230); -// - the self-check measures every item and says when one shrinks, disappears, stops being written, -// goes without its backup, sits on a degraded array, or is replaced by an empty copy of itself — -// urgent for what is irreplaceable, a warning for what is valuable. - // The classes: how precious the data is. A fixed vocabulary, ranked by the operator (2026-10-06): a // class is what the protections are derived from, so a new one is a decision, not a manifest's choice. const ( @@ -225,10 +207,6 @@ func (it DataItem) MeasuredBy() string { return it.Measure } -// OwnedByModule is whether the item is in one of the module's own directories — the mesh's to retire — -// rather than an operator's path it was given, which the mesh never retires and never deletes. -func (it DataItem) OwnedByModule() bool { return strings.HasPrefix(it.Path, "${dir:") } - // BackupWithin is the item's bound on its last good backup. func (it DataItem) BackupWithin() time.Duration { if d, err := ParseDataDuration(it.Within); err == nil && d > 0 { @@ -290,15 +268,6 @@ func (m Manifest) ConsumerDataOf(provision string) (ConsumerData, bool) { return c, ok } -// KeptByOf is how precious what this module keeps with a provision's provider is, and whether it says. -func (m Manifest) KeptByOf(provision string) (KeptData, bool) { - if m.Data == nil { - return KeptData{}, false - } - k, ok := m.Data.KeptBy[provision] - return k, ok -} - // keepsByClass is whether a class keeps something a binding must not move away from. func keepsByClass(class string) bool { return class == ClassIrreplaceable || class == ClassValuable || class == ClassRebuildable @@ -339,8 +308,6 @@ func (m Manifest) NeedsBackupHolder() bool { return false } -// --- derived: the backup holder's lines --------------------------------------------------------- - // The node-backup seat's kinds (novox/hq to-be 43, ADR 0233): `backup` is what to run and which paths // to keep, `data` every item with its class and protection, for the holder to measure and watch. const ( @@ -417,8 +384,6 @@ func (m Manifest) allContributions() []SeatContribution { return append(out, derived...) } -// --- the catalogue check ------------------------------------------------------------------------ - // volumeMount is a container's volume, `:[:]`, its source possibly a // `${dir:}` — whose own colon is not the separator. var volumeMount = regexp.MustCompile(`^(\$\{(?:dir|access):[a-z0-9][a-z0-9-]*\}[^:]*|[^:]+):([^:]+)(?::(.*))?$`) diff --git a/go/catalogue/data_placed.go b/go/catalogue/data_placed.go index 0744fe6..673c655 100644 --- a/go/catalogue/data_placed.go +++ b/go/catalogue/data_placed.go @@ -6,12 +6,6 @@ import ( "strings" ) -// **The data items of a node's modules, placed, read from the composition** (novox/hq ADR 0571 clause 3, ADR 0233 §22 to -// §25): disk-load measures each item every module on its node declares, and the node-backup holder, mounts and nfs-server -// read the readings. Each item is its module's declaration with its path resolved to this node's place, its protection -// and how it is measured: the same lines the node-backup holder is handed as `${backup:data}`, composed by the same -// function, so a reader that composes from the bus and a holder handed the file read one list. - // PlacedDataItem is one data item a module on a node declares, as placed on that node. type PlacedDataItem struct { Module string `json:"module"` diff --git a/go/catalogue/data_test.go b/go/catalogue/data_test.go index a3e98d7..7cfdde9 100644 --- a/go/catalogue/data_test.go +++ b/go/catalogue/data_test.go @@ -1,7 +1,6 @@ package catalogue import ( - "slices" "testing" ) @@ -56,63 +55,3 @@ func TestKeepingConsumerDataFollowsTheClass(t *testing.T) { t.Fatal("an older definition that grants no longer keeps its consumers' data") } } - -// The backup holder's lines are derived: the dump runs and the directory it writes into is kept, -// anything but a cache is copied by default — the standard nightly plan — an item on redundancy is not -// copied, and every item is listed with its class and protection for the holder to measure and watch: -// directories filled, a home directory filled from the machine, an operator's path from where the -// assignment placed it. A backup line still written by hand beside a data section is not placed. -func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) { - pg := mustParse(t, declaredStore) - pg.Contributions = append(pg.Contributions, SeatContribution{Seat: BackupSeat, Kind: "backup", Content: "path /stale"}) - agent := mustParse(t, `{"module":"agent","version":"1", - "data":{"own":[{"id":"home","path":"${dir:home}","class":"valuable"},{"id":"tmp","path":"${dir:home}/tmp","class":"cache"}]}, - "resources":[{"id":"home","type":"directory","path":"${machine:account-home}/.agent","mode":"0700"}]}`) - media := mustParse(t, `{"module":"media","version":"1", - "accesses":[{"id":"films","mode":"read"}], - "data":{"own":[{"id":"films","path":"${access:films}","class":"irreplaceable","redundancy":"on a redundant array; no room for a copy","measure":"dataset"}, - {"id":"meta","path":"${dir:meta}","class":"rebuildable"}]}, - "resources":[{"id":"meta","type":"directory","mode":"0700"}]}`) - facts := map[string]string{"account-home": "/home/op"} - with := Rendering{Settings: SettingsBy{"media": {LayerOf("node", map[string]any{AccessesSetting: map[string]any{"films": "/tank/films"}})}}} - backup, _, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindBackup, with, facts, nil) - if err != nil { - t.Fatal(err) - } - want := "# agent\npath /home/op/.agent\n# media\npath /var/lib/media/meta\n# pg\nrun dump-all > /srv/store/dumps/all\npath /srv/store/dumps\n" - if backup != want { - t.Fatalf("backup lines:\n%s\nwant:\n%s", backup, want) - } - data, _, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindData, with, facts, nil) - if err != nil { - t.Fatal(err) - } - want = "# agent\nitem home valuable /home/op/.agent /home/op/.agent backup walk\nitem tmp cache /home/op/.agent/tmp - none walk\n" + - "# media\nitem films irreplaceable /tank/films - redundancy dataset\nitem meta rebuildable /var/lib/media/meta /var/lib/media/meta backup walk\n" + - "# pg\nitem store irreplaceable /srv/store /srv/store/dumps backup walk\nitem dumps rebuildable /srv/store/dumps - none walk\n" - if data != want { - t.Fatalf("data lines:\n%s\nwant:\n%s", data, want) - } - // ADR 0571 clause 3: the same text, read into items, as disk-load and the backup holder read it; a path a shell - // would split is refused. - items, err := ParseDataItems(data) - if err != nil || len(items) != 3 || items["media"][0] != (PlacedDataItem{Module: "media", ID: "films", Class: "irreplaceable", - Path: "/tank/films", Protection: "redundancy", Measure: MeasureDataset}) || items["pg"][0].CoveredBy != "/srv/store/dumps" { - t.Fatalf("the data lines read as %+v: %v", items, err) - } - if _, err := ParseDataItems("# a\nitem x cache /a b - none walk\n"); err == nil { - t.Fatal("a path with a space was read") - } - if _, _, err := seatContributions([]Manifest{agent}, Manifest{}, BackupSeat+":"+BackupKindData, Rendering{}, nil, nil); err == nil { - t.Fatal("a home directory with no account on the machine reached the holder as a placeholder") - } - // What keeps something irreplaceable depends on the machine's backup holder — it backs it up or - // watches its array; valuable data alone is backed up where a holder is, and refused nowhere. - if !slices.Contains(DependsOn(nil, media), BackupSeat) || !slices.Contains(DependsOn(nil, pg), BackupSeat) || - slices.Contains(DependsOn(nil, agent), BackupSeat) { - t.Fatalf("depends: media %v, pg %v, agent %v", DependsOn(nil, media), DependsOn(nil, pg), DependsOn(nil, agent)) - } - if it, _ := media.DataItem("films"); it.OwnedByModule() || it.Protection() != "redundancy" { - t.Fatalf("an operator's path reads %+v", it) - } -} diff --git a/go/catalogue/declaration.go b/go/catalogue/declaration.go index 8360b98..4788692 100644 --- a/go/catalogue/declaration.go +++ b/go/catalogue/declaration.go @@ -1,13 +1,5 @@ package catalogue -// Turning a resolution into the declaration a node is sent. -// -// Separate from resolving because they answer different questions. Resolving asks *what should -// this machine run*; this asks *what does that look like as resources*, and the second is where -// settings are applied, generators are called, contributions are collected and credentials are -// placed. Both lived in one file until it was doing four jobs at once — which is the shape the -// system this replaces failed in, one import at a time. - import ( "encoding/json" "errors" diff --git a/go/catalogue/delivery_seat.go b/go/catalogue/delivery_seat.go index f1d2d24..9efc57c 100644 --- a/go/catalogue/delivery_seat.go +++ b/go/catalogue/delivery_seat.go @@ -1,11 +1,5 @@ package catalogue -// The delivery's owner (novox/hq ADR 0239, to-be 47): one holder for the mesh, the module mesh-delivery, -// which owns a delivery — one commit in one repository, from its pull request's head to every machine — -// and a delivery group, and asks the controller for every act. A seat of the mesh's own, so the role has -// one name whoever holds it, and the controller can tell whether anything holds it: while nothing does, -// the controller starts every walk itself, as it did before there was a delivery to own. - // DeliverySeat is the seat mesh-delivery holds. const DeliverySeat = "mesh-delivery" diff --git a/go/catalogue/dir_into.go b/go/catalogue/dir_into.go index e9b3f92..725c977 100644 --- a/go/catalogue/dir_into.go +++ b/go/catalogue/dir_into.go @@ -7,29 +7,6 @@ import ( "strings" ) -// A directory the mesh places (novox/hq ADR 0112, to-be 27, issue 119). -// -// **A module definition names no host path.** A directory resource may omit `path`; the mesh -// resolves where it lands when the declaration is composed — `//`, the root a -// node's own setting with /var/lib as the default. From then on the module's own files, mounts -// and environment name the place as `${dir:}`, the same shape as `${bound:…}` and -// `${secret:…}`: a fact the module asks for by name and never states. -// -// **A directory that states a path keeps it, and still answers `${dir:}`.** That is the -// placement for an adopted machine: data that must sit where the predecessor already put it is -// declared with the path as the exception it is, and everything else in the module names it by -// id — so moving it later is one line, not a search. -// -// **The mesh's own files for a module are placed too** (novox/hq issue 174). What the mesh writes -// *for* a module — its sealed bus credential, its merged configuration, its bindings — is the -// mesh's plumbing, not the module's data, and sits under `/mesh/`. A directory -// saying `"place": "mesh"` is that place; the definition names the files beneath it by -// `${dir:}` and states no path. -// -// **Resolved here, not on the machine.** The host receives concrete paths exactly as it always -// has; nothing new reaches it and it learns no field. Which also means a resolved path changing -// is a spec change like any other — and the spec comparison must see it (novox/hq issue 126). - // defaultDataRoot is where module data lands when a node states no root of its own. const defaultDataRoot = "/var/lib" diff --git a/go/catalogue/engine_seat.go b/go/catalogue/engine_seat.go index e5b6e3c..dfe2960 100644 --- a/go/catalogue/engine_seat.go +++ b/go/catalogue/engine_seat.go @@ -1,13 +1,5 @@ package catalogue -// The node seat node-engine (novox/hq ADR 0563 §1): held on every node by that node's engine (mesh-host), which serves -// its verbs on its own bus account. No module claims it and no assignment holds it: what the mesh knows of its holder -// is the node's record and the engine's haves (ADR 0563 §1 changes ADR 0110 §6 and §7 for this one seat). -// -// Its verbs carry no signature (§7): the runtime proves the caller, which matters only for the record. The read-only -// ones change nothing or are bounded in the engine's own state (§2); hand-over and access-act perform nothing on the -// call, and act only on the operator's warrant to the engine's own ask (§3). - // EngineSeat is the node seat every node's engine holds. const EngineSeat = "node-engine" diff --git a/go/catalogue/enrolment_seat.go b/go/catalogue/enrolment_seat.go index b01a054..26593bd 100644 --- a/go/catalogue/enrolment_seat.go +++ b/go/catalogue/enrolment_seat.go @@ -1,11 +1,5 @@ package catalogue -// **Joining a machine is the seat mesh-enrolment, held once** (novox/hq ADR 0587 §1, issue 774): held for the mesh by -// the module mesh-enrolment, the only caller the vault's manifest names for its enrolment-token and node-account verbs -// and a caller it names for `sign`. Six verbs, five events, none carrying a secret or anything derived from the token: -// the one hash an event carries is the digest of the public keys a node presented. It mints and keeps no secret and -// holds no signing key. Its state and its key layout are go/delivery's (enrolment.go). - // EnrolmentSeat is the seat mesh-enrolment. const EnrolmentSeat = "mesh-enrolment" diff --git a/go/catalogue/environment_into.go b/go/catalogue/environment_into.go index a638f94..eb3a5c3 100644 --- a/go/catalogue/environment_into.go +++ b/go/catalogue/environment_into.go @@ -7,23 +7,6 @@ import ( "strings" ) -// The account's environment and the login shell's code, composed from the modules a node runs -// (novox/hq ADR 0203, ADR 0204). -// -// **The same shape as the jails.** Every module may contribute — a toolchain its directory on PATH, -// a version manager a variable naming its home, a prompt the code that loads it — naming no node, no -// path and no file of the shell's (ADR 0112). The one module holding the matching seat places the -// result with a placeholder in its own file, and the controller fills it from every module on the -// node. A node not running a module has none of its contribution, and unassigning one takes its -// lines away at the next composition. -// -// **Two kinds of contribution, kept apart on purpose.** The environment is facts, which the -// controller writes in two standard formats — POSIX assignment and the service manager's -// environment.d — so a terminal, a script, the login shell's `execute` and a graphical session all -// read the same values (ADR 0203). Shell code is not a fact: it is text in one shell's syntax, which -// the controller sorts into a slot and pastes without reading, as it pastes a jail's stanza (ADR -// 0204). - // EnvironmentSeat and LoginShellSeat are the seats whose holders may place what the modules // contributed: the account's environment, and the login shell's code. const ( @@ -454,19 +437,6 @@ func entriesOf(entries []placedOn) string { return strings.Join(out, ":") } -// shellCode is every module's code for one shell and one slot (novox/hq ADR 0204 §3): in module -// order, each module's pieces in the order it declared them, each preceded by a line naming the -// module, and empty when nothing is contributed. -func shellCode(modules []Manifest, shell, slot string) string { - return shellCodeOn(modules, shell, slot, nil) -} - -// shellCodeOn is shellCode on a machine with these facts: a moved contributor's item of the slot's collection is -// rendered in its module's place as its old form was (heldItemText), so a collector not handed keeps every line. -func shellCodeOn(modules []Manifest, shell, slot string, facts map[string]string) string { - return shellCodeFor(modules, shell, slot, facts, nil) -} - // oldFormOfCollected is a file's text with each `${collected:contribution:::…}` of a kind received as data // read as the old `${contribution:::…}`: where the collector is not handed, the controller renders the // kind's items as it rendered its contributions, so the two forms place the same text (design 57 L3, issue 781). diff --git a/go/catalogue/environment_into_test.go b/go/catalogue/environment_into_test.go index 1df412e..aeb63f8 100644 --- a/go/catalogue/environment_into_test.go +++ b/go/catalogue/environment_into_test.go @@ -170,28 +170,6 @@ func shells() []Manifest { } } -// ADR 0204 §3: a slot holds that shell's code only, in module order, each module's pieces in the -// order it declared them under a line naming it; empty when nothing is contributed. -func TestShellCodeLandsInItsSlotInModuleOrderForItsShellOnly(t *testing.T) { - if got, want := shellCode(shells(), "zsh", "normal"), "# powerlevel10k\n"+ - "source ~/.local/share/powerlevel10k/powerlevel10k.zsh-theme\n"+ - "[[ -f ~/.local/share/powerlevel10k/p10k.zsh ]] && source ~/.local/share/powerlevel10k/p10k.zsh\n"+ - "# zsh-autosuggestions\n"+ - "source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh\n"; got != want { - t.Fatalf("zsh's normal slot is\n%s\nnot\n%s", got, want) - } - if got, want := shellCode(shells(), "zsh", "last"), "# zsh-syntax-highlighting\n"+ - "source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n"; got != want { - t.Fatalf("zsh's last slot is\n%s\nnot\n%s", got, want) - } - if got, want := shellCode(shells(), "bash", "last"), "# direnv\neval \"$(direnv hook bash)\"\n"; got != want { - t.Fatalf("bash's last slot is %q, not %q", got, want) - } - if got := shellCode(shells(), "fish", "first"); got != "" { - t.Fatalf("a slot nobody contributed to holds %q", got) - } -} - // The holder of node-login-shell, as WP3's zsh module writes its block, with its own zsh around the // slots — which holds `${…}` of the shell's own that no mesh pass may touch either. func zshHolder() Manifest { diff --git a/go/catalogue/events.go b/go/catalogue/events.go index 684cae5..2aa52e5 100644 --- a/go/catalogue/events.go +++ b/go/catalogue/events.go @@ -5,19 +5,6 @@ import ( "slices" ) -// What a module may call an event, and what a consumer may ask for. -// -// A module names an event **locally**: `order.placed`, not a subject and not a routing key -// (design 29 §1). A consumer names the emitter and the event: `billing.order.placed`. The mesh -// derives the subject from those, so reorganising the subject space leaves every manifest correct. -// -// **Nothing checked this until every manifest in the catalogue was wrong the same way** -// (novox/hq 04-ISSUES/127). All thirty-seven kept the old bus's routing key — -// `module..` — which the derivation read as "a module called `module`", so every -// cross-module subscription in the mesh pointed at a namespace nobody publishes to. Nothing failed: -// the services started and none of them reacted. The documentation on these fields taught the old -// form too, which is why the drift was uniform rather than scattered. - // eventName is one name in a local event: lower-case, and no wildcard. var eventName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) diff --git a/go/catalogue/files.go b/go/catalogue/files.go index f7c0a5a..d8befe2 100644 --- a/go/catalogue/files.go +++ b/go/catalogue/files.go @@ -7,14 +7,6 @@ import ( "strings" ) -// A node's files, read and managed (novox/hq ADR 0394, issue 900; superseding ADR 0310; hq to-be 41 WP9). -// -// **One seat for every file on a node.** The holder runs as the tool runner's account, the node's operator -// account (ADR 0394 §2), and reaches further through `sudo -n` where that account's rights do not. It refuses by -// name the places a secret is kept (reads only, ADR 0394 §7) and the paths a module owns, unless a write names a reason (`override`). -// node-reads stays beside it until mesh-files is assigned on every node (ADR 0394 §9); it goes in the next -// delivery. - // FilesSeat is the role that reads and manages a node's files and accounts (novox/hq ADR 0394). const FilesSeat = "node-files" diff --git a/go/catalogue/filtering.go b/go/catalogue/filtering.go index 3a14f3f..cce6bb6 100644 --- a/go/catalogue/filtering.go +++ b/go/catalogue/filtering.go @@ -216,28 +216,6 @@ func widest(rules []Rule) []Rule { // SSHPort is the one port a machine may never lose, whatever else it is told. const SSHPort = 22 -// AsNftables renders a rule set as a complete nftables configuration. -// -// `outward` says this machine is reachable from outside the mesh, which is the only thing that -// decides whether ssh is answered there as well as on the private network. -// -// `foundation` is the ports the MESH ITSELF needs reachable, which no module declares. -// -// **Everything else in this file is derived from what modules say they listen on, and the -// foundation is not a module** (novox/hq 04-ISSUES/051). So the broker — the port every machine -// dials to enrol and to receive every declaration it is ever sent — was absent from the ruleset, -// and nothing noticed: a mesh of one never dials its own broker across the network. The first -// machine to join a firewalled anchor is refused by the packet filter during enrolment, before -// the mesh can report anything about it. -// -// It is a floor for the same reason ssh is. A machine nobody can reach is a machine nobody can -// repair; a machine the mesh cannot reach is a machine the mesh cannot manage. Neither is a thing -// any module asks for, and neither may be derived away. -func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int, - outwardLinks []string, tunnel string) string { - return AsNftablesWithLocal(rules, mesh, outward, foundation, outwardLinks, tunnel, nil) -} - // AsNftablesWithLocal is AsNftables with the machine's local networks: the networks it reported on a // link facing outside that count (novox/hq ADR 0340, JudgeLocalNetworks), which a rule from // `local-network` admits on its own port alone, beside the private network. None is not widened to diff --git a/go/catalogue/filtering_foundation_test.go b/go/catalogue/filtering_foundation_test.go deleted file mode 100644 index 6005651..0000000 --- a/go/catalogue/filtering_foundation_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package catalogue - -import ( - "strings" - "testing" -) - -// The mesh's own ports survive a ruleset derived from modules that do not mention them. -// -// **The firewall is computed from what modules declare they listen on, and the foundation is not a -// module** (novox/hq 04-ISSUES/052). So the broker's port — the one every machine dials to enrol -// and to receive every declaration it is ever sent — was absent from every ruleset the mesh ever -// generated, and nothing caught it: a mesh of one never dials its own broker across the network, -// so the ruleset looks complete right up until a second machine tries to join and is refused by -// the packet filter, during enrolment, before the mesh can report anything about it. -func TestTheBrokersPortIsOpenedThoughNoModuleDeclaresIt(t *testing.T) { - const brokerPort = 5671 - - // A machine on the private network, with one ordinary module rule, and nothing that mentions - // the broker — which is every machine. - rules := []Rule{{Port: 8080, From: FromMesh, Because: []string{"some-module"}}} - out := AsNftables(rules, []string{"10.42.0.1"}, false, []int{brokerPort}, nil, "mesh0") - - if !strings.Contains(out, "tcp dport 5671 accept") { - t.Fatalf("the broker's port is not opened, so no machine could enrol:\n%s", out) - } - - // From anywhere, deliberately: a node enrols BEFORE it has an address on the private network, - // so a rule narrowed to that network would close the door being knocked on. - for _, line := range strings.Split(out, "\n") { - if strings.Contains(line, "5671") && strings.Contains(line, "saddr") { - t.Fatalf("the broker's port is narrowed to the private network, which a machine that "+ - "has not yet enrolled is not on:\n%s", line) - } - } - - // And forwarded, not only accepted on input: the broker is a published container port, so a - // cross-node dial is redirected and forwarded and never reaches the input chain. Without this - // a joined node reaches the broker only until its first connection's conntrack entry drops — - // which a broker restart (adopting the foundation's broker) causes — and then never receives - // another declaration (novox/hq issue 063). - if !strings.Contains(out, "ct original proto-dst 5671 accept") { - t.Fatalf("the broker's port is not forwarded, so a DNAT'd broker is unreachable "+ - "cross-node after it restarts:\n%s", out) - } -} - -// And a mesh that was never told about a broker still gets a ruleset, rather than an empty one or -// a panic. A control plane in that state cannot issue tokens either, which is where it surfaces. -func TestNoBrokerMeansNoFoundationRuleRatherThanNoRuleset(t *testing.T) { - out := AsNftables(nil, []string{"10.42.0.1"}, false, nil, nil, "mesh0") - if !strings.Contains(out, "table inet mesh") { - t.Fatalf("no ruleset at all:\n%s", out) - } - if strings.Contains(out, "never derived, never closed\n\t\ttcp dport") { - t.Fatalf("a foundation rule was written for a mesh with no broker:\n%s", out) - } -} diff --git a/go/catalogue/filtering_test.go b/go/catalogue/filtering_test.go index 81f3765..4ce4b6e 100644 --- a/go/catalogue/filtering_test.go +++ b/go/catalogue/filtering_test.go @@ -34,26 +34,6 @@ func TestTheRuleSetIsEveryAssignedModulesPorts(t *testing.T) { } } -// Every rule names what caused it, and both causes survive. -func TestTwoModulesWantingOnePortAreBothNamed(t *testing.T) { - r := Resolution{Modules: []Manifest{ - {Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere, Why: "the site"}}}, - {Module: "board", Listens: []Listening{{Port: 443, From: FromEverywhere, Why: "the board"}}}, - }} - rules := mustFilter(t, r, nil) - if len(rules) != 1 { - t.Fatalf("one port became %d rules", len(rules)) - } - if len(rules[0].Because) != 2 { - t.Fatalf("a module that wanted this port open is not named: %+v", rules[0]) - } - // The consequence, which is the reason this matters: removing web must not read as closing 443. - nft := AsNftables(rules, nil, false, nil, nil, "mesh0") - if !strings.Contains(nft, "web") || !strings.Contains(nft, "board") { - t.Fatalf("the rendered rule set does not name both sources:\n%s", nft) - } -} - // Widening, and only widening. func TestAPortOpenToEveryoneIsNotAlsoRestrictedToTheMesh(t *testing.T) { r := Resolution{Modules: []Manifest{ @@ -72,151 +52,6 @@ func TestAPortOpenToEveryoneIsNotAlsoRestrictedToTheMesh(t *testing.T) { } } -// What is not declared is closed. -func TestWhatNoModuleDeclaredIsClosed(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}}, - }}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0") - // Naming the chain, not just the policy: the forward chain drops too, and an assertion on - // "policy drop" alone passes while the input chain accepts everything. It did, once, here. - if !strings.Contains(nft, "type filter hook input priority filter; policy drop;") { - t.Fatalf("the input chain does not drop by default, so nothing is filtered:\n%s", nft) - } - // Some port nothing asked for. **Not 22**, which this used to use: ssh is now the one line in - // the chain that nothing derives, because a mesh that has been assigned almost nothing computes - // a firewall that shuts the port used to fix it (novox/hq issue 047). - if strings.Contains(nft, "dport 9999") { - t.Fatalf("a port no module declared was opened:\n%s", nft) - } - // Dropping by default must not mean unreachable: the node's own outbound link to the broker - // is the one connection the whole mesh runs on, and it is a reply to something it started. - if !strings.Contains(nft, "ct state established,related accept") { - t.Fatalf("a node applying this would lose its own link to the mesh:\n%s", nft) - } - if !strings.Contains(nft, "iif lo accept") { - t.Fatalf("loopback is filtered, which breaks everything on the machine:\n%s", nft) - } -} - -// Loading it twice must be the same as loading it once, and must not disturb anything else. -// -// `flush ruleset` would do the first and not the second: it empties every table on the machine, -// including the ones the container runtime writes for its bridges. -func TestReloadingReplacesOnlyTheMeshsOwnRules(t *testing.T) { - nft := AsNftables(nil, nil, false, nil, nil, "mesh0") - if strings.Contains(nft, "flush ruleset") { - t.Fatalf("loading the rule set empties every table on the machine:\n%s", nft) - } - if !strings.Contains(nft, "delete table inet mesh") { - t.Fatalf("loading it a second time would add to what is there rather than replace it:\n%s", nft) - } - // The delete must not fail on a machine that has never loaded one, or the service does not - // start and the machine filters nothing while reporting a configuration error. - if !strings.Contains(nft, "table inet mesh {}\ndelete table inet mesh") { - t.Fatalf("the delete has nothing to delete on a machine loading this for the first time:\n%s", nft) - } -} - -// The rule set governs what is forwarded too, or it does not govern container ports at all. -// -// **This reverses an earlier decision, and keeps the thing that decision was protecting.** There was -// no forward chain, on the reasoning that dropping there stops container traffic the runtime -// allowed — every container on the node, control plane included. That much is true. What it missed -// is that a published port is redirected and then forwarded, so it never reaches the input chain, -// and a firewall without a forward chain is silent about exactly the ports worth protecting -// (novox/hq issue 047). -// -// So the chain exists and denies by default, and the runtime's own networks are allowed explicitly -// — which is how the system being replaced has been doing it on these machines for months. -func TestWhatIsForwardedIsGovernedToo(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "hook forward priority filter; policy drop") { - t.Fatalf("forwarded traffic is not governed, so container ports are open:\n%s", nft) - } -} - -// And this machine's own guests keep working, which is the whole reason the chain was left out -// before — by not being mentioned (novox/hq ADR 0140). -// -// It used to be done by naming the address ranges they sit on: two fixed here and the rest recorded -// per machine. That list broke a workstation's containers at a flip and could not be made correct, -// because a range describes one machine and cannot tell a network the mesh made from one a -// predecessor left behind. What replaced it is a single line about the links traffic arrives on. -func TestThisMachinesOwnGuestsKeepWorkingWithoutBeingNamed(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0") - if !strings.Contains(nft, `iifname != { "eth0", "mesh0" } accept`) { - t.Fatalf("what did not arrive from outside is not accepted, so this machine's own guests "+ - "reach nothing:\n%s", nft) - } -} - -// No address of a machine's own networks appears anywhere in a rendered filter. -// -// This is the assertion that fails against the previous behaviour, and it is why it is written on -// the text rather than on an outcome: the two ranges were a constant in this file, so nothing but -// reading the output catches one creeping back in. -func TestNoNetworkOfTheMachinesOwnIsNamed(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0") - for _, gone := range []string{"172.16.0.0/12", "192.168.128.0/17", "saddr 192.168", "saddr 172."} { - if strings.Contains(nft, gone) { - t.Fatalf("%q is named, and a range describes one machine and goes stale in silence:\n%s", - gone, nft) - } - } -} - -// **The tunnel is constrained, not treated as inside.** -// -// Accepting everything arriving over the private network would make a port nothing declares -// reachable from every machine in the mesh — the derivation abandoned, and a rule that reads as a -// restriction while restricting nothing. So the tunnel is named beside the outward links, and -// traffic arriving on it meets the declared rules like anything else. -func TestTheTunnelIsConstrainedLikeAnOutwardLink(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0") - line := `iifname != { "eth0", "mesh0" } accept` - if !strings.Contains(nft, line) { - t.Fatalf("the tunnel is not constrained, so an undeclared port is reachable from any "+ - "machine in the mesh:\n%s", nft) - } -} - -// A machine with two links facing outside has both constrained. Asserted on the one line, because a -// rule covering one and not the other would leave a machine filtering half of what reaches it. -func TestEveryOutwardLinkIsConstrained(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0", "wlan0"}, "mesh0") - if !strings.Contains(nft, `iifname != { "eth0", "wlan0", "mesh0" } accept`) { - t.Fatalf("not every outward link is constrained:\n%s", nft) - } -} - -// A guest asks its host for an address and for names, and those two arrive at the input chain. Asked -// for by the link they arrive on, so a resolver bound anywhere but an outward link keeps answering. -func TestGuestsMayAskTheirHostForAnAddressAndNames(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0") - for _, want := range []string{ - `iifname != { "eth0", "mesh0" } udp dport { 53, 67 } accept`, - `iifname != { "eth0", "mesh0" } tcp dport 53 accept`, - } { - if !strings.Contains(nft, want) { - t.Fatalf("a guest cannot ask its host for an address or a name, which is not a closed "+ - "port but a network that does not work:\n%s", nft) - } - } -} - -// With no link named at all the chain denies rather than rendering an empty set, which nftables -// refuses — and a rule set that does not load is a machine filtering nothing while its unit reports -// success. Composing a declaration for such a machine is refused upstream; this is the floor. -func TestNoLinkNamedRendersNoCatchAllRatherThanAnEmptySet(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, nil, "") - if strings.Contains(nft, "{ }") || strings.Contains(nft, "iifname != {}") { - t.Fatalf("an empty set is rendered, which nftables refuses:\n%s", nft) - } - if !strings.Contains(nft, "hook forward priority filter; policy drop") { - t.Fatalf("the forward chain does not deny:\n%s", nft) - } -} - // A machine that has not said which links face outside is sent no filter, and the refusal names the // module that would have loaded it so the reader knows what is being withheld. func TestAMachineThatNamedNoOutwardLinkIsSentNoFilter(t *testing.T) { @@ -245,62 +80,6 @@ func TestAMachineWithNoFilterModuleIsNotRefused(t *testing.T) { } } -// A published port is matched by what the client asked for, not by where the packet ends up. -// -// The runtime rewrites the destination before this chain sees it, so a rule naming the published -// port would match nothing and the port would stay shut while reporting itself open. -func TestAPublishedPortIsMatchedByWhatWasAskedFor(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "web", Listens: []Listening{{Port: 8080, From: FromEverywhere}}}, - }}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "ct original proto-dst 8080 accept") { - t.Fatalf("the forwarded rule does not match the port a client asked for:\n%s", nft) - } -} - -// And a mesh-scoped port stays mesh-scoped when it is reached through the runtime. -func TestAMeshScopedPortIsMeshScopedWhenForwarded(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - }}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "ip saddr { 198.51.100.2 } ct original proto-dst 5432 accept") { - t.Fatalf("a mesh-only port is reachable from anywhere once forwarded:\n%s", nft) - } -} - -// "From the mesh" is the addresses the mesh actually has. -func TestFromTheMeshIsTheNodesTheMeshKnows(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - }}, nil), []string{"198.51.100.2", "198.51.100.3"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 5432 accept") { - t.Fatalf("a mesh-scoped port was not restricted to the mesh's addresses:\n%s", nft) - } -} - -// The case that must not be widened silently. -func TestAMeshPortOnANodeWithNoMeshIsClosedAndSaysSo(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - }}, nil), nil, false, nil, nil, "mesh0") - if strings.Contains(nft, "dport 5432 accept") { - t.Fatalf("a port meant for the mesh was opened to everything:\n%s", nft) - } - if !strings.Contains(nft, "closed") { - t.Fatalf("it was closed silently, which is a fault nobody can find:\n%s", nft) - } -} - -// A port bound for something else on the same machine must not reach the network. -func TestAMachineScopedPortIsNotOpened(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "cache", Listens: []Listening{{Port: 6379, From: FromMachine}}}, - }}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0") - if strings.Contains(nft, "dport 6379 accept") { - t.Fatalf("a port for this machine only was opened to the network:\n%s", nft) - } -} - // The rule set is the machine's, not the asking module's. func TestTheModuleAskingForTheRuleSetGetsEveryModulesPorts(t *testing.T) { r := Resolution{Modules: []Manifest{ @@ -326,24 +105,6 @@ func TestTheModuleAskingForTheRuleSetGetsEveryModulesPorts(t *testing.T) { } } -// nftables matches the two address families separately, and one set holding both is a syntax -// error — so the file fails to load, the service reports a fault, and the machine filters nothing. -func TestAMeshOnBothAddressFamiliesRendersBoth(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - }}, nil), []string{"198.51.100.2", "2001:db8::2"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "ip saddr { 198.51.100.2 } tcp dport 5432 accept") { - t.Fatalf("the machines with v4 addresses were dropped:\n%s", nft) - } - if !strings.Contains(nft, "ip6 saddr { 2001:db8::2 } tcp dport 5432 accept") { - t.Fatalf("the machines with v6 addresses were dropped:\n%s", nft) - } - // And never in one set, which is the syntax error this exists to avoid. - if strings.Contains(nft, "198.51.100.2, 2001:db8::2") { - t.Fatalf("both families are in one set, so the file will not load:\n%s", nft) - } -} - // The host does not sort, so the order the mesh writes is the order a machine applies. // // A module's service or container routinely depends on a file the mesh computed — a certificate, @@ -728,42 +489,6 @@ func TestExposureRefusesAPortNotListenedOnAndABadSource(t *testing.T) { } } -// ssh survives whatever the mesh does or does not know about this machine. -// -// **The one rule here that nothing derives.** Every other line comes from what is assigned, which -// is the point of the mechanism — and a mesh part-way through adopting a machine has been assigned -// almost nothing, so what it computes is a chain that drops the port used to fix it. The session -// loading the rules lives on conntrack until it drops, and then the machine is reached from a -// rescue console (novox/hq issue 047). -func TestSSHIsOpenFromTheMeshEvenWhenNothingIsAssigned(t *testing.T) { - nft := AsNftables(nil, []string{"198.51.100.2", "198.51.100.3"}, false, nil, nil, "mesh0") - if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 22 accept") { - t.Fatalf("ssh is not open to the mesh, so a machine can lock everyone out:\n%s", nft) - } - // And not to the world, on a machine that does not face it. - if strings.Contains(nft, "\t\ttcp dport 22 accept") { - t.Fatalf("ssh is open to everywhere on a machine that faces inward only:\n%s", nft) - } - // And from outside as well, on a machine that faces outward — because that is the way in when the - // private network is the thing that broke. - nft = AsNftables(nil, []string{"198.51.100.2"}, true, nil, nil, "mesh0") - if !strings.Contains(nft, "\t\ttcp dport 22 accept") { - t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft) - } -} - -// A machine the mesh knows no addresses for still answers ssh. -// -// **This is the machine it matters most for.** Before the private network exists there is nothing -// to narrow the rule to, so narrowing it shuts the port entirely — on the first machine anybody -// adopts, reached over the network, closed by the act of adopting it. -func TestSSHIsNeverLeftWithoutARule(t *testing.T) { - nft := AsNftables(nil, nil, false, nil, nil, "mesh0") - if !strings.Contains(nft, "tcp dport 22 accept") { - t.Fatalf("a machine with no mesh addresses has no ssh rule, so adopting it locks it:\n%s", nft) - } -} - // chainBody is one chain's own lines, so an assertion cannot be satisfied by an identical line in // another chain. // @@ -785,95 +510,3 @@ func chainBody(t *testing.T, nft, chain string) string { } return rest[:j] } - -// **Anything on this machine may call anything on this machine.** -// -// Local is not a boundary this mesh draws, and whether a caller sits in a container was never meant to -// change the answer. It did, because the chain asked about addresses: a caller on the machine carries -// the machine's address and a caller in one of its containers carries a bridge address, so a rule -// naming the machines' own addresses silently refused every container on them. -// -// Measured: a module reaching its database on its own machine's name timed out for eleven hours while -// the machine itself could reach it (novox/hq 04-ISSUES/145). -func TestAnythingOnThisMachineMayCallAnythingOnIt(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - {Module: "private", Listens: []Listening{{Port: 9999, From: FromMachine}}}, - }}, nil), []string{"10.10.0.1", "10.10.0.2"}, false, nil, []string{"eth0"}, "mesh0") - - // In the INPUT chain, which is where a call to a service on this machine arrives. The forward - // chain carries the same line in the same words, so asserting on the whole file proves nothing. - input := chainBody(t, nft, "input") - if !strings.Contains(input, `iifname != { "eth0", "mesh0" } accept`) { - t.Fatalf("a caller on this machine cannot reach a service on it:\n%s", input) - } - // One rule, for every service here — not a line per port that only covers the ports somebody - // remembered to think about. - if strings.Contains(input, `iifname != { "eth0", "mesh0" } tcp dport 5432`) { - t.Fatalf("the local allowance is still written per port:\n%s", input) - } - // And the private network still reaches what is exposed to it, which is a different question. - if !strings.Contains(input, "ip saddr { 10.10.0.1, 10.10.0.2 } tcp dport 5432 accept") { - t.Fatalf("the private network no longer reaches a service exposed to it:\n%s", input) - } -} - -// The three reaches, as three lines. This is the whole of what the filter says about who may call what. -func TestTheThreeReachesAreThreeLines(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "internal-only", Listens: []Listening{{Port: 5432, From: FromMesh}}}, - {Module: "public", Listens: []Listening{{Port: 443, From: FromEverywhere}}}, - }}, nil), []string{"10.10.0.1"}, false, nil, []string{"eth0"}, "mesh0") - - input := chainBody(t, nft, "input") - for what, want := range map[string]string{ - "on this machine": `iifname != { "eth0", "mesh0" } accept`, - "over the private network": "ip saddr { 10.10.0.1 } tcp dport 5432 accept", - "from anywhere": "tcp dport 443 accept", - } { - if !strings.Contains(input, want) { - t.Fatalf("a caller %s cannot reach what is exposed to it (%q):\n%s", what, want, input) - } - } -} - -// A port open to everything needs no such line — it is already open to a guest. -func TestAPublicPortNeedsNoGuestLine(t *testing.T) { - nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}}, - }}, nil), []string{"10.10.0.1"}, false, nil, []string{"eth0"}, "mesh0") - if strings.Count(nft, `iifname != { "eth0", "mesh0" } tcp dport 443`) != 0 { - t.Fatalf("a public port was given a guest line it does not need:\n%s", nft) - } -} - -// **The hub relays the mesh** (novox/hq ADR 0007, issue 196). Two machines that are not co-located -// reach each other through the hub, so the hub forwards a packet that arrives on the tunnel and -// leaves on it. The forward chain judged that packet by the hub's own published ports, and two -// machines behind the hub reached each other only on the ports the hub happened to publish. -// -// Measured: from one home machine to another through the hub, 17 of 55 ports answered, and they -// were exactly the hub's own; the SYN for the rest never left the hub. -func TestTheMeshPassingThroughIsRelayedNotJudgedAsThisMachines(t *testing.T) { - nft := AsNftables(nil, []string{"10.42.0.1", "10.42.0.2"}, false, nil, []string{"eth0"}, "mesh0") - relay := `iifname "mesh0" oifname "mesh0" accept` - if !strings.Contains(chainBody(t, nft, "forward"), relay) { - t.Errorf("the forward chain does not relay the mesh through this machine:\n%s", chainBody(t, nft, "forward")) - } - // Relaying is not receiving: nothing in the input chain opens because of it. - if strings.Contains(chainBody(t, nft, "input"), "oifname") { - t.Errorf("the input chain names an outgoing interface, which no packet for this machine has:\n%s", - chainBody(t, nft, "input")) - } - // And off the tunnel into this machine's own containers is still judged: the tunnel is not - // accepted wholesale, only in and out on it. - if strings.Contains(chainBody(t, nft, "forward"), `iifname "mesh0" accept`) { - t.Errorf("the forward chain accepts everything off the tunnel:\n%s", chainBody(t, nft, "forward")) - } - - // A machine with no tunnel relays nothing, and names no interface it does not have. - alone := AsNftables(nil, nil, false, nil, []string{"eth0"}, "") - if strings.Contains(alone, "oifname") { - t.Errorf("a machine with no tunnel was given a relay rule:\n%s", alone) - } -} diff --git a/go/catalogue/handed.go b/go/catalogue/handed.go index 54b1b46..8d2cefb 100644 --- a/go/catalogue/handed.go +++ b/go/catalogue/handed.go @@ -11,28 +11,6 @@ import ( "strings" ) -// A module handed to the provisioner (novox/hq ADR 0349 clauses 2, 4, 6 and 17, design 57 P6, issue 672). -// -// **Marked in its manifest, one module at a time.** A module whose manifest says `"moved": true` has moved: each -// file of it that names a template is filled by the provisioner's client on the node, from the templates archive -// its build packed, and the controller fills nothing in it. Every other resource of the module, and every file of -// a module not marked, is composed exactly as before (clause 17: each file the old way or the new way, never both). -// -// **The build keeps what the client needs.** Resolving a moved module's build keeps, beside the files' text the -// mesh holds, where its templates archive is and what it hashes to, and which template each file is filled from -// (Handed). The text stays, so a machine whose node-engine cannot take a handed file is still sent the module the -// old way, byte for byte. -// -// **Handed only where the machine can take it.** A node's declaration hands a moved module's files over only when -// its node-engine writes a filled file on the client's word alone (Rendering.Hands) and the node runs a provisioner -// client (Rendering.Provisioner): then each such file is declared with its path, mode, owner and who owns it at -// runtime and without its text, the declaration names it among the filled files, and the module's plan says the -// client where its templates are and the value of every placement placeholder they name. Anywhere else the module -// composes as if it were not marked. - -// MovedField is the manifest's key marking a module moved. -const MovedField = "moved" - // Handed is what a moved module's build kept for the provisioner (novox/hq ADR 0349 §2): its templates archive, as // recorded (store-relative, routed at composition like any archive), and its digest; and, for each file resource // filled from a template, that template's path inside the archive. Written by Resolve, never by hand. diff --git a/go/catalogue/health.go b/go/catalogue/health.go index 058c9a8..ff5953f 100644 --- a/go/catalogue/health.go +++ b/go/catalogue/health.go @@ -3,30 +3,10 @@ package catalogue import ( "fmt" "sort" - "strconv" "strings" "time" ) -// A module says how each long-running resource is ready (novox/hq ADR 0240 rule 2, to-be 48 §2 and §8, -// Phase B). -// -// **On the resource, beside its other fields**: one kind and its timing. The node-engine judges every -// long-running resource alive with no declaration (Phase A); this is how a module says what *ready* means -// for one — the image's own check adopted by name, an HTTP request to a declared endpoint, a TCP connect, -// a command in the container, the unit's own readiness, or one of the module's own tools. -// -// **An endpoint is named, never a port or an address**: the check follows the machine's port for that -// endpoint as the endpoint does, so a port this machine gave elsewhere moves the check with it. The -// controller composes the name into the port the machine published it on, and sends the field only to a -// node-engine that reads it (link.ReadinessContract): an older, strict engine refuses a field it does not -// know, whole. -// -// **The bounds are the record's**: an interval not under ten seconds, a timeout under the interval, at -// least two failing looks in a row, and a grace plus the failing looks within five minutes — so a -// resource broken from its start is said within the gate's ten. Refused here, near the author, and again -// by the node-engine, far away, in the same words. - // HealthField is the resource field. const HealthField = "health" @@ -264,31 +244,3 @@ func healthInto(resource map[string]any, m Manifest, with Rendering) { } resource[HealthField] = out } - -// HealthWords is a declared check in a line, for `module check` and `node show`. -func HealthWords(h Health) string { - var what string - switch h.Kind { - case HealthHTTP: - what = "http " + orSlash(h.Path) + " on " + h.Endpoint - if h.Status != 0 { - what += " expecting " + strconv.Itoa(h.Status) - } - case HealthTCP: - what = "tcp on " + h.Endpoint - case HealthTool: - what = "its tool " + h.Tool - case HealthRuntime: - what = "its image's own check" - default: - what = h.Kind - } - return fmt.Sprintf("%s every %s", what, h.Interval) -} - -func orSlash(p string) string { - if p == "" { - return "/" - } - return p -} diff --git a/go/catalogue/health_seats.go b/go/catalogue/health_seats.go index e208f1f..68164fc 100644 --- a/go/catalogue/health_seats.go +++ b/go/catalogue/health_seats.go @@ -1,17 +1,5 @@ package catalogue -// **The mesh's health, out of the controller** (novox/hq ADR 0564 clause 1, issue 1952): four seats at mesh scope, each -// held once, each holder a process on its own bus credential that writes only its own state. -// -// - mesh-conditions, held by the module mesh-conditions (the condition keeper): the one writer of the open conditions, -// raised from the observers' findings and cleared when they go (clauses 2, 3, 9, 13); -// - mesh-watchdogs, held by the watchman (mesh-delivery-watchman): the signals as keys that expire, its stuck keys and -// presence as findings, and the dead letters from the lease write (clauses 4, 5); -// - mesh-self-check, held by the module mesh-self-check: the probes, every five minutes (clause 6); -// - mesh-healers, held by the module mesh-healers: the healers, acting on the keeper's events (clauses 7, 8). -// -// Their events keep the controller's names and bodies (go/health), so a reader changes only the seat it hears. - // The four health seats. const ( ConditionsSeat = "mesh-conditions" diff --git a/go/catalogue/holdings_test.go b/go/catalogue/holdings_test.go index dedc566..fcdbd4c 100644 --- a/go/catalogue/holdings_test.go +++ b/go/catalogue/holdings_test.go @@ -71,20 +71,6 @@ func TestAHolderOnRecordElsewhereLeavesThisMachinesClaimantSilent(t *testing.T) } } -// A record naming a seat's former name still applies to it after a rename (ADR 0122). -func TestAHolderRecordedUnderAFormerNameStillHolds(t *testing.T) { - busSeatDelivering(t, "mesh-bus") - wasAliases := aliases - t.Cleanup(func() { UseAliases(wasAliases) }) - UseAliases(map[string]string{"the-broker": "mesh-broker"}) - record := []Held{{Claim: "the-broker", Scope: ScopeMesh, Node: "anchor", Module: "new-broker"}} - - held, problems := checkClaims([]Manifest{oldBroker(), newBroker()}, Node{Name: "anchor"}, nil, record) - if len(problems) != 0 || len(held) != 1 || held[0].Module != "new-broker" { - t.Fatalf("a record under the former name did not settle the seat: held=%v problems=%v", held, problems) - } -} - // CanHold is the one judgement registration and the handover share, against the store's row. func TestCanHoldJudgesClaimScopeAndWhatTheSeatDelivers(t *testing.T) { busSeatDelivering(t, "mesh-bus") diff --git a/go/catalogue/hostname_seat_test.go b/go/catalogue/hostname_seat_test.go index c151efa..8acf0f8 100644 --- a/go/catalogue/hostname_seat_test.go +++ b/go/catalogue/hostname_seat_test.go @@ -10,48 +10,6 @@ import ( // machine's own lines in /etc/hosts. The seat was renamed (ADR 0122), so what claims the old name — a // manifest registered before the rename — still holds the one seat. -func withHostnameAlias(t *testing.T) { - t.Helper() - was := aliases - t.Cleanup(func() { aliases = was }) - UseAliases(map[string]string{"node-hosts-file": "node-hostname"}) -} - -func TestTheHostsFilesFormerNameResolvesToTheHostnameSeat(t *testing.T) { - if _, known := SeatNamed("node-hostname"); !known { - t.Fatal("node-hostname is not in the mesh's set") - } - withHostnameAlias(t) - seat, known := SeatNamed("node-hosts-file") - if !known || seat.Name != "node-hostname" || seat.Scope != ScopeNode { - t.Fatalf("the former name did not resolve: %+v %v", seat, known) - } - var verbs []string - for _, v := range seat.Serves { - verbs = append(verbs, v.Name) - } - if strings.Join(verbs, " ") != "entries add remove" { - t.Errorf("the renamed seat serves %v; its verbs are unchanged", verbs) - } -} - -// One seat under either name: the module registered before the rename and the one after cannot both -// hold it on one machine. -func TestTheOldAndTheNewClaimantAreOneSeatOnAMachine(t *testing.T) { - withHostnameAlias(t) - cat := shelf( - mod("hosts", nil, nil, nil, Claim{Name: "node-hosts-file"}), - mod("hostname", nil, nil, nil, Claim{Name: "node-hostname"}), - ) - _, err := Resolve(cat, []string{"hosts", "hostname"}, workstation(), World{}) - if err == nil || !strings.Contains(err.Error(), "node-hostname") { - t.Errorf("hosts and hostname both held the machine's names on one machine: %v", err) - } - if _, err := Resolve(cat, []string{"hosts"}, workstation(), World{}); err != nil { - t.Errorf("a machine still assigned hosts under the old name does not resolve: %v", err) - } -} - // The catalogue's module: /etc/hostname is the operator's `hostname` setting. Without it the module is // left out, naming the key — the mesh never renames a machine on its own — and a mesh-wide setting // naming ${machine:name} gives every machine its mesh name. diff --git a/go/catalogue/identity.go b/go/catalogue/identity.go index 376f3df..42f892e 100644 --- a/go/catalogue/identity.go +++ b/go/catalogue/identity.go @@ -7,23 +7,6 @@ import ( "strings" ) -// Who a consumer is, said once by the mesh (novox/hq 04-ISSUES/023). -// -// **The provisioner used to invent this and nothing else could derive it.** It made a role called -// `mesh__`, which is a reasonable name and is knowable nowhere else: not by the -// control plane, not by the binding, and above all not by the consumer — which has to present it -// in order to authenticate. The one identifier needed to connect was the one thing no part of the -// mesh would say. -// -// So the mesh says it. It goes to the provider in the grant and to the consumer in its binding, -// from **one derivation**, which is what makes the two ends agree by construction rather than by -// two conventions that were the same on the day they were written. -// -// **It is still name-agnostic.** The mesh does not know what a role or an access key or a client -// is; it says who is asking, and each provisioner makes that true in whatever its own system -// calls an identity. What a provider does with it is the provider's business, as everything about -// a provision is. - // identityUnusable is every character that is not safe unquoted in the systems these names reach. // // Conservative on purpose: lower-case letters, digits and underscore reach a PostgreSQL role, a diff --git a/go/catalogue/installation.go b/go/catalogue/installation.go index 2a00b7c..31067eb 100644 --- a/go/catalogue/installation.go +++ b/go/catalogue/installation.go @@ -1,27 +1,5 @@ package catalogue -// A definition names no installation (novox/hq ADR 0112, ADR 0155, issues 122 and 134). -// -// A module definition holds what is true of the module everywhere; what is particular to one mesh — -// a public name, a forge's address, a node's public address — is resolved at assignment. The rule -// stood for a month with nothing checking it, and a sweep found thirty of seventy-one definitions -// naming the installation they were written in. This is the check. -// -// **What is judged is what the mesh acts on, not what a person reads.** A domain in a `why` or a -// `description` is documentation the mesh never reads; reporting it beside `KC_HOSTNAME` would teach -// people to ignore the report. What is judged is every other string value: a name under a public -// top-level domain, or a public address. Two families of name are the world's and not this mesh's, -// and are allowed where they can only mean the world: the public registries an `image` may be pulled -// from, and the public resolvers a machine may forward to. The container runtime's own alias for -// its host is the runtime's, true on every machine that runs it. -// -// **A name that is right where it stands is declared, one by one, with its reason.** A federated -// server's config names the federation's public directory; an application built outside the mesh -// is pulled from the registry that built it, until the mesh builds it. The resource carries -// `names-on-purpose`, a map from each such name to why — the shape `secrets-in-environment` has, -// per name — so a reader sees which names a definition means to carry and why, a name the map does -// not cover is still reported, and the catalogue-wide test is the list that shrinks as names move. - // NamesOnPurpose is the catalogue-level word a resource carries for the names it means to name: // each name mapped to its reason. The host never sees it. const NamesOnPurpose = "names-on-purpose" diff --git a/go/catalogue/jails_into.go b/go/catalogue/jails_into.go index 34305bc..dd3c053 100644 --- a/go/catalogue/jails_into.go +++ b/go/catalogue/jails_into.go @@ -8,14 +8,6 @@ import ( "strings" ) -// A node's fail2ban jails, composed from the modules it runs (novox/hq to-be 31). -// -// **The same shape as the firewall.** Every module's `listens` become the node's rule set; every -// module's `jails` become the node's fail2ban config. A module that runs an authenticating service -// declares what a break-in on it looks like and how to ban it, naming no node and no path (ADR -// 0112); the intrusion-prevention holder — the one module with `jailing` — gathers them and writes -// them where it owns. A node not running a module has none of its jails. - // jailsInto composes every jail declared by the modules on a node into the files the holder writes: // one jail file (all stanzas, so the fail2ban service restarts on a single resource) and one filter // file per jail (its failregex, which fail2ban references by the jail's name). diff --git a/go/catalogue/keyed.go b/go/catalogue/keyed.go index 6161afd..49ea461 100644 --- a/go/catalogue/keyed.go +++ b/go/catalogue/keyed.go @@ -2,16 +2,6 @@ package catalogue import "slices" -// The one rule of which provisions the seat mesh-provisioner keys and a want waits for (novox/hq ADR 0570 §1, §2, §4; -// issue 774). The plan composition puts a need in a consumer's plan by it, the holder of mesh-provisioner keys what the -// plans name, and the assigner's rule 8 waits for what Awaited answers: three parts, one rule, so they cannot drift -// again (on 2026-10-30 the assigner waited for every `requires` while the plans named only some, and every new build of -// grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make). -// -// A need a module answers (not a record, not nothing) is keyed when its consumer is handed to the provisioner, or the -// provider's offer says the provider helper answers it. A need of a consumer not handed whose provider's helper does -// not answer it is still answered by the controller's composition: it is neither keyed nor waited for. - // HandedByManifest is whether a module is handed to the provisioner as far as its manifest says (Rendering.Handing // without the machine's own words, and begin's): marked moved, not a held module's stub, with a template its build // kept or an item. diff --git a/go/catalogue/laid_values.go b/go/catalogue/laid_values.go index caf1792..320a04f 100644 --- a/go/catalogue/laid_values.go +++ b/go/catalogue/laid_values.go @@ -5,11 +5,9 @@ import "encoding/json" // The sources mesh-config says a resolved value came from, in mesh-config_resolved (novox/hq ADR 0348 §9): a key of // it holds {"value": …, "source": …}. const ( - SourceMesh = "mesh" - SourceNode = "node" - SourceModule = "module" - SourceDefault = "default" - SourceUnset = "unset" + SourceMesh = "mesh" + SourceNode = "node" + SourceModule = "module" ) // LaidSource says whether a value resolved from source is laid over a module's files: mesh config, node config and the diff --git a/go/catalogue/logs.go b/go/catalogue/logs.go index ec93acd..3602bf2 100644 --- a/go/catalogue/logs.go +++ b/go/catalogue/logs.go @@ -6,14 +6,6 @@ import ( "fmt" ) -// What a module may call its logs (novox/hq ADR 0297). -// -// A log is a module's record of operations: entries appended under a key, each kept as long as the -// log is, in the order they came. A module names each log it owns **locally** — `changes`, never a -// stream or a subject (ADR 0201 §4) — and the mesh derives the stream from the module and the local -// name, as it derives a bucket. So the rule for a log's name is a state name's: one plain token, and -// a module that keeps a log has a name that is one plain token too. - // The mesh's caps on a log, in MiB: what a module may ask, and what it gets when it asks nothing. const ( LogLeastMiB = 1 diff --git a/go/catalogue/machine_into_files.go b/go/catalogue/machine_into_files.go index f0e38de..3ac5be8 100644 --- a/go/catalogue/machine_into_files.go +++ b/go/catalogue/machine_into_files.go @@ -7,28 +7,6 @@ import ( "strings" ) -// What a module may say about the machine it is running on. -// -// **A module cannot know where it will be assigned, and sometimes it must say so anyway.** Every -// other name in a declaration is either the module's own — which it wrote — or something it -// requires, which arrives as a binding. The machine underneath is neither: it is chosen when the -// module is assigned, long after the manifest was written, and until now nothing carried it into a -// file. -// -// The case that found this is a certificate authority inside the mesh (novox/hq ADR 0066). A proxy -// reaches it at the address the mesh handed over, `.internal` — so the authority's own -// certificate has to be issued for that name, or the first thing that happens is the proxy refusing -// to talk to it. The authority is the one thing that cannot be told its name by a binding: it -// provides, it does not require. Written as a literal it would be a manifest carrying one -// deployment's machine name, which is the shape [ADR 0066] exists to remove. -// -// Three facts, all the mesh's own vocabulary — the same `node` and `at` a contribution already -// carries, and the address behind `at`, for software that takes an address and not a name. The -// case that found the third is a resolver pointing the container runtime at itself: the runtime's -// list of resolvers is addresses, because a name there would have to be resolved by the resolver -// it names. Nothing about what a machine is *for*: that would be the mesh learning what a module -// means, which it does not do. - // ofMachine is where a module says a fact about the machine underneath it belongs: // ${machine:}. var ofMachine = regexp.MustCompile(`\$\{machine:([a-z0-9][a-z0-9_-]*)\}`) diff --git a/go/catalogue/naming.go b/go/catalogue/naming.go deleted file mode 100644 index 1b52013..0000000 --- a/go/catalogue/naming.go +++ /dev/null @@ -1 +0,0 @@ -package catalogue diff --git a/go/catalogue/node_resolver.go b/go/catalogue/node_resolver.go index a628cba..b883f98 100644 --- a/go/catalogue/node_resolver.go +++ b/go/catalogue/node_resolver.go @@ -2,25 +2,6 @@ package catalogue import "path" -// The machine's own resolver (novox/hq ADR 0247). -// -// **Most machines have none.** Every machine lists the mesh's resolvers in /etc/resolv.conf and nothing -// else, and the module holding its uplink writes that file (ADR 0223). A machine with a VPN client that -// pushes its own resolvers for its own domains needs a third answer: those domains to the VPN's servers, -// over the VPN's link, and every other name to the mesh's resolvers as before. One file cannot list both -// sets of servers — musl takes the first reply, glibc the first server's "no such name" — so the domains -// are routed by a resolver on the machine itself: the `node-resolver` seat's holder. -// -// **Where it is held, it owns the resolver file.** The file then names the machine's own resolver, which -// routes; the uplink's holder steps back from writing it on that machine, by the rule below and not by -// two modules writing one path. There are two uplink holders (NetworkManager's and systemd-networkd's), -// and the resolver is its own module so it is written once, not once in each. -// -// **It knows nothing of any VPN.** Its verbs route a set of domains to a set of servers over one link, -// list what is routed and remove a route. A module wrapping a VPN client that writes /etc/resolv.conf -// itself carries its own adapter and calls those verbs; a VPN that tells systemd-resolved its link's DNS -// itself needs none. - // ResolverSeat is the machine's own resolver (novox/hq ADR 0247). const ResolverSeat = "node-resolver" diff --git a/go/catalogue/objects.go b/go/catalogue/objects.go index 888c232..7c93b01 100644 --- a/go/catalogue/objects.go +++ b/go/catalogue/objects.go @@ -6,14 +6,6 @@ import ( "fmt" ) -// A module's own object bucket (novox/hq ADR 0592 §5, §9). -// -// A module declares an object bucket as it declares a state (ADR 0201) or a stream (ADR 0363): named locally, -// with its maximum size. The mesh creates it once, keeps every object with no age limit, and refuses a new object -// when it is full rather than dropping one; only its module writes it. Another module reads it only as a restricted -// stream is heard: a `reads` entry `.` naming it, and a module the owner's trusted config value of the -// bucket's grant names (ADR 0363 §4, §5), read-only. This is the shape only: its checks are the indexer's judge's. - // The mesh's bounds on a module's object bucket: its size in MiB (ADR 0592 §5, §25; the bound ADR 0363 §1 sets for a // stream). A bucket says its size: there is no default. const ( diff --git a/go/catalogue/optional_requirements.go b/go/catalogue/optional_requirements.go index ab40521..3c01f86 100644 --- a/go/catalogue/optional_requirements.go +++ b/go/catalogue/optional_requirements.go @@ -8,21 +8,6 @@ import ( "strings" ) -// A requirement a module can run without (novox/hq ADR 0330, issue 166). -// -// **Hard stays the default.** A requirement written as a bare name is hard, exactly as before: it -// resolves, or the assignment is refused, and a single catalogue candidate is installed for it. One -// written `{"name": "influxdb-api", "optional": true}` resolves the same way when a provider is -// already assigned in the mesh or reachable from the consumer (§2), and is otherwise **absent**: not -// refused, and nothing is installed for it, on this machine or elsewhere (§3). -// -// **An absent requirement is never rendered as an empty value** (§4–§6). Its binding, its pair -// credential and its grant are not composed, so a file that names one of them is delivered only while -// the requirement is bound (`only-when-bound`), and a `${bound:…}` placeholder of it outside such a -// file names the value it takes while absent: `${bound::|default:}`. The -// module check refuses anything else, so the gap is found when the manifest is written, not on a -// machine where it reads as a blank. - // OnlyWhenBound is a resource's field naming the optional requirement it is delivered only while // bound (ADR 0330 §5). Said in the catalogue and dropped before the declaration is sent: the // node-engine parses strictly and knows no such field. diff --git a/go/catalogue/placement.go b/go/catalogue/placement.go index c9f7137..7d0ef4b 100644 --- a/go/catalogue/placement.go +++ b/go/catalogue/placement.go @@ -8,40 +8,12 @@ import ( "strings" ) -// Where a module's data is on THIS machine is the assignment's (novox/hq ADR 0112, issue 153). -// -// A definition names no host path. It declares the directories it owns by id, and the operator's -// shared data it needs by id too (an `access`, ADR 0051). A node has a default layout for the -// former — // — and nothing at all for the latter, because shared data is -// wherever the operator keeps it. An adopted machine keeps its data where the predecessor put it: -// a 40 TB library on its own pool, a configuration on a second disk. Both halves are said on the -// assignment, validated the way `endpoints` is — an id the module does not declare is refused, -// because a setting that reaches nothing is a mistake — and resolved here, so the host receives -// concrete paths exactly as it always has and learns no field. -// -// {"places": {"config": "/services/sonarr/config", -// "data": {"path": "/mnt/plex/data", "owner": "1000:1000"}}, -// "accesses": {"series": "/storage/media/series", -// "downloads": "/storage/downloads"}} -// -// A placed directory is still the mesh's: created, chowned to the owner the assignment says (or -// the manifest's), removed when empty and no longer declared. A placed access is still the -// operator's: mounted, never created, chowned or removed. - // PlacesSetting is the settings key that places a module's declared directories, by id. const PlacesSetting = "places" // AccessesSetting is the settings key that says where a module's accesses are on this node, by id. const AccessesSetting = "accesses" -// **Placement is not config** (novox/hq ADR 0348 clause 11, issues 651 and 736). `places`, `accesses` and -// `networks` — where an assignment's things are — and `endpoints`, `expose`, `reach` and `ports` — where it -// is reached and from how far — are facts of an assignment the controller decides, held beside the -// assignment and apart from its settings: a Layer carries them in Placement, never in Values, and only -// their readers (Places, AccessPlaces, KeptNetworks, Endpoints, Exposure, Reaches, GivenPorts) read them. -// A person still says them as keys of a layer through `settings`; LayerOf splits such a layer, so what -// they type is routed to the placement and nothing they do changes. - // placementKeys are the keys of a layer that are placement, not config. var placementKeys = map[string]bool{PlacesSetting: true, AccessesSetting: true, NetworksSetting: true, EndpointsSetting: true, ExposeSetting: true, ReachSetting: true, PortsSetting: true} @@ -57,34 +29,6 @@ func PlacementKeys() []string { return keys } -// SplitPlacement divides a layer as a person says it into its config values and its placement. Each is -// nil when it holds nothing; the values themselves are not copied. -func SplitPlacement(values map[string]any) (config, placement map[string]any) { - for key, value := range values { - if placementKeys[key] { - if placement == nil { - placement = map[string]any{} - } - placement[key] = value - continue - } - if config == nil { - config = map[string]any{} - } - config[key] = value - } - return config, placement -} - -// LayerOf is a layer as a person says it — config values and placement in one map — split into the two. -func LayerOf(from string, values map[string]any) Layer { - config, placement := SplitPlacement(values) - if config == nil { - config = map[string]any{} - } - return Layer{From: from, Values: config, Placement: placement} -} - // Whole is the layer as a person says it: its values and its placement in one map, a new map. func (l Layer) Whole() map[string]any { out := make(map[string]any, len(l.Values)+len(l.Placement)) diff --git a/go/catalogue/placement_seat.go b/go/catalogue/placement_seat.go index 4b7af9b..37f9a0a 100644 --- a/go/catalogue/placement_seat.go +++ b/go/catalogue/placement_seat.go @@ -1,14 +1,5 @@ package catalogue -// The seat of placement (novox/hq ADR 0567): which module runs on which node, with its host ports, bindings, the -// node's pins and holdings, the config placement and values, and what a person decides of the node, is state on the -// bus held by this seat, changed only by a proposal the nodes it touches answer and the operator approves. Held once -// for the mesh by the assigner (mesh-delivery-assigner), beside its wants. Its states are named after the holding -// module, as every module state is (delivery.PlacementBucket, PlacementProposalsBucket, PlacementCandidatesBucket); -// they are the seat's contract, so every reader takes their names from mesh-sdk, and a change of holder is a migration -// the new holder's record decides (ADR 0567 §1). Its events are the holder's too, carried on the holder's stream -// (delivery.PlacementStream): the names are in delivery (PlacementProposedEvent and the rest). - // PlacementSeat is the seat the assigner holds for placement. const PlacementSeat = "mesh-placement" diff --git a/go/catalogue/placement_test.go b/go/catalogue/placement_test.go index ac86f13..0a938e4 100644 --- a/go/catalogue/placement_test.go +++ b/go/catalogue/placement_test.go @@ -1,10 +1,5 @@ package catalogue -import ( - "strings" - "testing" -) - // The case novox/hq issue 153 records: an adopted machine keeps its data where the predecessor put // it — a library on its own pool that must never move, a configuration on a second disk owned by // whoever the predecessor ran as. A definition may name none of that (ADR 0112); the assignment @@ -21,243 +16,3 @@ func placeable() Manifest { m.Accesses = []Access{{ID: "series", Mode: AccessReadWrite}, {ID: "spool"}} return m } - -func placedBy(values map[string]any) Rendering { - return Rendering{Settings: SettingsBy{"arr": {LayerOf("node anchor", values)}}} -} - -func TestAnAssignmentPlacesDirectoriesAndAccesses(t *testing.T) { - got, err := Resolve(shelf(placeable()), []string{"arr"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - out, err := got.Declaration(placedBy(map[string]any{ - PlacesSetting: map[string]any{ - "config": map[string]any{"path": "/services/arr/config/", "owner": "1001:2000"}, - }, - AccessesSetting: map[string]any{ - "series": "/storage/media/series", - "spool": "/storage/downloads", - }, - })) - if err != nil { - t.Fatal(err) - } - seen := map[string]map[string]any{} - for _, r := range out { - seen[r["id"].(string)] = r - } - config := seen["arr.config"] - if config["path"] != "/services/arr/config" || config["owner"] != "1001:2000" { - t.Fatalf("the placed directory is %v %v; want the assignment's path and owner", config["path"], config["owner"]) - } - if seen["arr.state"]["path"] != "/var/lib/arr" { - t.Fatalf("an unplaced directory left the default layout: %v", seen["arr.state"]["path"]) - } - var accesses []string - for _, r := range out { - if r["type"] == "access" { - accesses = append(accesses, r["path"].(string)+" "+r["mode"].(string)) - } - } - if strings.Join(accesses, ",") != "/storage/media/series read-write,/storage/downloads read" { - t.Fatalf("the accesses reached the machine as %v", accesses) - } - server := seen["arr.server"] - mounts := server["volumes"].([]any) - if mounts[0] != "/services/arr/config:/config" || mounts[1] != "/storage/media/series:/series" || - mounts[2] != "/storage/downloads:/downloads:ro" { - t.Fatalf("the mounts were not filled with the placed paths: %v", mounts) - } - if server["env"].(map[string]any)["SPOOL"] != "/storage/downloads" { - t.Fatalf("the environment was not filled: %v", server["env"]) - } -} - -// An access declared by id and placed by nobody resolves to nowhere, and that is refused with the -// setting to write — not mounted as the literal, not skipped. The refusal costs the module its -// place in the declaration, not the machine its declaration (novox/hq ADR 0163, rule 6). -func TestAnUnplacedAccessIsRefusedByName(t *testing.T) { - got, err := Resolve(shelf(placeable()), []string{"arr"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - with := placedBy(map[string]any{ - AccessesSetting: map[string]any{"series": "/storage/media/series"}, - }) - composed, err := got.Compose(with) - if err != nil { - t.Fatal(err) - } - why := composed.LeftOut["arr"] - if why == "" || !strings.Contains(why, `"spool"`) || !strings.Contains(why, AccessesSetting) { - t.Fatalf("an access nobody placed was not refused by name: %v", composed.LeftOut) - } - if _, declared := byID(composed.Resources)["arr.server"]; declared { - t.Fatal("the module with the unplaced access was declared anyway") - } -} - -// Validated like endpoints: an id the module does not declare reaches nothing, and the refusal -// says what it does declare; a relative path and a non-numeric owner are refused too. -func TestPlacementsAreValidated(t *testing.T) { - m := placeable() - layers := func(values map[string]any) []Layer { return placedBy(values).Settings["arr"] } - - _, err := Places(m, layers(map[string]any{PlacesSetting: map[string]any{"data": "/mnt/data"}})) - if err == nil || !strings.Contains(err.Error(), "does not declare") || !strings.Contains(err.Error(), `"config"`) { - t.Fatalf("placing an undeclared directory was accepted: %v", err) - } - _, err = Places(m, layers(map[string]any{PlacesSetting: map[string]any{"config": "services/arr"}})) - if err == nil || !strings.Contains(err.Error(), "absolute") { - t.Fatalf("a relative placement was accepted: %v", err) - } - _, err = Places(m, layers(map[string]any{PlacesSetting: map[string]any{ - "config": map[string]any{"path": "/services/arr", "owner": "media"}}})) - if err == nil || !strings.Contains(err.Error(), "uid:gid") { - t.Fatalf("a non-numeric owner was accepted: %v", err) - } - _, err = AccessPlaces(m, layers(map[string]any{AccessesSetting: map[string]any{"movies": "/storage/media/movies"}})) - if err == nil || !strings.Contains(err.Error(), "does not declare") || !strings.Contains(err.Error(), `"series"`) { - t.Fatalf("placing an undeclared access was accepted: %v", err) - } - _, err = AccessPlaces(m, layers(map[string]any{AccessesSetting: map[string]any{"series": "media/series"}})) - if err == nil || !strings.Contains(err.Error(), "absolute") { - t.Fatalf("a relative access was accepted: %v", err) - } - // And the two keys are never stray: they are validated here, not merged into a file. - if stray := UnusedSettings(m, layers(map[string]any{ - PlacesSetting: map[string]any{"config": "/services/arr/config"}, - AccessesSetting: map[string]any{"series": "/storage/media/series"}, - })); len(stray) != 0 { - t.Fatalf("the placement keys were reported as unused: %v", stray) - } -} - -// A definition that carries a path still works, as the default the assignment may replace — and -// the assignment's placement wins where both say. -func TestADefinitionsPathIsTheDefaultTheAssignmentReplaces(t *testing.T) { - m := placeable() - m.Accesses = []Access{{ID: "series", Path: "/services/media/series", Mode: AccessReadWrite}, {ID: "spool", Path: "/services/media/downloads"}} - got, err := Resolve(shelf(m), []string{"arr"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - out, err := got.Declaration(placedBy(map[string]any{ - PlacesSetting: map[string]any{"config": "/services/arr/config"}, - AccessesSetting: map[string]any{"series": "/storage/media/series"}, - })) - if err != nil { - t.Fatal(err) - } - var paths []string - for _, r := range out { - if r["type"] == "access" { - paths = append(paths, r["path"].(string)) - } - } - if strings.Join(paths, ",") != "/storage/media/series,/services/media/downloads" { - t.Fatalf("placed one, defaulted the other: got %v", paths) - } -} - -// **Placement is read from the placement, never from a layer's values** (novox/hq ADR 0348 clause 11, issue -// 651). A `places` or `accesses` among the config values places nothing; the same in Placement does. A -// mergeable file of today's form still takes every key of the layer as it is said, placement too, so it -// composes as it did while placement was kept in the layer. -func TestPlacementIsReadFromThePlacementOnly(t *testing.T) { - m := placeable() - said := map[string]any{ - PlacesSetting: map[string]any{"config": "/services/arr/config"}, - AccessesSetting: map[string]any{"series": "/storage/media/series", "spool": "/storage/downloads"}, - "log": "warn", - } - among := []Layer{{From: "anchor", Values: said}} - if placed, err := Places(m, among); err != nil || placed != nil { - t.Fatalf("a place among the config values placed %v (%v)", placed, err) - } - if _, _, err := accessesFor(m, among); err == nil { - t.Fatal("an access among the config values placed it") - } - beside := []Layer{LayerOf("anchor", said)} - if beside[0].Values["log"] != "warn" || beside[0].Values[PlacesSetting] != nil || beside[0].Placement[PlacesSetting] == nil { - t.Fatalf("LayerOf split %+v", beside[0]) - } - if placed, err := Places(m, beside); err != nil || placed["config"].Path != "/services/arr/config" { - t.Fatalf("the placement did not place: %v (%v)", placed, err) - } - - file := map[string]any{"id": "runtime", "type": "file", "path": "/x", "content": `{"a":1}`, "merge": "json"} - whole, err := ApplySettings(file, among) - if err != nil { - t.Fatal(err) - } - split, err := ApplySettings(file, beside) - if err != nil { - t.Fatal(err) - } - if whole["content"] != split["content"] || !strings.Contains(split["content"].(string), `"places"`) { - t.Fatalf("today's mergeable file composes %q, was %q", split["content"], whole["content"]) - } -} - -// **The four words of where an assignment is reached are placement too** (novox/hq ADR 0348 clause 11, issue 736). -// `endpoints`, `expose`, `reach` and `ports` among a layer's config values say nothing to the filter, the ports or -// the names; the same in Placement do, and LayerOf puts them there. A mergeable file of today's form still takes -// every key of the layer as it is said, so it composes as it did while they were kept among the values. -func TestTheFourWordsAreReadFromThePlacementOnly(t *testing.T) { - m := Manifest{Module: "forge", Version: "1", - Listens: []Listening{{Name: "web", Port: 3000, Protocol: "tcp", From: FromMesh}, {Name: "ssh", Port: 22, Protocol: "tcp", From: FromMesh}, - {Name: "api", Port: 8086, Protocol: "tcp", From: FromMesh}}, - Resources: []map[string]any{{"id": "server", "type": "container", "image": "x", "ports": []any{"3000", "222:22", "8086"}}}} - said := map[string]any{ - ExposeSetting: map[string]any{"22": FromEverywhere}, - PortsSetting: map[string]any{"22": float64(2222)}, - ReachSetting: map[string]any{"3000": "internal"}, - EndpointsSetting: map[string]any{"api": map[string]any{"label": "influx", "port": float64(18086), "reach": "public"}}, - "log": "warn", - } - among := []Layer{{From: "anchor", Values: said}} - if e, err := Exposure(m, among); err != nil || e != nil { - t.Fatalf("an expose among the config values exposed %v (%v)", e, err) - } - if g, err := GivenPorts(m, among); err != nil || g != nil { - t.Fatalf("a ports among the config values gave %v (%v)", g, err) - } - if r, err := Reaches(m, among); err != nil || r != nil { - t.Fatalf("a reach among the config values reached %v (%v)", r, err) - } - if b, err := Endpoints(m, among); err != nil || len(b) != 0 { - t.Fatalf("an endpoints among the config values configured %v (%v)", b, err) - } - - beside := []Layer{LayerOf("anchor", said)} - if len(beside[0].Values) != 1 || beside[0].Values["log"] != "warn" || len(beside[0].Placement) != 4 { - t.Fatalf("LayerOf split %+v", beside[0]) - } - if e, err := Exposure(m, beside); err != nil || e[22] != FromEverywhere { - t.Fatalf("the placement did not expose: %v (%v)", e, err) - } - if g, err := GivenPorts(m, beside); err != nil || g[22] != 2222 || g[8086] != 18086 { - t.Fatalf("the placement did not give the ports: %v (%v)", g, err) - } - if r, err := Reaches(m, beside); err != nil || r[3000] != "internal" || r[8086] != "public" { - t.Fatalf("the placement did not reach: %v (%v)", r, err) - } - if b, err := Endpoints(m, beside); err != nil || b["api"].Label != "influx" { - t.Fatalf("the placement did not configure the endpoint: %v (%v)", b, err) - } - - file := map[string]any{"id": "runtime", "type": "file", "path": "/x", "content": `{"a":1}`, "merge": "json"} - whole, err := ApplySettings(file, among) - if err != nil { - t.Fatal(err) - } - split, err := ApplySettings(file, beside) - if err != nil { - t.Fatal(err) - } - if whole["content"] != split["content"] || !strings.Contains(split["content"].(string), `"endpoints"`) { - t.Fatalf("today's mergeable file composes %q, was %q", split["content"], whole["content"]) - } -} diff --git a/go/catalogue/port_into.go b/go/catalogue/port_into.go index 071f36e..d12d309 100644 --- a/go/catalogue/port_into.go +++ b/go/catalogue/port_into.go @@ -8,43 +8,6 @@ import ( "strings" ) -// Telling a module which port it was given. -// -// **The mesh assigns the machine-side port and a module does not choose one** -// ([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)); on a node given one for a -// module it is the operator's number rather than the mesh's -// ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)). For a -// container's own listening socket that is invisible: the mesh rewrites `ports` into -// `assigned:wanted`, the software inside binds the number it has always bound, and the machine -// publishes a different one. -// -// **Two kinds of resource have no such layer.** -// -// - **A process** runs on the machine, there is nothing to rewrite, and it binds whatever its -// configuration says — so without this, every process binds the number written in its own -// config, two modules declaring the same one collide, and the mesh's whole reason for -// assigning ports is defeated by the resource kind that most needs it. -// - **A container that DIALS the machine** — a module's own sidecar reaching the service beside -// it over the machine's loopback — is told that address in its environment, and the mapping -// that saves the listener does nothing for the caller: what it must dial is the machine-side -// number, which is exactly the one the module cannot know (novox/hq 04-ISSUES/088). -// -// So a module asks. `${port:8080}` is "the machine-side port you gave me for the 8080 I said I -// listen on", and the module writes that where it would otherwise have written a literal — in a -// file's content, or in a value of a container's or a process's `env`. -// -// **The environment is filled by the control plane, exactly as a bound value is.** A port is not -// secret — the mesh holds it in the clear — so there is nothing for the host to be the only -// witness of, and the host learns no new field. That is what separates this from -// [ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md), which refuses a -// `${secret:…}` in an `env` outright: the objection there is to the value being in an environment -// at all, not to who fills it in. -// -// **It answers with the machine's number, wherever it is written.** A container reaching a sibling -// over the runtime's own network reaches it on the port inside that container and goes on writing -// that number literally — it is a number the module does control. This is for the machine side, -// which is the side nobody but the mesh can know. - // ofPort is where a module asks which port it was given: ${port:}. var ofPort = regexp.MustCompile(`\$\{port:([0-9]+)\}`) diff --git a/go/catalogue/predefined.go b/go/catalogue/predefined.go index 2f0e7d0..aca9071 100644 --- a/go/catalogue/predefined.go +++ b/go/catalogue/predefined.go @@ -13,27 +13,9 @@ import ( "sync" ) -// The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661). -// -// **A fixed, reviewable list, owned by the build.** Mesh config and node config hold values only for the keys -// in the catalogue's `predefined.json`, at the root of its repository: each key with its type, a description -// including its unit, its validation (enum, minimum and maximum, pattern) and whether it is trusted. A module's -// value may fall back to one of them, of the same type (ADR 0348 §2, §4). -// -// **Read where a manifest is judged, from the tree it is judged in.** A build reads the file at the root of the -// repository at the commit it builds, `module check` at the root of the repository above each manifest, the -// merge gate at the root of the tree under check; each refuses a file that is not exactly a list of keys, and -// judges every manifest it reads against it (ParseManifestWith). Everywhere else — registration, a stored build -// read again — a manifest is judged against the list the mesh published (PublishedPredefined): the newest trunk -// build's of the catalogue, which the controller keeps and loads with its store. - // PredefinedFile is the list's file at the root of the catalogue's repository. const PredefinedFile = "predefined.json" -// CatalogueRepository is the name of the repository whose list the mesh publishes: the catalogue's, by the name -// a check finds it beside under (internal/builder's check, ADR 0121). -const CatalogueRepository = "mesh-catalog" - // PredefinedKey is one key of the list: its type, its description, its validation and whether it is trusted // (ADR 0348 §7). It has no default: a module's value falling back to it has its own. type PredefinedKey struct { @@ -142,45 +124,6 @@ func ReadPredefined(root string) (PredefinedList, error) { return ParsePredefined(raw) } -// RepositoryRoot is the nearest directory at or above dir holding `.git`: the root of the repository a manifest -// is in, where `module check` reads the list. Empty when none is: a manifest outside a repository is judged -// against no list. -func RepositoryRoot(dir string) string { - at, err := filepath.Abs(dir) - if err != nil { - return "" - } - for { - if _, err := os.Lstat(filepath.Join(at, ".git")); err == nil { - return at - } - up := filepath.Dir(at) - if up == at { - return "" - } - at = up - } -} - -// PredefinedAbove is the list at the root of the repository above dir (RepositoryRoot), nil when there is no -// repository or it holds no list. -func PredefinedAbove(dir string) (PredefinedList, error) { - root := RepositoryRoot(dir) - if root == "" { - return nil, nil - } - return ReadPredefined(root) -} - -// Canonical is the list as JSON with its keys sorted: what a build carries and the controller keeps, so one list -// is one text. -func (l PredefinedList) Canonical() ([]byte, error) { - if l == nil { - return nil, nil - } - return json.Marshal(map[string]PredefinedKey(l)) -} - // Keys is the list's keys, sorted. func (l PredefinedList) Keys() []string { out := make([]string, 0, len(l)) @@ -191,34 +134,12 @@ func (l PredefinedList) Keys() []string { return out } -// NamesPredefined is whether any value of the manifest falls back to a predefined key. -func (m Manifest) NamesPredefined() bool { - for _, d := range m.Settings { - if d.Predefined != "" { - return true - } - } - return false -} - // publishedPredefined is the list the mesh published, which a manifest is judged against where no tree is read. var publishedPredefined struct { sync.RWMutex list PredefinedList } -// UsePredefined makes list the one a manifest is judged against where no tree is read: what the controller -// loads from its store, and what it keeps when a newer trunk build of the catalogue publishes one. Nil leaves -// the list it has: a store that holds none yet never empties one already loaded. -func UsePredefined(list PredefinedList) { - if list == nil { - return - } - publishedPredefined.Lock() - defer publishedPredefined.Unlock() - publishedPredefined.list = list -} - // PublishedPredefined is the list a manifest is judged against where no tree is read; nil when none is loaded. func PublishedPredefined() PredefinedList { publishedPredefined.RLock() diff --git a/go/catalogue/predefined_inputs.go b/go/catalogue/predefined_inputs.go deleted file mode 100644 index 1b52013..0000000 --- a/go/catalogue/predefined_inputs.go +++ /dev/null @@ -1 +0,0 @@ -package catalogue diff --git a/go/catalogue/predefined_test.go b/go/catalogue/predefined_test.go index 396e4b4..d203641 100644 --- a/go/catalogue/predefined_test.go +++ b/go/catalogue/predefined_test.go @@ -17,29 +17,6 @@ const theFirstKeys = `{ "agent-managed-settings": {"type": "object", "description": "the managed settings of every agent session, a JSON object", "trusted": true} }` -func TestTheFirstListIsRead(t *testing.T) { - list, err := ParsePredefined([]byte(theFirstKeys)) - if err != nil { - t.Fatal(err) - } - if got := strings.Join(list.Keys(), ","); got != "agent-managed-settings,font-size" { - t.Fatalf("keys %s", got) - } - if k := list["font-size"]; k.Type != ConfigNumber || *k.Minimum != 6 || *k.Maximum != 72 || k.Trusted { - t.Fatalf("font-size read as %+v", k) - } - if k := list["agent-managed-settings"]; k.Type != ConfigObject || !k.Trusted { - t.Fatalf("agent-managed-settings read as %+v", k) - } - // One list is one text, however it was written. - a, _ := list.Canonical() - again, _ := ParsePredefined(a) - b, _ := again.Canonical() - if string(a) != string(b) { - t.Fatalf("canonical text moved:\n%s\n%s", a, b) - } -} - func TestAListIsRefusedWholeForAnyWrongKey(t *testing.T) { for _, c := range []struct{ file, says string }{ {`[]`, "not a JSON object of keys"}, @@ -103,42 +80,3 @@ func TestTemplatesFilledKeepTheListTheManifestWasJudgedAgainst(t *testing.T) { t.Fatalf("filled from the tree, it was judged against another list: %v", err) } } - -// module check reads the list at the root of the repository above the manifest; no repository, no list. -func TestTheListIsFoundAtTheRepositoryRootAboveAManifest(t *testing.T) { - root := t.TempDir() - module := filepath.Join(root, "modules", "dunst") - if err := os.MkdirAll(module, 0o755); err != nil { - t.Fatal(err) - } - if list, err := PredefinedAbove(module); err != nil || list != nil { - t.Fatalf("outside a repository: %v %v", list, err) - } - if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { - t.Fatal(err) - } - if list, err := PredefinedAbove(module); err != nil || list != nil { - t.Fatalf("a repository without the file: %v %v", list, err) - } - if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(theFirstKeys), 0o644); err != nil { - t.Fatal(err) - } - list, err := PredefinedAbove(module) - if err != nil || len(list) != 2 { - t.Fatalf("the list above: %v %v", list, err) - } - if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(`{"font-size": {}}`), 0o644); err != nil { - t.Fatal(err) - } - if _, err := PredefinedAbove(module); err == nil { - t.Fatal("a wrong list above was read") - } - if err := os.Remove(filepath.Join(root, PredefinedFile)); err != nil { - t.Fatal(err) - } - if err := os.Symlink("/etc/hostname", filepath.Join(root, PredefinedFile)); err == nil { - if _, err := PredefinedAbove(module); err == nil || !strings.Contains(err.Error(), "not a plain file") { - t.Fatalf("a link read as the list: %v", err) - } - } -} diff --git a/go/catalogue/print_rehearsal_test.go b/go/catalogue/print_rehearsal_test.go deleted file mode 100644 index d2aa7ef..0000000 --- a/go/catalogue/print_rehearsal_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package catalogue - -import ( - "os" - "testing" -) - -func TestPrintRehearsalRuleset(t *testing.T) { - if os.Getenv("PRINT_RULESET") == "" { - t.Skip("set PRINT_RULESET") - } - rules := mustFilter(t, Resolution{Modules: []Manifest{ - {Module: "pub", Listens: []Listening{{Port: 8099, From: FromMesh, Why: "the thing it serves"}}}, - }}, nil) - t.Log("\n" + AsNftables(rules, []string{"192.0.2.20"}, true, nil, nil, "mesh0")) -} diff --git a/go/catalogue/reads.go b/go/catalogue/reads.go index 224e464..50991b8 100644 --- a/go/catalogue/reads.go +++ b/go/catalogue/reads.go @@ -8,20 +8,6 @@ import ( "strings" ) -// A node's files and accounts, read (novox/hq ADR 0310, extending ADR 0268 §5; hq to-be 41 WP8). -// -// **A seat of its own on every node.** Reading a file, searching one, listing a directory, the owner and mode -// of each component of a path, and looking up an account or a group are the reads ADR 0268 §5 has narrow tools -// take over from the login shell's `execute`, so an approval is asked for an act and never for a read. Every -// node needs them, the control-node included, and another module could offer them instead of the tool -// runner's (ADR 0126 §9): a seat, so the mesh finds them on every node by one name. -// -// **Read as an account that holds no secret.** The holder runs as an account of its own (runs-as, ADR 0259 -// §8), so the node's file modes bound what it sees — never the operator's account, which sees the -// operator's keys and the agent's credentials. On top, it refuses by name the places a secret is kept, -// which the controller names for it (secretPlaces), and every answer passes the content rule's secret class -// (mesh-sdk's outward, ADR 0310 §4). - // ReadsSeat is the role that reads a node's files and accounts (novox/hq ADR 0310). const ReadsSeat = "node-reads" diff --git a/go/catalogue/recreates.go b/go/catalogue/recreates.go index 431a3f8..800d33d 100644 --- a/go/catalogue/recreates.go +++ b/go/catalogue/recreates.go @@ -8,15 +8,6 @@ import ( "strings" ) -// What a move does to a module's containers (novox/hq ADR 0245). -// -// A container whose declaration changes is recreated, whatever changed in it: an image, an environment -// variable, a health check adopted. A module whose containers are recreated in one send is down while -// they start again — on 2026-10-07 a catalogue change that only adopted the images' own health checks -// recreated nine of mail's containers at once, and the operator's phone could not reach the mail. So a -// send says, per module, how many of its containers it recreates and whether any image changed, before -// it is sent and in the plan that sent it. - // Recreation is what moving a module from one build's manifest to another's does to its containers. type Recreation struct { // Containers is how many containers the new build declares. @@ -110,15 +101,6 @@ func sameDeclaration(a, b any) bool { return reflect.DeepEqual(na, nb) } -// What a move under a module's own `record` may do without a person (novox/hq ADR 0377). -// -// A module records because its manifest says so or because it keeps irreplaceable data: mail, whose containers -// recreated together interrupt everyone's mail, and the database server, whose restart drops every consumer. That -// hold also held the moves that interrupt nothing — on 2026-10-15 the database server moved with only its tools -// bundle changed, its walk said it "recreates none of its containers", and the machine stayed behind until a -// person pushed. So such a move rolls out like any other, and only a move that recreates a container or touches -// data still waits. - // Holds is why a move from one build's manifest to another's still waits for a person under a module's own // `record` (ADR 0377 §3), empty when it recreates none of its containers and moves none of its data: no container // changed, added or gone; no file or directory changed, added or gone that a container mounts, or that holds or diff --git a/go/catalogue/resolve.go b/go/catalogue/resolve.go index f81a39f..3498b98 100644 --- a/go/catalogue/resolve.go +++ b/go/catalogue/resolve.go @@ -1,18 +1,11 @@ package catalogue import ( - "errors" "fmt" "sort" "strings" ) -// Resolving is turning "these modules are assigned here" into "this is what the node runs". -// -// It refuses rather than guesses, everywhere. novox/hq ADR 0009: a requirement with several -// answers is refused and named, because counting candidates has no surprising behaviour and a -// solver that picks has to be understood before its answer can be trusted. - // Node is what resolution needs to know about the machine. type Node struct { Name string @@ -279,9 +272,6 @@ func (r *Refusal) Error() string { return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ") } -// ErrAmbiguous is returned inside a Refusal when a requirement has more than one answer. -var ErrAmbiguous = errors.New("more than one module provides that") - // Resolve works out everything a node runs, from what was assigned to it. // // The catalogue is every module the mesh knows about; assigned is what a person put on this node. @@ -877,21 +867,6 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world return resolution, nil } -// servedHere is *whether* a provider in this node's own set says a consumer needs to know -// anything, and provisionally what. -// -// **Provisional, and deliberately so.** Resolving runs before a machine's ports are assigned and -// before any settings are in view, so nothing said here can be the final answer: it is the module's -// manifest and nothing else. What reaches the consumer is re-derived from the resolved closure by -// Declaration (servedOnThisMachine), with this machine's port assignments and the provider's -// settings layers — the same two steps the mesh applies to a provider on any other machine. -// -// It still matters that this is right about *emptiness*: a keyless same-node provider is only -// delivered as a need at all when it serves something (novox/hq 04-ISSUES/038's sibling), and a -// need that is never created is a binding the consumer never gets. It is right about that from the -// manifest alone, which is why walking the catalogue mid-resolution is enough here and is not -// enough for the values. - // isModule reports whether a name is a module in its own right rather than only something // modules provide. // diff --git a/go/catalogue/roster.go b/go/catalogue/roster.go index fbf39d8..0058d27 100644 --- a/go/catalogue/roster.go +++ b/go/catalogue/roster.go @@ -8,24 +8,6 @@ import ( "text/template" ) -// What only the mesh knows, written where a module asks for it — in the module's own format. -// -// **The graph is the control plane's; the format is the module's.** The mesh knows which machines -// exist, what they are called and where they are. Turning that into a hosts file, a resolver's -// zones, an ssh known_hosts is somebody's configuration language, and the mesh has no business -// knowing it. So the mesh hands the roster to a template the module wrote and renders it; it never -// learns what the file means. -// -// This used to be a closed list of fact names, each with its format written in Go here — a hosts -// file, a resolver's zones. Every new consumer meant a new formatter in the control plane, in the -// consumer's configuration language. Now the data is the mesh's and the format is a template the -// module ships: the two built-in cases (the network module's `/etc/hosts`, dnsmasq's zones) render -// the same way any module's would, and the control plane holds no format at all. -// -// It replaced three modules that existed only because computed output needed somewhere to live — -// they ran no software, could not be swapped for anything, and appeared in the graph as modules -// while being a data channel wearing a costume (novox/hq ADR 0040). - // A RosterFile is a file the mesh renders from the roster of machines, in the format the module // gives as a Go text/template. The template sees a rosterView: `.Node` (this machine's bare name), // `.Suffix` (what its mesh name ends in), and two sets of `{Name, FQDN, Address}` — `.Names` and diff --git a/go/catalogue/runtime.go b/go/catalogue/runtime.go index f8beae0..8d6d495 100644 --- a/go/catalogue/runtime.go +++ b/go/catalogue/runtime.go @@ -7,14 +7,6 @@ import ( "strings" ) -// The node's tool runtime, as the catalogue knows it (novox/hq ADR 0175, to-be 38). -// -// **One module is the runtime.** Where it is assigned, one process per machine serves every assigned -// module's tools and every held seat's verbs, on the host side, from the bundles each module's build -// produced — and no module needs a container to reach the bus with its tools. The name is a constant -// rather than a manifest field because a rule turns on it: the composer places the runtime's process -// where this module is, and registration refuses the old pattern once this module exists. - // RuntimeModule is the module that is the node's tool runtime. Mirrored in the broker package, // which composes a principal of its own for it; the agreement test there holds the two to one string. const RuntimeModule = "node-tools" @@ -377,60 +369,6 @@ func toAny(in []string) []any { return out } -// RuntimeImageModule and RuntimeImageArtifact name the image every per-module tool container was -// built on: the tool runtime's own runtime image. With the runtime a module of its own, that image -// stays the way a module's SERVICE may be built and stops being the way tools reach a node (ADR 0175). -const ( - RuntimeImageModule = "mesh-tools" - RuntimeImageArtifact = "runtime" -) - -// ToolContainerOnTheRuntime says why a manifest is the pattern ADR 0175 retires — a module whose tools -// are served from a container built on the tool runtime's image — or nothing when it is not. Judged -// from the manifest's own `build.on` when it is a repository manifest, and from what its build stood -// on when it is a built one, because a resolved manifest carries no build. The gate itself is -// registration's (to-be 38 WP2.4): once the runtime module is in the catalogue, this is refused. -// -// Three things must hold, and each alone is fine: declaring tools (a bundle does that); a container -// (a module's service may well be one); building on the runtime's image (a service written against -// the SDK may). All three is a container whose purpose is tools, which the runtime now serves. -func ToolContainerOnTheRuntime(m Manifest, against []string) string { - if len(m.Tools) == 0 { - return "" - } - container := false - for _, r := range m.Resources { - if fmt.Sprint(r["type"]) == "container" { - container = true - } - } - if !container { - return "" - } - onTheRuntime := false - if m.Build != nil { - for _, on := range m.Build.On { - if on.Module == RuntimeImageModule && on.Artifact == RuntimeImageArtifact { - onTheRuntime = true - } - } - } - for _, ref := range against { - path, kept := InArtifactStore(Recorded(ref)) - if kept && strings.HasPrefix(path, RuntimeImageModule+"/"+RuntimeImageArtifact+"@") { - onTheRuntime = true - } - } - if !onTheRuntime { - return "" - } - return fmt.Sprintf( - "%s declares tools and a container built on %s's %s image — a container whose purpose is "+ - "serving tools. The node's tool runtime (%s) serves every module's tools from its bundle "+ - "now (novox/hq ADR 0175, to-be 38); declare the tools as a bundle and drop the container", - m.Module, RuntimeImageModule, RuntimeImageArtifact, RuntimeModule) -} - // bundleWords is what one module's tools bundles are given on this machine (novox/hq ADR 0192): // each loaded bundle's env, its ${dir:…} resolved to where this machine places the module's // directories and its ${port:…} to the port this machine gave it — the same resolution a diff --git a/go/catalogue/runtime_gate_test.go b/go/catalogue/runtime_gate_test.go index cc4ddfb..69ca70c 100644 --- a/go/catalogue/runtime_gate_test.go +++ b/go/catalogue/runtime_gate_test.go @@ -1,10 +1,5 @@ package catalogue -import ( - "strings" - "testing" -) - // The packet-filter manifest as it was the day the runtime was decided (novox/hq ADR 0175): tools, // served from a container built on the tool runtime's image, with NET_ADMIN so the container could // reach the filter. The exact pattern to-be 38 WP4 moves it off, and the one the gate refuses. @@ -37,52 +32,3 @@ const thePacketFilterAsItWas = `{ "artifacts": [{"name": "runtime", "kind": "image", "from": "Dockerfile"}] } }` - -func TestAToolContainerOnTheRuntimeImageIsNamedForWhatItIs(t *testing.T) { - m, err := decodeForTest([]byte(thePacketFilterAsItWas)) - if err != nil { - t.Fatal(err) - } - // From the repository: the manifest says what it builds on. - why := ToolContainerOnTheRuntime(m, nil) - if why == "" { - t.Fatal("the packet filter's tool container was not recognised from its build") - } - for _, word := range []string{"nftables", "mesh-tools", "runtime", "ADR 0175", "bundle"} { - if !strings.Contains(why, word) { - t.Errorf("the refusal does not say %q: %s", word, why) - } - } - - // Built: the manifest carries no build, and what it stood on says the same. - built, err := m.Resolve([]Built{{Name: "runtime", Kind: ArtifactImage, - Reference: ArtifactStoreScheme + "nftables/runtime@" + digest}}) - if err != nil { - t.Fatal(err) - } - stoodOn := []string{"anchor.internal:5100/mesh-tools/build@" + digest, "anchor.internal:5100/mesh-tools/runtime@" + digest} - if ToolContainerOnTheRuntime(built, stoodOn) == "" { - t.Error("the packet filter's tool container was not recognised from what its build stood on") - } - if ToolContainerOnTheRuntime(built, nil) != "" { - t.Error("a built manifest with no record of its base was judged to be on the runtime") - } - - // Each of the three alone is an ordinary module. - bundle := m - bundle.Resources = m.Resources[:len(m.Resources)-1] - if ToolContainerOnTheRuntime(bundle, nil) != "" { - t.Error("a module with tools and no container is the pattern the runtime serves, and was refused") - } - service := m - service.Tools = nil - if ToolContainerOnTheRuntime(service, nil) != "" { - t.Error("a service built against the SDK, declaring no tools, was refused") - } - elsewhere := m - elsewhere.Build = &Build{On: []BuildsOn{{Arg: "NODE_BASE", Image: "node@" + digest}}, - Artifacts: m.Build.Artifacts} - if ToolContainerOnTheRuntime(elsewhere, nil) != "" { - t.Error("a tool container on a public base was refused as though it were on the runtime's") - } -} diff --git a/go/catalogue/seat_contributions.go b/go/catalogue/seat_contributions.go index 192b7c0..f8068e5 100644 --- a/go/catalogue/seat_contributions.go +++ b/go/catalogue/seat_contributions.go @@ -6,14 +6,6 @@ import ( "strings" ) -// A module contributes to a seat it does not hold (novox/hq ADR 0212). -// -// ADR 0210 made a tool's configuration its seat holder's, and every other module's way in a -// contribution to the seat. The environment, the shell's slots and the power moments each became a -// field of their own; this is the general form, so a new seat that takes contributions is a row in -// the seat table rather than a change to the manifest: a contribution names a seat, a kind that -// seat receives, and text in the tool's own grammar, which the controller never reads. - // HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5). const HotkeysSeat = "node-hotkeys" diff --git a/go/catalogue/seat_contributions_test.go b/go/catalogue/seat_contributions_test.go index 7af7046..2d6de7a 100644 --- a/go/catalogue/seat_contributions_test.go +++ b/go/catalogue/seat_contributions_test.go @@ -1,8 +1,6 @@ package catalogue import ( - "reflect" - "strings" "testing" ) @@ -64,21 +62,6 @@ func TestNoContributionPlacesNothing(t *testing.T) { } } -func TestAContributionDependsOnItsSeat(t *testing.T) { - m := Manifest{Module: "laptop", Contributions: []SeatContribution{ - {Seat: HotkeysSeat, Kind: "trigger", Content: "x"}, - {Seat: DisplaySessionSeat, Kind: "config", Content: "y"}, - }} - if got, want := DependsOn(nil, m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) { - t.Errorf("depends on %v, want %v", got, want) - } - catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()} - if _, err := AssignRefusal(catalogue, "laptop", nil, []string{"laptop"}); err == nil || - !strings.Contains(err.Error(), HotkeysSeat) { - t.Errorf("a contributor without the holder was accepted: %v", err) - } -} - func TestTheHotkeysSeatIsTheMeshsAndReceivesTriggers(t *testing.T) { s, ok := SeatNamed(HotkeysSeat) if !ok || s.Scope != ScopeNode || len(s.Receives) != 1 || s.Receives[0].Kind != "trigger" { diff --git a/go/catalogue/seat_data.go b/go/catalogue/seat_data.go index 8397762..e63d9c6 100644 --- a/go/catalogue/seat_data.go +++ b/go/catalogue/seat_data.go @@ -11,21 +11,6 @@ import ( "text/template" ) -// A kind a seat receives as data, rendered by its holder (novox/hq ADR 0255). -// -// ADR 0212 made a contribution text in the tool's own grammar, which the controller places and never -// reads. That is right where the tool is the seat's for good — a hotkey daemon's trigger lines — and -// wrong where the seat's whole point is that its tool can be replaced: a module adding a battery to the -// bar would write i3status-rust's TOML, and a second bar would have to read it. So a seat may define a -// kind as data instead. The seat says the data's shape; a contributor gives data in that shape; the -// holder gives a template that renders one piece of it into its tool's grammar, as a module's facts -// template renders the roster (`facts`): the data is the mesh's, the format is the holder's. -// -// What the holder places is narrowed by the shape's placing fields — the bar's `bar` and `place` — in -// the placeholder, `${contribution:::=,…}`, and every combination of them is -// placed by the holder exactly once, so nothing a module contributes is dropped by a holder that forgot -// a corner of the shape. - // Shape is what a kind received as data looks like. type Shape struct { // Places are the fields a holder narrows its placeholders by, each with every value it takes. diff --git a/go/catalogue/seat_dependencies.go b/go/catalogue/seat_dependencies.go index bde015a..3bf664e 100644 --- a/go/catalogue/seat_dependencies.go +++ b/go/catalogue/seat_dependencies.go @@ -6,20 +6,6 @@ import ( "strings" ) -// A module depends on the node seats that apply its resources (novox/hq ADR 0207), on the -// seats it contributes to (novox/hq ADR 0210), and on the seats a seat it holds needs beside it -// (novox/hq ADR 0220). The third source once served only compiled seats and had one user, -// node-resolver-config needing node-uplink, which went with that seat when the resolver file became -// the uplink's own (novox/hq ADR 0223) — and now serves module-declared seats instead: mail-notifier -// needing node-notifier, and the like (novox/hq ADR 0378 §8). -// -// Some of what a module declares is applied through software on the machine that is itself a -// module: a service through the service manager, a package through the package manager, a container -// through the container runtime. A *capability* only says that software is installed; it does not -// say that a module of the mesh holds the role and answers for it. So the dependency is derived from -// the resources — never stated in a manifest, because a module that adds a service and forgets a -// field would pass — and is met when some module assigned to the same node holds the seat. - // The three seats that apply resources (novox/hq ADR 0207 §1). Named once, because the derivation, // the seed and the messages all turn on these strings. const ( @@ -50,14 +36,6 @@ var appliedThrough = map[string]string{ // A variable rather than a constant only so a test can hold both behaviours; nothing else sets it. var enforceSeatDependencies = true -// EnforcingSeatDependencies sets the switch and returns what puts it back. For tests in other -// packages that hold the behaviour from before the switch; nothing else calls it. -func EnforcingSeatDependencies(on bool) (restore func()) { - was := enforceSeatDependencies - enforceSeatDependencies = on - return func() { enforceSeatDependencies = was } -} - // foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5): // the host and the private network. Registered as modules so they can be assigned, but what they // declare is the installation's, not a module's, so it is never judged. The third piece, the @@ -265,128 +243,3 @@ func nodeSeatsClaimed(m Manifest) []string { } return out } - -// manifestsOf is the catalogue's definitions of the named modules; a name the catalogue does not -// know contributes nothing, as it does to a resolution. -func manifestsOf(catalogue map[string]Manifest, names []string) []Manifest { - var out []Manifest - seen := map[string]bool{} - for _, n := range names { - if m, known := catalogue[n]; known && !seen[n] { - seen[n] = true - out = append(out, m) - } - } - return out -} - -// AssignRefusal is why assigning `adding` to a node already running `assigned` is refused, or -// nothing (novox/hq ADR 0207 §4): each new module's dependency the node's assignments, the new ones -// included, leave unmet. -// -// **Only the new modules are judged.** A node already short of a holder is reported by `status`; -// refusing an unrelated assignment for it would make the remedy — assigning the holder — refused too. -// -// **A dependency nothing in the catalogue can meet is said, not refused.** A refusal names the -// module that would meet it; with none registered there is no remedy to name, and refusing would -// stop every assignment of that kind until a module that does not exist yet is written. The answer -// still says it, and `status` reports it — before the switch and after it alike: there is never a -// remedy to name for it. The first return is those lines. -func AssignRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) ([]string, error) { - set := manifestsOf(catalogue, append(append([]string(nil), assigned...), adding...)) - judged := map[string]bool{} - for _, a := range adding { - judged[a] = true - } - var refused, said []string - for _, u := range UnheldDependencies(catalogue, node, set, judged) { - if len(u.Holders) == 0 { - said = append(said, u.String()) - continue - } - refused = append(refused, u.String()) - } - if len(refused) > 0 { - return said, &Refusal{Problems: append(refused, - fmt.Sprintf("holders that depend on each other are assigned together: `assign %s …`", node))} - } - return said, nil -} - -// UnassignRefusal is why taking `removing` off a node running `assigned` is refused, or nothing -// (novox/hq ADR 0207): a seat the removed modules hold that nothing left on the node holds, while -// a module left there depends on it. Names the dependents, because they are what must go first — -// or the holder's replacement come. -func UnassignRefusal(catalogue map[string]Manifest, node string, assigned, removing []string) error { - gone := map[string]bool{} - for _, r := range removing { - gone[r] = true - } - var left []string - for _, a := range assigned { - if !gone[a] { - left = append(left, a) - } - } - before := map[string]bool{} - for _, u := range UnheldDependencies(catalogue, node, manifestsOf(catalogue, assigned), nil) { - before[u.Module+"\x00"+u.Seat] = true - } - dependents := map[string][]string{} - for _, u := range UnheldDependencies(catalogue, node, manifestsOf(catalogue, left), nil) { - if before[u.Module+"\x00"+u.Seat] { - continue // unmet already; not this removal's doing - } - dependents[u.Seat] = append(dependents[u.Seat], u.Module) - } - if len(dependents) == 0 { - return nil - } - seats := make([]string, 0, len(dependents)) - for s := range dependents { - seats = append(seats, s) - } - sort.Strings(seats) - var problems []string - for _, s := range seats { - problems = append(problems, fmt.Sprintf( - "%s holds %s on %s, and %s depend on it (novox/hq ADR 0207) — unassign them with it, "+ - "or assign another holder first", strings.Join(removing, ", "), s, node, - strings.Join(dependents[s], ", "))) - } - return &Refusal{Problems: problems} -} - -// CollisionRefusal is why assigning `adding` beside `assigned` is refused for what two modules -// would both declare, or nothing (novox/hq ADR 0210 §1, 04-ISSUES/235): one path, unit or name, or a package one -// declares present and another absent. A package several modules declare present is no collision (ADR 0303). -// -// **Refused, not kept like an unresolved provision.** An assignment is otherwise kept when the -// node does not resolve, because assignment is not an ordering: a consumer's provider can follow. -// A collision is not an order anything can complete — no further assignment makes two owners of one -// file one owner — and kept, it leaves the node unresolvable, so the next push of anything drops -// it from the mesh. Only collisions involving a module being added are refused; one already on the -// node is `status`'s, and refusing an unrelated assignment for it would block its own remedy. -func CollisionRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) error { - before := map[string]bool{} - for _, p := range checkResources(manifestsOf(catalogue, assigned)) { - before[p] = true - } - var problems []string - for _, p := range checkResources(manifestsOf(catalogue, append(append([]string(nil), assigned...), adding...))) { - if before[p] { - continue // on the node already; not this assignment's doing - } - if strings.Contains(p, "ADR 0303") { - problems = append(problems, p) // a package declared present and absent says its own rule - continue - } - problems = append(problems, p+" (novox/hq ADR 0210: one owner per node; the other module "+ - "depends on the owner's seat instead)") - } - if len(problems) == 0 { - return nil - } - sort.Strings(problems) - return &Refusal{Problems: append(problems, fmt.Sprintf("nothing was assigned to %s", node))} -} diff --git a/go/catalogue/seat_dependencies_test.go b/go/catalogue/seat_dependencies_test.go index 7c717d6..0a97ecc 100644 --- a/go/catalogue/seat_dependencies_test.go +++ b/go/catalogue/seat_dependencies_test.go @@ -1,7 +1,6 @@ package catalogue import ( - "errors" "reflect" "strings" "testing" @@ -85,86 +84,6 @@ func TestTheSeatsThatApplyResourcesAreTheMeshsOwnAtNodeScope(t *testing.T) { } } -func TestANodeWhoseAssignmentsHoldTheSeatsResolvesWithNothingUnheld(t *testing.T) { - web := withResources(mod("web", nil, nil, nil), res("container", "web"), res("service", "web-timer")) - cat := shelf(append(coreThree(), web)...) - got, err := Resolve(cat, []string{"systemd", "pacman", "docker", "web"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - if len(got.Unheld) != 0 { - t.Errorf("a node holding all three seats reports %v", got.Unheld) - } - if _, err := AssignRefusal(cat, "workstation", []string{"systemd", "pacman", "docker"}, []string{"web"}); err != nil { - t.Errorf("assigning beside the three holders was refused: %v", err) - } -} - -func TestAnAssignmentMissingAHolderIsRefusedNamingTheSeatAndItsPossibleHolders(t *testing.T) { - web := withResources(mod("web", nil, nil, nil), res("container", "web"), res("service", "web-timer")) - cat := shelf(append(coreThree(), web)...) - _, err := AssignRefusal(cat, "workstation", []string{"systemd", "pacman"}, []string{"web"}) - var refusal *Refusal - if !errors.As(err, &refusal) { - t.Fatalf("web assigned to a node without a container runtime was not refused: %v", err) - } - msg := err.Error() - for _, want := range []string{"web on workstation depends on " + ContainerRuntimeSeat, "docker", "ADR 0207"} { - if !strings.Contains(msg, want) { - t.Errorf("the refusal does not say %q:\n%s", want, msg) - } - } - // What the node does hold is not named as missing. - if strings.Contains(msg, "depends on "+ServiceManagerSeat) { - t.Errorf("the refusal names a seat systemd already holds:\n%s", msg) - } -} - -func TestADependencyNoCatalogueModuleCanMeetIsSaidNeverRefused(t *testing.T) { - // No runtime module in the catalogue: refusing would stop every container's assignment until one - // is written, with no remedy to name — so it is said, with the switch on as with it off. - web := withResources(mod("web", nil, nil, nil), res("container", "web")) - cat := shelf(web) - for _, on := range []bool{false, true} { - enforceSeatDependencies = on - said, err := AssignRefusal(cat, "workstation", nil, []string{"web"}) - if err != nil { - t.Fatalf("switch %v: a dependency nothing could meet was refused: %v", on, err) - } - if len(said) != 1 || !strings.Contains(said[0], "no module in the catalogue claims it yet") { - t.Errorf("switch %v: the assignment does not say what it depends on: %v", on, said) - } - if _, err := Resolve(cat, []string{"web"}, workstation(), World{}); err != nil { - t.Errorf("switch %v: a dependency nothing could meet refused the node: %v", on, err) - } - } - enforceSeatDependencies = true -} - -func TestTheHoldersMutualDependenceResolvesWhenAssignedTogether(t *testing.T) { - cat := shelf(coreThree()...) - // Alone, each needs the other. - if _, err := AssignRefusal(cat, "workstation", nil, []string{"systemd"}); err == nil || - !strings.Contains(err.Error(), "pacman") { - t.Errorf("systemd alone was not refused naming pacman: %v", err) - } - if _, err := AssignRefusal(cat, "workstation", nil, []string{"pacman"}); err == nil || - !strings.Contains(err.Error(), "systemd") { - t.Errorf("pacman alone was not refused naming systemd: %v", err) - } - // Together, in one act, they meet each other — and docker meets its own seat. - if _, err := AssignRefusal(cat, "workstation", nil, []string{"systemd", "pacman", "docker"}); err != nil { - t.Errorf("the three holders assigned together were refused: %v", err) - } - got, err := Resolve(cat, []string{"systemd", "pacman"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - if len(got.Unheld) != 0 { - t.Errorf("systemd and pacman together report %v", got.Unheld) - } -} - func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) { // Before the switch (the state the mesh ran in until every node held the three seats). enforceSeatDependencies = false @@ -196,33 +115,6 @@ func TestWithTheSwitchFlippedAnUnmetDependencyRefusesTheNode(t *testing.T) { } } -func TestTheFoundationsDeclarationsAreNotJudged(t *testing.T) { - // The private network, as the controller computes it: its tools' package and its service are - // the mesh's, written per node, and so are never a module's dependency. - network := withResources(mod("mesh-wireguard", []string{"private-network"}, nil, nil), - res("package", "wireguard-tools"), res("service", "overlay-up")) - network.Computed = "mesh-wireguard" - // The host, whatever it declares. - host := withResources(mod("mesh-host", nil, nil, nil), res("file", "launcher"), res("service", "nox-mesh-host")) - cat := shelf(append(coreThree(), network, host)...) - - for _, m := range []Manifest{network, host} { - if d := DependsOn(cat, m); len(d) != 0 { - t.Errorf("%s, the foundation's, depends on %v", m.Module, d) - } - } - got, err := Resolve(cat, []string{"mesh-wireguard", "mesh-host"}, workstation(), World{}) - if err != nil { - t.Fatal(err) - } - if len(got.Unheld) != 0 { - t.Errorf("the foundation on a node with no holders reports %v", got.Unheld) - } - if _, err := AssignRefusal(cat, "workstation", nil, []string{"mesh-wireguard", "mesh-host"}); err != nil { - t.Errorf("assigning the foundation was refused: %v", err) - } -} - // A module-declared seat's needs are judged exactly as a compiled seat's once were (novox/hq ADR // 0220, ADR 0378 §8): "mail" declares the node seat "mail-notifier" needing node-notifier, the // mesh's own (desktop notifications); "notifier" claims mail-notifier; "dunst" claims node-notifier, @@ -236,42 +128,3 @@ func mailNotifierAndDunst() map[string]Manifest { mod("dunst", nil, nil, nil, Claim{Name: NotifierSeat}), ) } - -func TestAModuleDeclaredSeatsNeedsAreJudgedLikeACompiledSeatsOnceWere(t *testing.T) { - cat := mailNotifierAndDunst() - _, err := AssignRefusal(cat, "workstation", nil, []string{"notifier"}) - var refusal *Refusal - if !errors.As(err, &refusal) || !strings.Contains(err.Error(), "node-notifier") { - t.Fatalf("notifier assigned without dunst was not refused naming node-notifier: %v", err) - } - if _, err := AssignRefusal(cat, "workstation", []string{"dunst"}, []string{"notifier"}); err != nil { - t.Errorf("notifier beside dunst was refused: %v", err) - } - if err := UnassignRefusal(cat, "workstation", []string{"dunst", "notifier"}, []string{"dunst"}); err == nil || - !strings.Contains(err.Error(), "node-notifier") { - t.Errorf("taking dunst from under notifier was not refused: %v", err) - } -} - -func TestUnassigningTheLastHolderWhileDependentsRemainIsRefused(t *testing.T) { - sshd := withResources(mod("sshd", nil, nil, nil), res("service", "sshd")) - cat := shelf(append(coreThree(), sshd)...) - on := []string{"systemd", "pacman", "docker", "sshd"} - - err := UnassignRefusal(cat, "workstation", on, []string{"systemd"}) - if err == nil { - t.Fatal("the last service manager came off a node still running services") - } - for _, want := range []string{ServiceManagerSeat, "sshd", "pacman", "docker"} { - if !strings.Contains(err.Error(), want) { - t.Errorf("the refusal does not name %q:\n%v", want, err) - } - } - // A dependent comes off freely, and the holders with everything depending on them in one act. - if err := UnassignRefusal(cat, "workstation", on, []string{"sshd"}); err != nil { - t.Errorf("a dependent's unassignment was refused: %v", err) - } - if err := UnassignRefusal(cat, "workstation", on, on); err != nil { - t.Errorf("unassigning everything together was refused: %v", err) - } -} diff --git a/go/catalogue/seat_into.go b/go/catalogue/seat_into.go index ea7c833..ad36dad 100644 --- a/go/catalogue/seat_into.go +++ b/go/catalogue/seat_into.go @@ -9,37 +9,6 @@ import ( "strings" ) -// Telling a module where this machine put the holder of a seat. -// -// **The foundation's ports are the node's** (novox/hq ADR 0100): the port a foundation server was -// given at genesis becomes that node's setting for the module that serves it, and every reader -// follows the setting. Every consumer's binding did. The control plane's own connections did not -// (04-ISSUES/102): they are written at genesis, before any module exists to bind to — full -// connection strings, sealed, with the port inside — so when the node moved the store, the -// control plane went on dialling where genesis had written and the mesh was headless. -// -// The control plane cannot open its own sealed connection to move the port, and it cannot bind -// the store as a consumer would: a binding mints a credential, and what the control plane holds -// is the foundation's superuser, made before the mesh. What it can do is read the node's settings -// when it composes its own declaration — it is the thing that composes every other module's — and -// say in its own environment which port this machine put the store at. -// -// So a module may ask about a **seat** (ADR 0079: a foundation seat is named after the server it -// guards — `mesh-store`, `mesh-broker`). `${seat:mesh-store:5432}` is "the port this machine put -// the holder of the mesh-store seat's 5432 at". Not a provision: nothing is required, nothing is -// granted, no credential is minted. A seat is the mesh's own vocabulary for the store and the -// broker, which is what makes this the control plane's way of naming them and not a way for a -// module to reach a server it was not granted — the answer is a port number the mesh holds in the -// clear, and the credential to use it is still the module's own to have. -// -// **The answer may be empty, and that is the one place a placeholder answers with nothing.** The -// store and the broker are raised at genesis, before the mesh knows them as modules; a mesh raised -// on the catalogue's own ports never gives them a setting at all. In both, the port genesis wrote -// into the connection string is the right one, and the mesh has nothing to add. An empty answer -// says exactly that, and what reads it — the control plane's `_PORT` twin — treats an empty value -// as no value. Answering with the software's own port instead would override what genesis wrote -// with a number the mesh never checked, on the one machine where that is a headless mesh. - // ofSeat is where a module asks about a seat: ${seat::}. var ofSeat = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):([0-9]+)\}`) diff --git a/go/catalogue/seat_invokes.go b/go/catalogue/seat_invokes.go index 6e5d59d..d16a0b1 100644 --- a/go/catalogue/seat_invokes.go +++ b/go/catalogue/seat_invokes.go @@ -4,12 +4,6 @@ import ( "strings" ) -// A seat verb a module invokes is one some holder serves (issue 774, sweep item 1). messenger invoked -// `seat:node-engine.root-free`, which nothing in the mesh answered, so every ask that offers an approval was held on -// Telegram, silently. A manifest names what it calls (novox/hq ADR 0152); that a seat by that name exists, promises -// that verb, and has a holder is only known over the whole catalogue, so it is judged here, beside the seats' other -// rules (CatalogueProblems). - // UnheldSeat ends the problem of a `seat:.` whose seat no module given claims: over the whole catalogue a // refusal, over some manifests alone a seat whose holder was not given (IsUndeclaredSeat says both). const UnheldSeat = "which no module claims, so nothing answers it" diff --git a/go/catalogue/seat_paths.go b/go/catalogue/seat_paths.go index da4dacd..e112ccc 100644 --- a/go/catalogue/seat_paths.go +++ b/go/catalogue/seat_paths.go @@ -6,23 +6,6 @@ import ( "strings" ) -// The paths a seat's holder owns (novox/hq ADR 0287). -// -// **Who owns a machine-wide file is the role's question, answered once.** Until this, which module owned -// /etc/hosts or /etc/resolv.conf was decided record by record (ADRs 0199, 0222, 0223, 0241) and enforced -// only after the fact, by checkResources finding two modules on one machine declaring the same path: a -// module that did not hold node-hostname could declare /etc/hosts on a machine where nobody held it, and -// nothing refused it. Now a seat's definition names the paths the role owns on any machine, and three -// refusals follow from it, each where the mesh can still say no: -// -// - a claim whose module does not declare every path its seat names, by CanHold, at registration and at -// handover; -// - a module declaring a path a seat names while claiming none of the seats that name it, at -// registration (CatalogueProblems), and what the controller generates for a machine, at composition; -// - a module's own seat naming a path another seat already names, at registration — the second loses. -// -// Reading a seat's path is access, never a resource, so it stays allowed. - // SeatPath is one path a seat's holder owns: absolute, literal, on the machine where the holder runs. type SeatPath struct { Path string `json:"path"` @@ -143,66 +126,6 @@ func unclaimedSeatPaths(m Manifest, named map[string][]string) []string { return problems } -// PathOwner is the module that owns one seat's path on one machine (novox/hq ADR 0287): the seat naming -// the path, and that seat's holder there, with the stepping back applied. -type PathOwner struct { - Path string `json:"path"` - Node string `json:"node"` - Seat string `json:"seat"` - Module string `json:"module"` -} - -// PathOwners is, for every path a seat in the set names and every machine where a seat naming it is held, -// who owns the path there. A seat that steps back for another held on the same machine is passed over. -func PathOwners(set []Seat, held []Held) []PathOwner { - // node → seat → holding module - holders := map[string]map[string]string{} - for _, h := range held { - s, ok := SeatNamed(h.Claim) - if !ok || h.Scope != ScopeNode || s.Scope != ScopeNode { - continue - } - if holders[h.Node] == nil { - holders[h.Node] = map[string]string{} - } - if _, taken := holders[h.Node][s.Name]; !taken { - holders[h.Node][s.Name] = h.Module - } - } - naming := map[string][]Seat{} // path → the seats naming it - steps := map[string]map[string]string{} // path → seat → the seat it steps back for - for _, s := range set { - for _, p := range s.Paths { - naming[p.Path] = append(naming[p.Path], s) - if p.StepsBackFor != "" { - if steps[p.Path] == nil { - steps[p.Path] = map[string]string{} - } - steps[p.Path][s.Name] = p.StepsBackFor - } - } - } - var out []PathOwner - for _, p := range sortedKeys(naming) { - for _, node := range sortedKeys(holders) { - for _, s := range naming[p] { - module, holds := holders[node][s.Name] - if !holds { - continue - } - if forSeat := steps[p][s.Name]; forSeat != "" { - if _, there := holders[node][forSeat]; there { - continue - } - } - out = append(out, PathOwner{Path: p, Node: node, Seat: s.Name, Module: module}) - break - } - } - } - return out -} - // seatPathsOnMachine is each path the modules of one machine declare that a seat names, for a module // claiming none of the seats naming it (novox/hq ADR 0287). Registration refuses the same of a manifest; // this holds what the controller generates for a machine to it too, because a computed module's resources diff --git a/go/catalogue/seat_paths_test.go b/go/catalogue/seat_paths_test.go index 44faa65..27fa3f1 100644 --- a/go/catalogue/seat_paths_test.go +++ b/go/catalogue/seat_paths_test.go @@ -79,30 +79,6 @@ func TestAGeneratedResourceOnASeatsPathIsRefused(t *testing.T) { } } -// Who owns a seat's path on a machine: the seat naming it and its holder there, with the stepping back -// applied — the resolver's holder where node-resolver is held, else the uplink's. -func TestPathOwnersApplyTheSteppingBack(t *testing.T) { - held := []Held{ - {Claim: "node-uplink", Scope: ScopeNode, Node: "laptop", Module: "networkmanager"}, - {Claim: ResolverSeat, Scope: ScopeNode, Node: "laptop", Module: "systemd-resolved"}, - {Claim: "node-uplink", Scope: ScopeNode, Node: "server", Module: "systemd-networkd"}, - {Claim: "node-hostname", Scope: ScopeNode, Node: "server", Module: "hostname"}, - } - var got []string - for _, o := range PathOwners(Seats(), held) { - got = append(got, o.Path+" "+o.Node+" "+o.Seat+" "+o.Module) - } - want := []string{ - "/etc/hostname server node-hostname hostname", - "/etc/hosts server node-hostname hostname", - "/etc/resolv.conf laptop node-resolver systemd-resolved", - "/etc/resolv.conf server node-uplink systemd-networkd", - } - if strings.Join(got, "\n") != strings.Join(want, "\n") { - t.Errorf("the owners are\n%s\nnot\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) - } -} - // The stepping back comes from the seat, and is of a rendered file only: an uplink holder declaring the // resolver file as a resource beside a node-resolver holder is two writers of one path, refused when the // machine is resolved — never composed twice — as before the seat named its paths. diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 84bc33c..dd15c89 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -5,17 +5,6 @@ import ( "strings" ) -// The seats a mesh can have (novox/hq ADR 0110). -// -// **A closed set, defined here rather than by whoever claims one.** Until this, a well-formed name -// became a seat by being claimed, so nothing could say which seats a mesh has or who fills them: -// `the-showcase` and `the-build-machine` were each invented by the module claiming it. The set is -// what a person reads to learn what a mesh can have, so an entry nobody argued for is an entry -// nobody can explain — the same reason every shape in the host's vocabulary names its decision. -// -// A seat is held by a module assignment. What the mesh knows about a holder is what it knows about -// that assignment; nothing about holders is kept here or anywhere else. - // Seat is one role the mesh defines. type Seat struct { // Name is what a manifest claims. @@ -604,10 +593,6 @@ func optionalAsCompiled(row, compiled []Verb) []Verb { // held record — still resolves to it after a rename, and nothing downstream has to change. var aliases = map[string]string{} -// UseAliases replaces the former-name map with the one the control plane read from its store. Empty -// is fine and ordinary: a mesh whose seats have never been renamed has no aliases. -func UseAliases(m map[string]string) { aliases = m } - // Seats is every seat the mesh defines, in reading order. func Seats() []Seat { return append([]Seat(nil), seats...) diff --git a/go/catalogue/seats_declared.go b/go/catalogue/seats_declared.go index 11278aa..f6414bc 100644 --- a/go/catalogue/seats_declared.go +++ b/go/catalogue/seats_declared.go @@ -5,20 +5,6 @@ import ( "strings" ) -// Seats a module declares of its own (novox/hq ADR 0118). -// -// The set of seats a mesh has is **derived**: the mesh's own, in seats.go, plus those declared by -// every module it has registered. Still closed — a seat named nowhere is refused — but computed -// from the catalogue rather than written in the controller, which is what ADR 0110 actually -// needed and a hand-maintained table could not keep. Its own evidence: the enumeration done by -// hand while that record was written reported eleven claims where there were thirteen. -// -// **What can be checked from one manifest and what cannot.** A declaration's shape, its scope, -// and the reserved prefix are facts about the manifest in front of you. Whether a seat anybody -// names actually exists, whether two modules declared the same one, and whether a holder -// satisfies the protocol are facts about the *catalogue* — so they are checked at registration, -// by CatalogueProblems, which is the last moment the mesh can still say no. - // A SeatDeclaration is a role a module offers on the bus: what may be sent to it, what it says, // and what it answers. A caller declares that it uses the *seat*, never the module, so the // implementation can be replaced under it. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index 653aaa3..7702bee 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -102,23 +102,3 @@ func TestUseSeatsReplacesTheSetButNeverEmptiesIt(t *testing.T) { t.Fatalf("the working set is %d seats, not the one that was loaded", len(Seats())) } } - -// A former name resolves to the seat it was renamed from (novox/hq ADR 0122), so a manifest's claim -// and a held record naming the old name break nothing after a rename. -func TestAFormerNameResolvesAfterARename(t *testing.T) { - defer func() { UseSeats(DefaultSeats()); UseAliases(nil) }() - UseSeats([]Seat{{Name: "git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0121"}}) - UseAliases(map[string]string{"git": "git"}) - - // The old name resolves to the renamed seat. - if s, ok := SeatNamed("git"); !ok || s.Name != "git" { - t.Fatalf("the former name did not resolve to the renamed seat: %+v ok=%v", s, ok) - } - // And a holder recorded under the old name is still found for the provision the seat delivers. - providers := []Provider{{Node: "anchor", At: "anchor.internal", Module: "gitea"}} - held := []Held{{Claim: "git", Scope: ScopeMesh, Node: "anchor", Module: "gitea"}} - holder, found := HolderAmong("git", providers, held) - if !found || holder.Module != "gitea" { - t.Fatalf("the holder recorded under the former name was not matched: %+v found=%v", holder, found) - } -} diff --git a/go/catalogue/secrets_into_files.go b/go/catalogue/secrets_into_files.go index 7baca99..7406a9b 100644 --- a/go/catalogue/secrets_into_files.go +++ b/go/catalogue/secrets_into_files.go @@ -7,22 +7,6 @@ import ( "strings" ) -// A credential and a configuration file meeting. -// -// **The gap this closes.** A granted credential arrives as a file whose entire content is the -// password. That is what a program reading a password file wants — and most programs do not read -// one. They read `KEY=value`, or a JSON document with the password at some path inside it, or a -// YAML file in a home directory. Before this, a module in that position could be handed the bare -// value or nothing, and both are useless. -// -// The mesh cannot compose the document, because it discarded the value (novox/hq ADR 0024). So -// the module supplies the document with a hole in it, the mesh delivers the value sealed beside -// it, and the host — the only thing that ever sees both — puts one into the other on the machine. -// -// The host has always been able to do this. Nothing filled the values in, so the hole could be -// written and never closed, and the host refused the file. That refusal was correct and the -// feature was unreachable. - // placeholder is what a module's file content says where a sealed value belongs: ${secret:name}. // // The same expression the host matches, written out again rather than shared: they are separate diff --git a/go/catalogue/served_by_setting_test.go b/go/catalogue/served_by_setting_test.go index 91c16b7..02e2848 100644 --- a/go/catalogue/served_by_setting_test.go +++ b/go/catalogue/served_by_setting_test.go @@ -138,41 +138,6 @@ func TestGrafanaComposesTheSameOnceInfluxdbServesItsSettings(t *testing.T) { } } -// What a provider serves is its definition's, filled from settings only where it asks with -// ${setting:} (novox/hq issue 696, ADR 0348 clause 11): a layer that sets a key the provider -// serves as a literal replaces nothing, and a key it does not serve is never added. -func TestALayerNoLongerReplacesAServedKey(t *testing.T) { - served, err := Settle(map[string]any{"host": "mail", "port": 25}, - []Layer{{From: "the mesh", Values: map[string]any{"host": "post", "sitename": "Mail"}}}) - if err != nil { - t.Fatal(err) - } - if served["host"] != "mail" { - t.Errorf("a layer replaced the served host: %v", served) - } - if _, leaked := served["sitename"]; leaked { - t.Errorf("a setting for a file reached the consumers: %v", served) - } - - // And a layer setting it is said to reach nothing, rather than taken for a change. - m := Manifest{Module: "mail", Serves: map[string]map[string]any{"smtp": {"host": "mail"}}} - if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"host": "post"}}}); len(unused) != 1 { - t.Errorf("a layer setting a literal served key was not said to reach nothing: %v", unused) - } - - // influxdb as it was before issue 696, serving literals, under a layer like ace's: the layer no - // longer reaches grafana — which is why the catalogue moves influxdb first. - got := composedLikeAce(t, influxdbServing(false), aceLikeLayer()) - for _, id := range []string{"grafana.influxdb-datasource", "grafana.bound-influxdb-api"} { - if strings.Contains(got[id], "made-up") { - t.Errorf("%s still carries the layer's value over a literal served key: %s", id, got[id]) - } - } - if !strings.Contains(got["grafana.influxdb-datasource"], `dbName: default\n`) { - t.Errorf("the data source does not name the served literal: %s", got["grafana.influxdb-datasource"]) - } -} - // influxdb's own runtime configuration takes the keys it names: org and bucket from a layer like // ace's, and not the endpoint block the mesh reads itself, which today's form wrote there too. func TestInfluxdbsConfigTakesItsOrgAndBucket(t *testing.T) { diff --git a/go/catalogue/served_own_names.go b/go/catalogue/served_own_names.go index 9599542..44eba42 100644 --- a/go/catalogue/served_own_names.go +++ b/go/catalogue/served_own_names.go @@ -5,22 +5,6 @@ import ( "sort" ) -// A provider may tell its consumers its own composed names (novox/hq ADR 0346, issue 624). -// -// **The name is already the mesh's.** A module contributes a label to a requirement — a route — and -// the mesh joins it with the providing machine's domains; the module reads the result in its own files -// as `${bound::name}` (novox/hq 04-ISSUES/122). Its consumers could not be told it: a -// served value named only the consumer (ADR 0202) and settings. The forge's package registry writes -// every tarball at its public name, and a consumer that must trust that name had no source for it but a -// second copy kept by hand. So a served value may say `${bound::}` for a requirement -// the provider contributes to, and the mesh fills it from the same composition the provider's own files -// and its proxy receive. -// -// **A name the providing machine does not compose leaves the key out** (ADR 0346 §3): a machine with -// no public domain has no public name to give, and an empty value or a refusal would be worse than a -// consumer told nothing — a refused provider machine takes every provision it offers down with it. The -// plan says which key was left out, and why, on the provider's machine and under each consumer's need. - // requirementsContributedTo is every requirement m contributes to, sorted. func requirementsContributedTo(m Manifest) []string { seen := map[string]bool{} @@ -101,22 +85,3 @@ func orHere(node string) string { } return node } - -// ServedLeftOut is what the providers in this node's own set leave out of what they serve, because -// this machine composes no name a served value asks for (ADR 0346 §3): said on the provider's machine, -// where the remedy is. -func (r Resolution) ServedLeftOut(settings SettingsBy) []string { - var said []string - for _, m := range r.Modules { - for _, provision := range sortedServes(m.Serves) { - _, lines, err := r.ServedOwnNames(m, provision, m.Serves[provision], WithDefaults(m, settings[m.Module])) - if err != nil { - said = append(said, fmt.Sprintf("%s serves %s, and its own names could not be composed: %v", - m.Module, provision, err)) - continue - } - said = append(said, lines...) - } - } - return said -} diff --git a/go/catalogue/served_own_names_test.go b/go/catalogue/served_own_names_test.go index 292947c..2ede50f 100644 --- a/go/catalogue/served_own_names_test.go +++ b/go/catalogue/served_own_names_test.go @@ -43,31 +43,6 @@ func TestAProviderServesItsOwnComposedName(t *testing.T) { } } -func TestANameTheMachineDoesNotComposeLeavesTheKeyOutAndSaysSo(t *testing.T) { - m := aForgeManifest(t) - r := Resolution{Node: "home"} // no public domain - got, said, err := r.ServedOwnNames(m, "npm-package-registry", m.Serves["npm-package-registry"], nil) - if err != nil { - t.Fatal(err) - } - if _, there := got["public-root"]; there { - t.Fatalf("a name not composed is left out, never written as the placeholder or empty: %v", got) - } - if got["npm-path"] == nil { - t.Fatalf("only the key that names it is left out: %v", got) - } - if len(said) != 1 || !strings.Contains(said[0], `forge serves npm-package-registry without "public-root"`) || - !strings.Contains(said[0], "home composes no such name") { - t.Fatalf("said: %v", said) - } - if lines := (Resolution{Node: "home", Modules: []Manifest{m}}).ServedLeftOut(nil); len(lines) != 1 { - t.Fatalf("the plan on the provider's machine says it: %v", lines) - } - if lines := (Resolution{Node: "home", PublicDomain: "example.org", Modules: []Manifest{m}}).ServedLeftOut(nil); len(lines) != 0 { - t.Fatalf("nothing to say when the name is composed: %v", lines) - } -} - // On the provider's own machine the consumer is served the same name (ADR 0346 §2). func TestAConsumerBesideTheProviderIsServedItsName(t *testing.T) { m := aForgeManifest(t) diff --git a/go/catalogue/setting_defaults.go b/go/catalogue/setting_defaults.go index 8f8ad4e..23339e9 100644 --- a/go/catalogue/setting_defaults.go +++ b/go/catalogue/setting_defaults.go @@ -4,22 +4,6 @@ import ( "sort" ) -// A preference has a default; the operator's own value has none (novox/hq ADR 0262, extending ADR -// 0112 and taking the default half of ADR 0164). -// -// `${setting:}` was refused whenever no layer set the key, and `settings set` refused a key no -// file asked for yet. Together they meant a running module could never gain a setting: the file that -// asks for it fails to compose until somebody sets it, and nobody can set it until the file asks. -// A definition may now give a key a default, with why, when the value is a **preference** — a font -// size, a width, a number of workers: something true of the software that a machine may tune. A -// value that is inherently the operator's — a domain, a public name, an identity, a secret — gets no -// default and stays refused by name until a layer sets it, which is what ADR 0112 and ADR 0155 were -// written for. -// -// The default is the lowest layer. The mesh's layer, then the node's, override it. It fills only -// `${setting:}`: a mergeable file's content already is its defaults, and laying a default over -// it would reach every mergeable file of the module (novox/hq issue 168). - // SettingDeclaration is one key's default, as the definition gives it. type SettingDeclaration struct { // Kind says what sort of value this is. `preference` is the only kind with a default; it is diff --git a/go/catalogue/setting_defaults_test.go b/go/catalogue/setting_defaults_test.go index 1d6489f..2d7c01d 100644 --- a/go/catalogue/setting_defaults_test.go +++ b/go/catalogue/setting_defaults_test.go @@ -97,18 +97,6 @@ func TestAComposedMachineGetsTheDefaultAndTheNodesValue(t *testing.T) { } } -// Setting a key the module gives a default is not refused as reaching nothing: it overrides the -// default, which is how a running module gains a setting with no gap between. -func TestAKeyWithADefaultIsNotStray(t *testing.T) { - m := notifier() - m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n" - stray := UnusedSettings(m, []Layer{{From: "laptop", Values: map[string]any{"font-size": float64(13), "colour": "red"}}}) - joined := strings.Join(stray, "; ") - if strings.Contains(joined, `"font-size"`) || !strings.Contains(joined, `"colour"`) { - t.Fatalf("stray: %s", joined) - } -} - // A default fills ${setting:…} only: it never becomes a key of a mergeable file (issue 168). func TestADefaultIsNotMergedIntoAJSONFile(t *testing.T) { m := notifier() @@ -274,42 +262,3 @@ func aModuleWithAFieldSetAsideIsComposedWhole(t *testing.T) { t.Fatalf("the left-out module's data is no longer backed up:\n%s", list) } } - -// A left-out module's backup lines are best effort: a line that cannot be placed — an access nobody -// placed, a placement setting that does not read — is said among what could not be placed, and the -// machine is declared (novox/hq ADR 0262). -func TestALeftOutModulesUnplaceableBackupLineCostsOnlyThatLine(t *testing.T) { - holder := Manifest{Module: "backups", Version: "1", - Claims: []Claim{{Name: BackupSeat, Scope: ScopeNode}}, - Resources: []map[string]any{{"id": "list", "type": "file", "path": "/etc/backups.list", "mode": "0644", - "content": "${contribution:" + BackupSeat + ":backup}"}}} - media := Manifest{Module: "media", Version: "1", - Accesses: []Access{{ID: "library", Mode: "read"}}, - Data: &Data{Own: []DataItem{{ID: "library", Path: "${access:library}", Class: "valuable"}}}} - placedBadly := Manifest{Module: "notes", Version: "1", - Resources: []map[string]any{{"id": "d", "type": "directory", "path": "/srv/notes", "mode": "0700"}}, - Data: &Data{Own: []DataItem{{ID: "d", Path: "${dir:d}", Class: "valuable"}}}} - r := anAdoptedAnchor() - r.Modules = append(r.Modules, holder, media, placedBadly) - with := anchorRendering(false) - with.Settings["notes"] = []Layer{LayerOf("anchor", map[string]any{PlacesSetting: "not a map"})} - composed, err := r.Compose(with) - if err != nil { - t.Fatalf("an unplaceable line of a left-out module failed the machine: %v", err) - } - for _, m := range []string{"media", "notes"} { - if _, left := composed.LeftOut[m]; !left { - t.Errorf("%s is not left out: %v", m, composed.LeftOut) - } - } - unplaced := strings.Join(composed.Unplaced, "\n") - for _, want := range []string{"media's backup for node-backup", "notes's backup for node-backup", "placement setting does not read"} { - if !strings.Contains(unplaced, want) { - t.Errorf("not said among what could not be placed: %q in\n%s", want, unplaced) - } - } - list, _ := byID(composed.Resources)["backups.list"]["content"].(string) - if strings.Contains(list, "/srv/notes") || strings.Contains(list, "${") { - t.Fatalf("a line was placed from the definition's default or unfilled:\n%s", list) - } -} diff --git a/go/catalogue/setting_in_owner_test.go b/go/catalogue/setting_in_owner_test.go index f441f64..ebd01e5 100644 --- a/go/catalogue/setting_in_owner_test.go +++ b/go/catalogue/setting_in_owner_test.go @@ -95,16 +95,6 @@ func TestAnOwnerSettingThatIsNoNumberIsRefused(t *testing.T) { } } -// A key a directory's owner asks for is a destination, so setting it is not called stray. -func TestASettingAnOwnerAsksForIsNotStray(t *testing.T) { - m := recycler() - m.Resources = m.Resources[:1] - stray := strings.Join(UnusedSettings(m, []Layer{{From: "ace", Values: map[string]any{"puid": "1001", "pgid": "2000"}}}), "; ") - if strings.Contains(stray, "puid") || strings.Contains(stray, "pgid") { - t.Errorf("called stray: %s", stray) - } -} - // An owner is written as the number parsed: "0001" and "00" reach the node-engine as 1 and 0, never as text. // Uid 0 is allowed, because a key an owner asks for is the terminal's alone. func TestAnOwnerSettingIsWrittenAsTheNumber(t *testing.T) { diff --git a/go/catalogue/setting_in_unit_test.go b/go/catalogue/setting_in_unit_test.go index 31ba7af..1f9d3ff 100644 --- a/go/catalogue/setting_in_unit_test.go +++ b/go/catalogue/setting_in_unit_test.go @@ -95,19 +95,6 @@ func TestASettingThatMakesNoUnitNameIsRefused(t *testing.T) { } } -// A key a unit's name asks for is a destination, so setting it is not called stray, and judging the -// settings sees it. -func TestASettingAUnitAsksForIsNotStrayAndIsJudged(t *testing.T) { - m := scrubber() - stray := strings.Join(UnusedSettings(m, []Layer{{From: "ace", Values: map[string]any{"scrub-pool": "storage"}}}), "; ") - if strings.Contains(stray, "scrub-pool") { - t.Errorf("called stray: %s", stray) - } - if err := JudgeSettings(m, nil, false); err == nil || !strings.Contains(err.Error(), "scrub-pool") { - t.Errorf("a unit's setting nothing sets is not refused when judged: %v", err) - } -} - func TestAnInstanceValueMayNotLeadWithADashNorBeLong(t *testing.T) { m := scrubber() for _, bad := range []string{"-storage", "--help", strings.Repeat("a", 65)} { diff --git a/go/catalogue/setting_into.go b/go/catalogue/setting_into.go index 0ec1d33..9d8dde0 100644 --- a/go/catalogue/setting_into.go +++ b/go/catalogue/setting_into.go @@ -9,22 +9,6 @@ import ( "strings" ) -// An operator's value, where a definition needs one (novox/hq ADR 0112, ADR 0155, design 27). -// -// A mail server's domain, a site's name, the address a proxy forwards from: values that are true of -// one installation and of no other, and that a module's software must be told. They had nowhere to -// live but the definition, which is how a catalogue meant for any mesh came to name this one -// (novox/hq issues 122, 134). ADR 0112 names the operator as one of the four providers; this is the -// operator answering. -// -// `${setting:}` in a file's content, in a service's unit name and in a directory's owner (a uid or -// gid only, issue 279) is filled from the module's settings layers — the mesh's, -// then this node's — the same layers a mergeable JSON file and a contribution already take, so -// `settings set ` is the one place a person's values go. **Refused when no layer sets it**, -// naming the key and the remedy: a definition that carried a default for a mail domain would be -// carrying the very literal this removes, and a blank written silently would be a service that -// comes up wrong somewhere that names neither the module nor the key. - // settingRef is how a definition asks for an operator's value: ${setting:}. var settingRef = regexp.MustCompile(`\$\{setting:([a-z0-9][a-z0-9_.-]*)\}`) @@ -150,24 +134,6 @@ func settingIntoUnit(resource map[string]any, layers []Layer, module string) err return nil } -// A declared directory may be owned by the account its module runs as (novox/hq issue 279). -// -// A containerised module runs as the identity its assignment sets per node — its puid and pgid -// settings, written into the environment file its image reads (ADR 0112, issue 153) — and a directory -// its app must write, beyond the one its image re-owns at start, was owned by whatever the manifest -// spelt: right on a machine whose identity matched the image's default, refused by the app on one whose -// did not. So a directory's `owner` may name the settings that identity is: `${setting:}` for the -// user alone, `${setting:}:${setting:}` for both, filled from the same layers at -// composition. -// -// **Whole numbers only.** An owner a setting fills is a uid or a gid — never an account's name, never -// text — so no value can put anything into the ownership the node-engine applies but a number; one that -// is not is refused at composition, naming the key, and the directory is left as it was. The shape is -// judged where the manifest is read (OwnerSettingProblems): each side of the colon a whole placeholder -// or a whole number, and every key one the module itself asks for elsewhere — in a file, a unit, a -// contribution or a served fact — or declares a default for, so the owner follows the identity its -// process is told it runs as, never a key that reaches nothing else. - // wholeID is a value as a uid or a gid, and whether it is one: a whole number below the kernel's "no id" // (2^32-1). Uid 0 is one: a key an owner asks for is the controller's terminal's alone (TerminalKeys). func wholeID(v string) (uint64, bool) { @@ -238,79 +204,6 @@ func orNoSettings(layers []Layer) string { return "; set today: " + strings.Join(keys, ", ") } -// settingKeysUsedBy is every key a module's files, units, directory owners, contributions and served -// facts ask for, so a setting that lands in one is not called stray. -func settingKeysUsedBy(m Manifest) map[string]bool { - used := settingKeysAskedOutsideOwners(m) - for _, r := range m.Resources { - if fmt.Sprint(r["type"]) == "directory" { - if owner, ok := r["owner"].(string); ok { - for _, k := range settingsUsed(owner) { - used[k] = true - } - } - } - } - return used -} - -// settingKeysAskedOutsideOwners is every key a module's files, units, items, contributions and served facts -// ask for: what a directory's owner may follow (OwnerSettingProblems). -func settingKeysAskedOutsideOwners(m Manifest) map[string]bool { - used := map[string]bool{} - note := func(s string) { - for _, k := range settingsUsed(s) { - used[k] = true - } - } - for _, r := range m.Resources { - switch fmt.Sprint(r["type"]) { - case "file": - if content, ok := r["content"].(string); ok { - note(content) - } - case "service": - if unit, ok := r["unit"].(string); ok { - note(unit) - } - case "container": - for _, g := range groupsOf(r) { - if s, ok := g.(string); ok { - note(s) - } - } - } - } - // An item's text, filled from its template (WithTemplatesFrom), places a key as a file's does (issue 1658). - for _, it := range m.Items { - note(it.Content) - } - inValues := func(values map[string]any) { - for _, v := range values { - if s, ok := v.(string); ok { - note(s) - } - } - } - for _, values := range m.Contributes { - inValues(values) - } - for _, locals := range m.ContributesMany { - for _, values := range locals { - inValues(values) - } - } - for _, values := range m.Serves { - inValues(values) - } - return used -} - -// A container's supplementary groups may be an operator's value (novox/hq ADR 0480 clause 7, ADR 0348 §11, issue 1636): -// n8n joins the media library's group, whose number is the machine's, never the definition's (ADR 0112 §1). Each -// element of `groups` is a whole placeholder or a whole number; a placeholder is filled with the number the -// config value holds, and the list reaches the node-engine as numbers. - // groupsOf is a container resource's groups as the manifest wrote them (text or numbers), none when it names none. func groupsOf(r map[string]any) []any { groups, _ := r["groups"].([]any) diff --git a/go/catalogue/setting_into_test.go b/go/catalogue/setting_into_test.go index 4b6f628..4183293 100644 --- a/go/catalogue/setting_into_test.go +++ b/go/catalogue/setting_into_test.go @@ -38,18 +38,3 @@ func TestASettingNothingSetIsRefusedByName(t *testing.T) { t.Fatalf("content was changed on refusal: %q", file["content"]) } } - -// A key a file asks for is a destination, so setting it is not called stray. -func TestASettingAFileAsksForIsNotStray(t *testing.T) { - m := Manifest{Module: "mail", Resources: []map[string]any{ - {"id": "env", "type": "file", "content": "DOMAIN=${setting:domain}\n"}, - }} - layers := []Layer{{From: "the mesh", Values: map[string]any{"domain": "example.tld", "stray": "x"}}} - unused := strings.Join(UnusedSettings(m, layers), "; ") - if strings.Contains(unused, `"domain"`) { - t.Fatalf("a key a file asks for was called stray: %s", unused) - } - if !strings.Contains(unused, `"stray"`) { - t.Fatalf("a key nothing reads was not named: %s", unused) - } -} diff --git a/go/catalogue/setting_takes.go b/go/catalogue/setting_takes.go index e62ec41..27ec55a 100644 --- a/go/catalogue/setting_takes.go +++ b/go/catalogue/setting_takes.go @@ -6,25 +6,9 @@ import ( "unicode" ) -// A mergeable file names the setting keys it takes (novox/hq ADR 0326, issue 168). -// -// Before this, every key of a module's layers merged into every one of its mergeable files: the -// mesh's own words, a key meant for a `${setting:}` in another file, a key for a second -// configuration file. So a module could not own two configurable files, and every receiver had to -// ignore what it did not know. ADR 0164 §11 decided that a setting names the files that read it; this -// is that relation, stated from the file's side, so the operator's own values stay declared nowhere -// (ADR 0262 §2). - // TakesSettingsField is the key a mergeable file carries to name the top-level setting keys it takes. const TakesSettingsField = "takes-settings" -// **A taken value may be laid under the file's own name** (issue 751). An entry of takes-config is a key, laid -// under that key, or {"key": , "as": }: the key stays a config key, -// lower-case, and the file is written in its software's spelling, `atlassian-url` as ATLASSIAN_URL. Like every -// taken value it is laid only when a layer sets it, never from a default, so an unset one leaves the file's bytes -// and the code's own fallback (its environment) as they were; for that reason it is never `required` -// (ConfigProblems). The settings form has no such entry: it is the config form's alone. - // Fields of a renamed takes-config entry. const ( takenKeyField = "key" @@ -66,15 +50,6 @@ func asTakenEntry(item any) (takenEntry, bool) { return takenEntry{}, false } -// takenKeyList is the keys a resource takes, in order: the config keys, never the file's names for them. -func takenKeyList(resource map[string]any) []string { - var keys []string - for _, e := range takenEntries(resource[TakesSettingsField]) { - keys = append(keys, e.key) - } - return keys -} - // takenKeys is the keys a resource names in takes-settings, each with the name the file has for it, and whether // it names any list at all. An empty list is a list: that file takes no setting. func takenKeys(resource map[string]any) (map[string]string, bool) { @@ -136,23 +111,6 @@ func takenNames(resource map[string]any) []string { return names } -// namesItsKeys says whether a module's mergeable files name the keys they take (ADR 0326 §2): true -// only when it has one and every one carries takes-settings. A module with a silent mergeable file -// keeps today's form (§3), where every key reaches every mergeable file. -func namesItsKeys(m Manifest) bool { - merging := 0 - for _, r := range m.Resources { - if how, _ := r["merge"].(string); how == "" { - continue - } - merging++ - if _, said := r[TakesSettingsField]; !said { - return false - } - } - return merging > 0 -} - // takenSettingsProblems is every rule of ADR 0326 §2 and §4, judged where a definition is checked and // where a stored one is composed. func takenSettingsProblems(m Manifest) []string { @@ -272,24 +230,3 @@ func usableKey(key string) bool { } return true } - -// takenSaid is what a stray key's message adds for a module whose files name their keys: which keys -// each file takes, so the person sees where the key would have had to be named. -func takenSaid(m Manifest) string { - if !namesItsKeys(m) { - return "" - } - var files []string - for _, r := range m.Resources { - if how, _ := r["merge"].(string); how == "" { - continue - } - keys := takenKeyList(r) - if len(keys) == 0 { - files = append(files, fmt.Sprintf("%v takes none", r["id"])) - continue - } - files = append(files, fmt.Sprintf("%v takes %s", r["id"], strings.Join(keys, ", "))) - } - return " (" + strings.Join(files, "; ") + ")" -} diff --git a/go/catalogue/setting_takes_test.go b/go/catalogue/setting_takes_test.go index c392906..5822728 100644 --- a/go/catalogue/setting_takes_test.go +++ b/go/catalogue/setting_takes_test.go @@ -86,32 +86,6 @@ func TestAnEmptyListTakesNoSetting(t *testing.T) { } } -func TestAFileThatNamesNothingKeepsTodaysForm(t *testing.T) { - // ADR 0326 §3, ADR 0164 §15: a module none of whose files names keys is left as it was. - m := twoFiles(nil, nil) - app := settledContent(t, m.Resources[0], []Layer{{From: "the mesh", Values: map[string]any{"url": "x"}}}) - if app["url"] != "x" { - t.Errorf("a silent file should still take every key until its module names them: %v", app) - } - if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"anything": 1}}}); len(unused) != 0 { - t.Errorf("today's form has nothing stray, and said: %v", unused) - } -} - -func TestAKeyNoFileNamesIsStray(t *testing.T) { - // ADR 0326 §5: once the files name their keys, a key none of them names — and nothing else - // takes — is stray, and is said with what each file takes. - m := twoFiles([]any{"server", "search"}, []any{"url"}) - unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{ - "server": 1, "url": "x", "site-name": "Search", "ui": map[string]any{"theme": "dark"}}}}) - if len(unused) != 1 || !strings.Contains(unused[0], `"ui"`) { - t.Fatalf("only ui reaches nothing; named: %v", unused) - } - if !strings.Contains(unused[0], "app takes server, search") || !strings.Contains(unused[0], "sidecar takes url") { - t.Errorf("the stray key's message does not say what each file takes: %s", unused[0]) - } -} - func containsText(list []string, text string) bool { for _, s := range list { if strings.Contains(s, text) { diff --git a/go/catalogue/settings.go b/go/catalogue/settings.go index 4c57f07..3b75ce3 100644 --- a/go/catalogue/settings.go +++ b/go/catalogue/settings.go @@ -13,17 +13,6 @@ import ( "strings" ) -// Settings are how somebody changes a config file a module owns, without editing it. -// -// Managed files are generated and never edited (novox/hq ADR 0011), so a person's intention has -// to be expressed somewhere the generator can see it. Then the file is produced from the module's -// defaults and that intention together, and upstream can rewrite its half freely. -// -// **An override beats a default, so there is nothing to resolve.** When the module changes a key -// somebody has set, the setting wins — it was a statement about that key, made deliberately, and -// the default was only ever what to do in the absence of one. This is the same shape as the rest -// of the mesh refusing to guess: there is no guess to make. - // Merge is how a file's content is combined with settings. A module names one per resource. const ( // MergeJSON treats the content as a JSON document and merges settings into it by key. @@ -389,107 +378,6 @@ func sortedKeysAny(m map[string]any) []string { return out } -// UnusedSettings names settings that reach nothing. -// -// Somebody who sets a key on a module with nothing mergeable, or misspells one, has changed -// nothing — and would find out by the machine not behaving differently, which is the slowest -// way there is. This is what makes that visible at the moment they set it. -// -// Where a key can land: any mergeable file takes any key; a file asking for `${setting:}` -// takes that key (ADR 0155); a contribution takes a key it declares, and no other (novox/hq -// 04-ISSUES/173); a served fact takes only the keys it asks for as `${setting:}`, since a layer -// replaces no served key (novox/hq issue 696); and the mesh's own words — `expose`, `ports`, `reach`, `endpoints` — -// are read by the mesh. A key none of those takes is stray, and is said so rather than dropped. -func UnusedSettings(m Manifest, layers []Layer) []string { - named := namesItsKeys(m) - for _, r := range m.Resources { - if how, _ := r["merge"].(string); how != "" && !named { - // Today's form (novox/hq ADR 0326 §3, ADR 0164 §15): a mergeable file that names no - // keys takes every key, so nothing is stray. - return nil - } - } - // A computed module has no resources here to look at — they are worked out per node, and - // whether a setting lands is not knowable until then. Silence rather than a wrong answer: - // claiming every setting on the private network is stray would be worse than saying nothing. - if m.Computed != "" { - return nil - } - - lands := settingKeysUsedBy(m) - // A mergeable file takes the keys it names (novox/hq ADR 0326 §5). - for _, r := range m.Resources { - for _, key := range takenNames(r) { - lands[key] = true - } - } - // A key the module gives a default reaches what asks for it (novox/hq ADR 0262): setting it - // overrides the default, which is how a running module gains a setting without a gap between. - for key := range m.Settings { - lands[key] = true - } - for _, values := range m.Contributes { - for key := range values { - lands[key] = true - } - } - for _, locals := range m.ContributesMany { - for _, values := range locals { - for key := range values { - lands[key] = true - } - } - } - var unused []string - for _, layer := range layers { - for key := range layer.Whole() { - if lands[key] { - continue - } - // `expose` is a real destination for a module that listens: it overrides a port's - // source (novox/hq ADR 0046), validated in Exposure, so it is not stray here. - if key == ExposeSetting && len(m.Listens) > 0 { - continue - } - // `ports` gives a module's port a machine port on one node (novox/hq ADR 0100), - // validated in GivenPorts, so it is not stray here either. - if key == PortsSetting { - continue - } - // `reach` says how far one of this module's endpoints reaches (novox/hq ADR 0138) — the - // filter's source, which names are composed, and therefore which authority certifies - // them. Validated in Reaches, so not stray. - if key == ReachSetting && len(m.Listens) > 0 { - continue - } - // `endpoints` configures a module's endpoints by name — the machine port, the subdomain and - // the reach as one block each (novox/hq ADR 0138). Validated in Endpoints, so not stray. - if key == EndpointsSetting && len(m.Listens) > 0 { - continue - } - // `places` puts a declared directory where this machine keeps it, `accesses` says where - // the operator's data is (novox/hq issue 153). Validated in Places and AccessPlaces. - if key == PlacesSetting && len(directoriesOf(m)) > 0 { - continue - } - if key == AccessesSetting && len(m.Accesses) > 0 { - continue - } - // `networks` keeps a found network for a taken container on one adopted machine - // (novox/hq ADR 0163). Validated in KeptNetworks, so not stray. - if key == NetworksSetting { - continue - } - unused = append(unused, fmt.Sprintf( - "%s sets %q, and %s has no file that merges it, asks for no ${setting:%s}, and "+ - "declares no %q in what it contributes%s", - layer.From, key, m.Module, key, key, takenSaid(m))) - } - } - sort.Strings(unused) - return unused -} - func stringsOf(v any) []string { raw, ok := v.([]any) if !ok { diff --git a/go/catalogue/settings_judge_test.go b/go/catalogue/settings_judge_test.go deleted file mode 100644 index 3dbee5e..0000000 --- a/go/catalogue/settings_judge_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package catalogue - -import ( - "strings" - "testing" -) - -// A setting is judged where it is stored, and an impossible one costs a module, not a machine -// (novox/hq ADR 0163, rule 6): the one judgement, used by SetSettings before storing and by -// Compose when a definition has moved under a stored setting. -func TestASettingThatCannotComposeIsRefusedByNameAndLeavesOnlyItsModuleOut(t *testing.T) { - web := Manifest{Module: "hello-web", - Listens: []Listening{{Port: 8080, From: FromEverywhere}}, - Resources: []map[string]any{{"id": "server", "type": "container", "name": "hello-web", - "ports": []any{"8080"}}}} - for _, c := range []struct { - name string - layer map[string]any - refuse string - }{ - {"a port the module does not publish", map[string]any{PortsSetting: map[string]any{"9999": 10000}}, - "hello-web gives port 9999 a machine port, and no container of its publishes 9999"}, - {"a mesh-wide port", map[string]any{PortsSetting: map[string]any{"8080": 10000}}, - "a port is a fact about one machine"}, - } { - from := "anchor" - if c.name == "a mesh-wide port" { - from = MeshWideLayer - } - err := JudgeSettings(web, []Layer{LayerOf(from, c.layer)}, true) - if err == nil || !strings.Contains(err.Error(), c.refuse) { - t.Errorf("%s: judged %v, want %q", c.name, err, c.refuse) - } - } - if err := JudgeSettings(web, []Layer{{From: "anchor", Placement: map[string]any{PortsSetting: map[string]any{"8080": 10000}}}}, true); err != nil { - t.Fatalf("a port the module publishes was refused: %v", err) - } - - // Composed, a module whose stored setting no longer works is left out by name; the rest of - // the machine is declared. - r := anAdoptedAnchor() - with := anchorRendering(false) - with.Settings = SettingsBy{"hello-web": {{From: "anchor", Placement: map[string]any{PortsSetting: map[string]any{"9999": 10000}}}}} - composed, err := r.Compose(with) - if err != nil { - t.Fatal(err) - } - why, left := composed.LeftOut["hello-web"] - if !left || !strings.Contains(why, "no container of its publishes 9999") { - t.Fatalf("hello-web is not left out by name: %v", composed.LeftOut) - } - if len(composed.LeftOut) != 1 { - t.Fatalf("more than hello-web is left out: %v", composed.LeftOut) - } - got := byID(composed.Resources) - if _, declared := got["hello-web.server"]; declared { - t.Fatal("the left-out module's container is still declared") - } - if _, declared := got["distribution.store"]; !declared { - t.Fatal("the rest of the machine was not declared") - } - if left := r.LeftOut(with.Settings, false); len(left) != 1 || left["hello-web"] == "" { - t.Fatalf("the judgement a plan reads differs from what compose did: %v", left) - } -} - -// A taken container keeps a found network by a per-machine setting (novox/hq ADR 0163, rule 4): -// on an adopted machine only, for a container the module declares, and it reaches the container's -// declaration as the networks it also joins. -func TestAKeptNetworkReachesTheContainerOnAnAdoptedMachineOnly(t *testing.T) { - r := anAdoptedAnchor() - keep := SettingsBy{"hello-web": {LayerOf("anchor", - map[string]any{NetworksSetting: map[string]any{"server": []any{"predecessor_default"}}})}} - - with := anchorRendering(true) - with.Settings = keep - composed, err := r.Compose(with) - if err != nil { - t.Fatal(err) - } - if len(composed.LeftOut) != 0 { - t.Fatalf("a kept network left a module out: %v", composed.LeftOut) - } - server := byID(composed.Resources)["hello-web.server"] - networks, _ := server["networks"].([]any) - if len(networks) != 1 || networks[0] != "predecessor_default" { - t.Fatalf("the container does not join the kept network: %v", server) - } - if _, has := byID(composed.Resources)["distribution.store"]["networks"]; has { - t.Fatal("another container joins a network nobody kept for it") - } - - // Converged, the setting reaches nothing it was for, and the module is left out saying so. - with = anchorRendering(false) - with.Settings = keep - composed, err = r.Compose(with) - if err != nil { - t.Fatal(err) - } - if why := composed.LeftOut["hello-web"]; !strings.Contains(why, "anchor is converged") { - t.Fatalf("a kept network on a converged machine: %v", composed.LeftOut) - } - - web := r.Modules[4] - for _, c := range []struct { - name string - layer Layer - want string - }{ - {"mesh-wide", LayerOf(MeshWideLayer, map[string]any{NetworksSetting: map[string]any{"server": []any{"x"}}}), - "a found network is a fact about one machine"}, - {"an unknown container", LayerOf("anchor", map[string]any{NetworksSetting: map[string]any{"db": []any{"x"}}}), - `names the container "db", which it does not declare`}, - {"not a list", LayerOf("anchor", map[string]any{NetworksSetting: map[string]any{"server": "x"}}), - "is a list of network names"}, - {"not a network name", LayerOf("anchor", map[string]any{NetworksSetting: map[string]any{"server": []any{"a/b"}}}), - "which is not a network name"}, - } { - _, err := KeptNetworks(web, []Layer{c.layer}, true) - if err == nil || !strings.Contains(err.Error(), c.want) { - t.Errorf("%s: %v, want %q", c.name, err, c.want) - } - } - if kept, err := KeptNetworks(web, nil, false); err != nil || kept != nil { - t.Fatalf("no setting: %v %v", kept, err) - } -} diff --git a/go/catalogue/settings_test.go b/go/catalogue/settings_test.go index 2313d8b..92f1052 100644 --- a/go/catalogue/settings_test.go +++ b/go/catalogue/settings_test.go @@ -159,37 +159,6 @@ func TestAFileThatDoesNotMergeIsLeftAlone(t *testing.T) { } } -func TestSettingsThatReachNothingAreNamed(t *testing.T) { - // Somebody who misspells a module, or sets a key on one with nothing mergeable, has changed - // nothing — and would otherwise find out by the machine not behaving differently, which is - // the slowest way there is. - m := Manifest{Module: "thing", Resources: []map[string]any{ - {"id": "conf", "type": "file", "path": "/etc/thing", "content": "plain"}, - }} - unused := UnusedSettings(m, []Layer{{From: "node", Values: map[string]any{"port": 1}}}) - if len(unused) != 1 || !strings.Contains(unused[0], "no file that merges it") { - t.Errorf("settings that reached nothing were not named: %v", unused) - } -} - -func TestASettingLandsOnlyWhereSomethingDeclaresIt(t *testing.T) { - // novox/hq 04-ISSUES/173. A module that contributes a route and serves a provision takes a - // setting for a key either declares, and a setting for a key neither declares is stray — it - // would not reach the route or the served fact, so it must be said rather than dropped. - m := Manifest{Module: "mail", - Contributes: map[string]map[string]any{"reverse-proxy": {"host": "mail", "port": 8080}}, - Serves: map[string]map[string]any{"smtp": {"host": "mail", "port": 25}}, - Resources: []map[string]any{ - {"id": "env", "type": "file", "path": "/etc/mail.env", "content": "SITE=${setting:sitename}\n"}, - }} - layers := []Layer{{From: "the mesh", Values: map[string]any{ - "host": "post", "sitename": "Mail", "website": "https://www.example.tld"}}} - unused := UnusedSettings(m, layers) - if len(unused) != 1 || !strings.Contains(unused[0], `"website"`) { - t.Errorf("only website reaches nothing; named: %v", unused) - } -} - func TestContentThatIsNotJSONIsRefusedWhereSomebodyIsLooking(t *testing.T) { // Rather than on the machine, at apply time, as a file the program cannot read. _, err := ApplySettings(file(`this is not json`), nil) @@ -198,27 +167,6 @@ func TestContentThatIsNotJSONIsRefusedWhereSomebodyIsLooking(t *testing.T) { } } -func TestAServedValueMayBeTheOperators(t *testing.T) { - // A mail provider serves its domain and an identity provider its issuer; neither is the - // definition's to state (ADR 0155). Filled from the layers, refused by name when unset. - served, err := Settle(map[string]any{"port": 587, "domain": "${setting:domain}"}, - []Layer{{From: "the mesh", Values: map[string]any{"domain": "example.tld"}}}) - if err != nil { - t.Fatal(err) - } - if served["domain"] != "example.tld" { - t.Errorf("the operator's value did not fill the served key: %v", served) - } - _, err = Settle(map[string]any{"domain": "${setting:domain}"}, nil) - if err == nil || !strings.Contains(err.Error(), `"domain"`) { - t.Errorf("a served value nothing sets must be refused by name; got %v", err) - } - m := Manifest{Module: "mail", Serves: map[string]map[string]any{"smtp": {"domain": "${setting:domain}"}}} - if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"domain": "x"}}}); len(unused) != 0 { - t.Errorf("a setting a served fact asks for is not stray: %v", unused) - } -} - // No layer replaces a value the vault fills, at any depth (novox/hq issue 610): searxng's server.secret_key // sits under `server`, a key its file takes, and a layer reaching it was let through by a top-level-only check. func TestNoLayerReplacesAVaultSecretAtAnyDepth(t *testing.T) { diff --git a/go/catalogue/shares.go b/go/catalogue/shares.go index 2ee52e8..64b652e 100644 --- a/go/catalogue/shares.go +++ b/go/catalogue/shares.go @@ -1,24 +1,5 @@ package catalogue -// A machine's shares, and the shares a machine mounts (novox/hq ADR 0263). -// -// **Two roles, one per side of the wire.** A machine that shares a directory with the mesh does it -// through the holder of `node-nfs-server`: it writes the machine's export file, runs the NFS service, -// opens its port to the private network only, and provides each share as the provision `nfs-share`. A -// machine that wants the files gets them through the holder of `node-mounts`: it writes a mount unit and -// an automount unit per share, so nothing mounts at boot and nothing can fail a boot, and it says a -// device that comes and goes is absent rather than failed. -// -// **One holder per machine on each side.** Two modules writing one machine's export file, or two writing -// mount units for one mount point, is the conflict a seat exists to refuse. Both seats deliver nothing: -// `nfs-share` is provided at the mesh's scope, by the module holding `node-nfs-server` on the machine that -// shares, and a seat at a node's scope cannot be the answer for a provision at the mesh's. -// -// **The data is the operator's** (ADR 0051). Neither holder creates, chowns or removes anything under a -// shared path; the export maps every client to the path's owner, so no client acts as another account on -// the server. Their verbs read, and the ones that act take over what a person wrote by hand only on a -// person's word: a dataset's export property, an fstab line. - // NFSServerSeat is the role of the machine that shares directories over NFS (novox/hq ADR 0263). const NFSServerSeat = "node-nfs-server" diff --git a/go/catalogue/state.go b/go/catalogue/state.go index 390dd5c..85a747e 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -8,14 +8,6 @@ import ( "strings" ) -// What a module may call its state, and whose state it may ask to read (novox/hq ADR 0201). -// -// A module names its state **locally** — `servers`, never a bucket or a subject — and another -// module's as `.`, the way a consumed event names its emitter (design 32 §1). The -// mesh derives the bucket from the two names, so the module and the local name must each be one -// token: the bucket joins them with an underscore, which neither may contain, so two modules can -// never derive one bucket. - // stateName is one local name of a module's state: lower-case, no dot, no underscore. var stateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) diff --git a/go/catalogue/streams.go b/go/catalogue/streams.go index 884a29b..f47111f 100644 --- a/go/catalogue/streams.go +++ b/go/catalogue/streams.go @@ -8,14 +8,6 @@ import ( "strings" ) -// A module's own stream (novox/hq ADR 0363). -// -// A module declares a stream as it declares a log (ADR 0297) or a bucket (ADR 0201): named locally, the -// events it carries named locally as `emits` names them, its age and its size within the mesh's bounds. -// The controller creates it and stays the only writer of stream definitions. A stream may be restricted -// by a trusted config value of its own module: then a consumer hears a part of it only where its own -// `consumes` asks and that value names it. - // The mesh's bounds on a module's stream: its age in days and its size in MiB, and what it gets when it // says nothing. const ( diff --git a/go/catalogue/template.go b/go/catalogue/template.go index a1d06ac..c050880 100644 --- a/go/catalogue/template.go +++ b/go/catalogue/template.go @@ -12,27 +12,6 @@ import ( "unicode/utf8" ) -// A file's text kept as a template file in its module's repository (novox/hq ADR 0349 §2, design 57 package T1, -// issue 654). -// -// **Two forms, one composition.** A file resource says its text inline (`content`) or names a template file -// beside its manifest (`"template": "files/"`), never both. The module's build packs `files/` into an -// archive artifact of its own — an `archive` artifact built `from: "files"` — and resolving the build reads each -// template's text back from that packed archive into `content` and drops `template`. So the manifest the mesh -// holds, composition and the node-engine are exactly as they were: a module with its text in `files/` composes -// byte-identically to the same module with it inline. Where there is no build — `module check`, the merge gate, -// `module add` — the text is read from the repository tree beside the manifest (WithTemplatesFrom). -// -// **files/ unless the manifest names another directory** (issue 731). A module whose files/ is already an archive -// unpacked onto the machine names its own (`"templates": "templates"`), packed into an archive no resource names, so -// its templates neither land beside what it unpacks nor change that archive's digest. -// -// **Read back by the builder, carried in the resolved manifest.** The builder reads each template from the very -// bytes it hashed into the archive's digest, so the text the mesh holds is the archive's. The controller -// registers the resolved manifest as the build result carries it and fetches nothing from the artifact store; a -// manifest that still names a template when it would be held — built by a builder that predates templates — is -// refused at registration (HeldTemplateProblem). - // TemplateField is the key a file resource names its template file by. const TemplateField = "template" diff --git a/go/catalogue/terminal_keys.go b/go/catalogue/terminal_keys.go index 2d8811b..fc3ff72 100644 --- a/go/catalogue/terminal_keys.go +++ b/go/catalogue/terminal_keys.go @@ -7,42 +7,6 @@ import ( "strings" ) -// Which settings are the controller's terminal's alone (novox/hq issue 339). -// -// A setting is the operator's word on how a module is configured, and the `settings` verb writes it for any -// caller allowed to call verbs — agents among them. Most settings change only the module itself. Some change -// what root or another module trusts, and those are said at the terminal alone, never through a verb: -// -// 1. `places` and `accesses`: where the node-engine creates and, as root, owns a module's directories, and -// which of the machine's paths are mounted into its container. -// 2. **Every key a provider serves**, and every setting a served value asks for. A setting overrides a served -// key (Settle), and what is served is what every consumer of the provision connects to and believes: a -// database's port, an object store's scheme, a registry's port, an identity provider's issuer and token -// path. Through a verb, any caller could point every consumer at a listener of its own and collect the -// credentials they present. -// 3. **Every setting a file asks for, unless the file says `"trusted": false`.** A file root or a consumer trusts — -// a logind drop-in, an env file that says which uid a container runs as, a script run as root — must not -// change through a verb, and the safe reading of a file that says nothing is that it is one of them (fail -// closed). `"trusted": false` is the opt-out, for a file nothing trusts: a person's own notifier settings. -// `module check` lists the files that say nothing, so an author can opt one out where that is true. -// **A mergeable file asks for every key its own content names** (novox/hq issue 340): a setting of that key -// lands in the file as a `${setting:…}` would, without the file ever spelling one. The agent's module keeps the -// managed settings and tool servers every Claude Code session on a node obeys in one, and through a verb an -// agent could have given every person's session a hook of its own. -// -// 4. **A module's whole layer, when it has a mergeable file not marked `"trusted": false`** (novox/hq issue 340, -// TrustedMergeable). A mergeable file takes any key a layer sets, not only those its content names, so no list -// of keys covers it: an empty runtime configuration a provider reads would take a `url` of the caller's. -// **Unless the module's files name the keys they take** (novox/hq ADR 0326, ADR 0329 §5): then a mergeable -// file takes exactly its `takes-settings` and no other key, so those keys of a trusted file are terminal keys -// and the rest of the layer is not. A key no file names is stray and refused where it is set. So betterbird's -// `tags`, in a file nothing trusts, is set through the verb, while its `accounts` and `identities` are not. -// 5. **Every setting a directory's owner asks for** (novox/hq issue 279): root chowns the directory to the number -// at the next push, so through a verb any caller could hand itself — or uid 0 — a module's folder. Whatever -// else asks for the key, a file marked `"trusted": false` included, the owner makes it the terminal's. -// -// Derived from the manifest, never listed by hand, so a provider or a trusted file added tomorrow is covered. - // TrustedField is the key a file resource carries to say whether the settings it asks for are trusted: true, or // absent, makes each a terminal key; false says, out loud, that none changes what root or a consumer trusts. Said in the // catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly. @@ -110,33 +74,6 @@ func UnsaidTrust(m Manifest) []string { return out } -// TrustedMergeable is every mergeable file of a module not marked `"trusted": false`, by id (novox/hq issue 340). -// A mergeable file takes any key a layer sets, not only those its content names: an empty runtime configuration a -// provider reads takes a `url` of a caller's as surely as a declared one. So a module holding one has its whole -// layer set at the controller's terminal; a verb may read it and change nothing. A module whose mergeable files -// name the keys they take (ADR 0326) has none here: each such file takes only its named keys, which TerminalKeys -// holds for a trusted one (ADR 0329 §5). -func TrustedMergeable(m Manifest) []string { - if namesItsKeys(m) { - return nil - } - var out []string - for _, r := range m.Resources { - if fmt.Sprint(r["type"]) != "file" { - continue - } - if how, _ := r["merge"].(string); how == "" { - continue - } - if trusted, said := r[TrustedField].(bool); said && !trusted { - continue - } - out = append(out, fmt.Sprint(r["id"])) - } - sort.Strings(out) - return out -} - // asksFor is every setting a file resource asks for: each `${setting:…}` in its content and, for a mergeable file, // every key at the top of the content it merges into (novox/hq issue 340). Those are the keys the file declares it // takes: a layer's value for one of them lands in it as surely as a placeholder would be filled. A key the content diff --git a/go/catalogue/terminal_settings_test.go b/go/catalogue/terminal_settings_test.go index a7ce7a6..4086479 100644 --- a/go/catalogue/terminal_settings_test.go +++ b/go/catalogue/terminal_settings_test.go @@ -5,36 +5,6 @@ import ( "testing" ) -// No placement and no access at the machine's own system or the mesh's state, however it is spelled (novox/hq -// issue 339); a module's own place elsewhere is taken. -func TestAPlacementOrAnAccessAtTheMachinesOwnIsRefused(t *testing.T) { - m := Manifest{Module: "notes", Resources: []map[string]any{{"id": "data", "type": "directory"}}, - Accesses: []Access{{ID: "media"}}} - for _, path := range []string{"/", "/etc", "/etc/", "/etc/sudoers.d", "/usr/bin", "/root", "/var", "/var/lib", - "/home", "/home/", "/run", "/run/user/1000", "/var/lib/mesh/x", "/var/lib/mesh-host", "/var/lib/mesh-host/identity", - "/srv/../etc", "//etc", "/proc/1", "/sys", "/dev", "/boot/efi", "/bin", "/sbin", "/lib", "/lib64"} { - layers := []Layer{LayerOf("laptop", map[string]any{PlacesSetting: map[string]any{"data": path}})} - if _, err := Places(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") { - t.Errorf("a place at %s: %v", path, err) - } - layers = []Layer{LayerOf("laptop", map[string]any{AccessesSetting: map[string]any{"media": path}})} - if _, err := AccessPlaces(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") { - t.Errorf("an access at %s: %v", path, err) - } - } - for _, path := range []string{"/srv/notes", "/mnt/plex/data", "/storage/media", "/services/media/movies", - "/var/lib/notes/data", "/home/restic/repo", "/var/lib/mesh-store"} { - layers := []Layer{LayerOf("laptop", map[string]any{PlacesSetting: map[string]any{"data": path}})} - if got, err := Places(m, layers); err != nil || got["data"].Path != path { - t.Errorf("a place at %s: %v %v", path, got, err) - } - layers = []Layer{LayerOf("laptop", map[string]any{AccessesSetting: map[string]any{"media": path}})} - if got, err := AccessPlaces(m, layers); err != nil || got["media"] != path { - t.Errorf("an access at %s: %v %v", path, got, err) - } - } -} - // A line break, a carriage return or a NUL in any string of any setting is refused, at any depth, keys too — // where a layer is judged, which is both where it is kept and where it is composed (novox/hq issue 339). func TestASettingHoldsOneLine(t *testing.T) { @@ -104,30 +74,6 @@ func TestEveryLineEndIsRefused(t *testing.T) { } } -// The review's additions: the spool, the container runtimes' data, /opt, and any home's .ssh. -func TestTheRuntimesDataAndAnyHomesSSHAreTheMachinesOwn(t *testing.T) { - m := Manifest{Module: "notes", Resources: []map[string]any{{"id": "data", "type": "directory"}}, - Accesses: []Access{{ID: "media"}}} - for _, path := range []string{"/var/spool", "/var/spool/cron", "/var/lib/docker", "/var/lib/docker/volumes", - "/var/lib/containers/storage", "/var/lib/containerd", "/var/lib/containerd/io.containerd.snapshotter.v1", "/opt", "/opt/app", "/home/alice/.ssh", "/home/alice/.ssh/keys", - "/srv/backup/.ssh", "/root/.ssh"} { - layers := []Layer{LayerOf("laptop", map[string]any{PlacesSetting: map[string]any{"data": path}})} - if _, err := Places(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") { - t.Errorf("a place at %s: %v", path, err) - } - layers = []Layer{LayerOf("laptop", map[string]any{AccessesSetting: map[string]any{"media": path}})} - if _, err := AccessPlaces(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") { - t.Errorf("an access at %s: %v", path, err) - } - } - for _, path := range []string{"/var/lib/dockerish", "/home/alice/ssh", "/home/alice/.sshd-notes", "/storage/media"} { - layers := []Layer{LayerOf("laptop", map[string]any{PlacesSetting: map[string]any{"data": path}})} - if _, err := Places(m, layers); err != nil { - t.Errorf("a place at %s: %v", path, err) - } - } -} - // What a provider serves its consumers is the terminal's (novox/hq issue 339): any key under its `serves`, and any // setting a served value asks for, is set at the terminal alone — a verb that could change a port could point every // consumer at a listener of the caller's own. So is any setting a file marked `trusted` asks for. @@ -165,30 +111,3 @@ func TestTerminalKeysAreDerived(t *testing.T) { } } } - -// A module whose mergeable files name the keys they take (ADR 0326) has no whole layer at the terminal: a -// trusted file's named keys are terminal keys, and a file nothing trusts leaves its keys to the verb (novox/hq -// ADR 0329 §5, issue 586: betterbird's tags beside its trusted accounts, identities, calendars, address books -// and filters). A module whose files name no keys keeps the whole layer at the terminal (issue 340). -func TestOnlyANamedKeyOfATrustedFileIsTheTerminals(t *testing.T) { - betterbird := Manifest{Module: "betterbird", Resources: []map[string]any{ - {"id": "prefs", "type": "file", "path": "/home/p/prefs.json", "merge": MergeJSON, "trusted": true, - TakesSettingsField: []any{"prefs"}, "content": `{"prefs": {}}`}, - {"id": "accounts", "type": "file", "path": "/home/p/accounts.json", "merge": MergeJSON, "trusted": true, - TakesSettingsField: []any{"accounts"}, "content": `{"accounts": {}}`}, - {"id": "configuration", "type": "file", "path": "/home/p/configuration.json", "merge": MergeJSON, - TakesSettingsField: []any{"identities", "calendars", "address-books", "filters"}, "content": `{}`}, - {"id": "tags", "type": "file", "path": "/home/p/tags.json", "merge": MergeJSON, "trusted": false, - TakesSettingsField: []any{"tags"}, "content": `{"tags": {}}`}}} - if got := TrustedMergeable(betterbird); len(got) != 0 { - t.Fatalf("files that name their keys put no whole layer at the terminal: %v", got) - } - if got := strings.Join(TerminalKeys(betterbird), ","); got != "places,accesses,accounts,address-books,calendars,filters,identities,prefs" { - t.Fatalf("the named keys of the trusted files, unmarked counting as trusted, and not tags: %s", got) - } - silent := Manifest{Module: "keycloak", Resources: []map[string]any{{"id": "runtime", "type": "file", - "path": "/var/lib/kc/runtime.json", "merge": MergeJSON, "content": `{}`}}} - if got := TrustedMergeable(silent); len(got) != 1 { - t.Fatalf("a file that names no keys still takes any key, so its module's layer stays the terminal's: %v", got) - } -} diff --git a/go/catalogue/the_bus_is_never_public_test.go b/go/catalogue/the_bus_is_never_public_test.go deleted file mode 100644 index 84371ca..0000000 --- a/go/catalogue/the_bus_is_never_public_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package catalogue - -import ( - "regexp" - "testing" -) - -// The bus is never public (novox/hq ADR 0169). Its port is what the bus module declares, the mesh, -// and the control plane adds no opening of its own: a machine joins through the tunnel, so the -// broker's host is filtered like any other. Before this, the broker port was a foundation port and -// rendered from anywhere beside its from-the-mesh rule. -func TestTheBusPortIsReachedFromTheMeshAlone(t *testing.T) { - rules := []Rule{{Port: 4222, Protocol: "tcp", From: FromMesh, Because: []string{"nats"}, - Why: []string{"the mesh bus"}}} - out := AsNftables(rules, []string{"10.10.0.1", "10.10.0.2"}, true, nil, []string{"eth0"}, "mesh0") - - if !regexp.MustCompile(`ip saddr \{ 10\.10\.0\.1, 10\.10\.0\.2 \} tcp dport 4222 accept`).MatchString(out) { - t.Fatalf("the bus is not reachable from the mesh:\n%s", out) - } - if regexp.MustCompile(`(?m)^\s*tcp dport 4222 accept`).MatchString(out) { - t.Fatalf("the bus is reachable from anywhere:\n%s", out) - } -} diff --git a/go/catalogue/unconsumed_placeholder.go b/go/catalogue/unconsumed_placeholder.go index b017d5a..06e0c3b 100644 --- a/go/catalogue/unconsumed_placeholder.go +++ b/go/catalogue/unconsumed_placeholder.go @@ -6,40 +6,6 @@ import ( "strings" ) -// A placeholder no pass consumes is refused, never written out as text (novox/hq issue 231). -// -// Each namespace is filled by its own pass with its own pattern, in the fields that pass reads. A -// word in that shape that no pattern matched — `${machnie:address}`, `${shel:zsh:first}`, a setting -// key no definition could declare such as `${setting:Undeclared}` — was left in the file as it was -// written and reached a machine as a value: the predecessor's failure the namespaced placeholders -// were meant to end (novox/hq ADR 0164). So the definition is swept, field by field, against the -// same table of what each pass fills where. -// -// **The definition, never the values.** Swept as the manifest wrote it, at the catalogue check and -// again at composition before any pass has run: what an operator's setting, a binding or a merged -// JSON setting puts into a file is its own software's text — `${env:HOME}` for Log4j, `${timeout:30}` -// for Spring — and refusing it there would refuse something its author never wrote, on a machine the -// check had passed (novox/hq issue 231, review of mesh-controller #211). -// -// What is refused: -// - a placeholder of a namespace the mesh knows, in a field that namespace's pass does not read: it -// would reach the machine as the same text; -// - any other `${:`, case-insensitively, unless a shell's parameter operator follows -// its colon — `${Machine:address}`, `${machine:.address}`, `${machine: address}`, an unclosed -// `${machine:address` — because no pass's pattern takes it; -// - a namespace-shaped placeholder of a word the mesh does not know: a lower-case word, a colon, and -// a key that begins with a letter or a digit and holds no space, brace or `$`. -// -// **The shell's own syntax is not that shape, and passes.** `${NAME:-…}` has an upper-case word, -// `${(%):-…}` and `${1:-.}` begin with no letter, and a lower-case variable with an operator after its -// colon — `${count:-}`, `${trial:+…}`, `${state:=…}` — has no key that begins with a letter or a digit. -// And an operator — `:-`, `:=`, `:+`, `:?` — after a word the mesh also uses is the shell's too: -// `${PORT:-8080}`, `${SHELL:-/bin/sh}`, `${SECRET:?unset}` and `${dir:-/tmp}` are among the commonest -// lines of a script or an env file, and pass whatever the word's case. -// What the shape does catch is a zsh modifier (`${path:t}`) or a substring (`${where:0:12}`) in a -// resource's own text: shell code of that kind belongs in the module's contributed shell code, which -// is not a resource and is never swept (novox/hq ADR 0204). - // filler is one namespace's pass: the patterns it fills with, and the fields it reads them in, by // resource type ("*" for any type). type filler struct { diff --git a/go/catalogue/unhealthy.go b/go/catalogue/unhealthy.go index f4fd74a..7b2a667 100644 --- a/go/catalogue/unhealthy.go +++ b/go/catalogue/unhealthy.go @@ -1,12 +1,5 @@ package catalogue -// How loud a module's unhealthy condition is (novox/hq ADR 0355, extending ADR 0240). -// -// **A module may declare its unhealthy urgent.** A module whose failure costs people something they rely on -// (mail refused or stuck for everyone with a mailbox) says so with `unhealthy`, and its condition -// `module...unhealthy` is raised urgent instead of a warning. **Urgent needs a why**, as an -// `upgrade` policy other than roll does: a person reading the catalogue sees what is lost while it is broken. - // The severities a module may declare for its unhealthy condition. const ( // UnhealthyUrgent raises the module's unhealthy condition urgent, on every machine it runs on. diff --git a/go/catalogue/unknown_field.go b/go/catalogue/unknown_field.go index 6ce7f3d..f6565bc 100644 --- a/go/catalogue/unknown_field.go +++ b/go/catalogue/unknown_field.go @@ -8,14 +8,6 @@ import ( "regexp" ) -// A key this controller does not know, in a manifest (novox/hq ADR 0262). -// -// Registration refuses it, as it always has. A manifest the store already holds was registered by a newer -// controller, and is read by this one after a rollback: refusing it there failed the whole catalogue, and -// with it every plan and every send. Dropping the key silently would be worse — a module running without -// something its manifest says. So the manifest is read, the module is left out of every machine's -// declaration by name, and the controller raises a condition until it is updated. - // UnknownFieldError is a key a manifest has that this controller does not know, wherever it is: at the // top of the manifest or inside a block (a state, an offer, a data item's backup, an own secret, a verb). // Typed, because the blocks' decoders wrap the decoder's words in their own. diff --git a/go/catalogue/upgrade.go b/go/catalogue/upgrade.go index 037e6cc..a89e994 100644 --- a/go/catalogue/upgrade.go +++ b/go/catalogue/upgrade.go @@ -6,15 +6,6 @@ import ( "time" ) -// What the mesh does when a module's build moves (novox/hq ADR 0236, extending ADR 0162 §3 and ADR -// 0218 §2). -// -// **Rolled out by default, one machine first and gated.** With the gate on the first machine and the -// rollback after it (to-be 45 §8), a build that moves is sent to one machine, judged there by its own -// health, and only then to the rest — or put back there, said, and sent nowhere else. Recording an -// upgrade and waiting for a person to push it is kept where a module says why, and where the mesh knows -// a rollback cannot undo what a new build does. - // The policies a module may declare. const ( // PolicyRoll sends one machine first, judges it at the gate, then the rest. diff --git a/go/catalogue/zones.go b/go/catalogue/zones.go index 7c4b4a2..c463573 100644 --- a/go/catalogue/zones.go +++ b/go/catalogue/zones.go @@ -6,15 +6,6 @@ import ( "strings" ) -// Zones: names a module answers itself (novox/hq ADR 0199). -// -// The mesh's resolver holds each node's internal domain and nothing else (ADR 0191, 0194). A module -// whose names are its own — the lab's scenario machines, known only while a scenario runs — declares -// the zone it answers and the listen that answers it; the controller hands the resolver's holder every -// zone with the declaring node's private address and the port that listen is published on, and the -// holder forwards the zone there. **A definition names no address** (ADR 0112): the zone is a setting, -// the listen is the module's own, and where they are is the mesh's fact. - // Zone is the manifest's declaration that a module answers the names in one zone. type Zone struct { // Name is the zone: a label or a dotted name, normally `${setting:}`, so the operator chooses