Send each resource's module and each module's build to a node that reads them; pin mesh-host at 0b391f49a25f (issue 1145)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/chain"
|
||||
"github.com/novox/mesh-host/validate"
|
||||
)
|
||||
|
||||
// The module of each resource and the build of each module (novox/hq issue 1145, design 47 release 1-2). The
|
||||
// composer's owner of an id and the module's build go on the wire only to a machine whose node-engine said it reads
|
||||
// them: an older one decodes strictly and refuses a key it does not know, whole.
|
||||
|
||||
func modulesDeclaration(reads bool) sendable {
|
||||
return sendable{
|
||||
Resources: []map[string]any{
|
||||
{"id": "web.app.conf", "type": "file", "path": "/etc/app.conf", "content": "x\n"},
|
||||
{"id": "web.app.data", "type": "file", "path": "/etc/data", "content": "d\n"},
|
||||
{"id": "mesh.guard", "type": "file", "path": "/etc/guard", "content": "g\n"}},
|
||||
moduleOf: map[string]string{"web.app.conf": "web.app", "web.app.data": "web.app"},
|
||||
buildIDs: map[string]string{"web.app": "build-7"},
|
||||
ReadsModules: reads,
|
||||
}
|
||||
}
|
||||
|
||||
// 1145/AC1
|
||||
func TestAMachineThatReadsModulesIsSentEachResourcesModuleAndEachModulesBuild(t *testing.T) {
|
||||
keys := bodyKeys(t, modulesDeclaration(true))
|
||||
builds, _ := keys["builds"].(map[string]any)
|
||||
if len(builds) != 1 || builds["web.app"] != "build-7" {
|
||||
t.Fatalf("1145/AC1: the body carries builds %v", keys["builds"])
|
||||
}
|
||||
want := map[string]any{"web.app.conf": "web.app", "web.app.data": "web.app", "mesh.guard": nil}
|
||||
for _, r := range keys["resources"].([]any) {
|
||||
r := r.(map[string]any)
|
||||
if r["module"] != want[r["id"].(string)] {
|
||||
t.Errorf("1145/AC1: resource %v carries module %v, want %v", r["id"], r["module"], want[r["id"].(string)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1145/AC2: the body is byte for byte what it is today.
|
||||
func TestAMachineThatDoesNotReadModulesIsSentTheBodyItWasSentBefore(t *testing.T) {
|
||||
d := modulesDeclaration(false)
|
||||
got, err := d.Body()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := sendable{Resources: d.Resources}
|
||||
want, _ := before.Body()
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("1145/AC2: the body changed:\n%s\nwas\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 1145/AC4: the node-engine's own validator, as the gate runs it, takes the two words.
|
||||
func TestTheNodeEnginesValidatorTakesAModulePerResourceAndABuildPerModule(t *testing.T) {
|
||||
raw, err := modulesDeclaration(true).Body()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var generic map[string]any
|
||||
if err := json.Unmarshal(raw, &generic); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if problems := validate.Declaration(raw); len(problems) != 0 {
|
||||
t.Fatalf("1145/AC4: refused: %q\n%s", problems, fmt.Sprint(generic))
|
||||
}
|
||||
}
|
||||
|
||||
// The build a module carries is the one its assignment names when that is the commit carried, else the one built from
|
||||
// the commit carried; none when neither is known.
|
||||
func TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt(t *testing.T) {
|
||||
runs := map[string]chain.Ref{"a": {Build: "b-a", Commit: "c1"}, "b": {Build: "b-old", Commit: "c0"}}
|
||||
looked := map[string]string{"b/c2": "b-new"}
|
||||
got, err := wireBuilds(map[string]string{"a": "c1", "b": "c2", "c": "c3", "d": ""}, runs,
|
||||
func(module, commit string) (string, error) { return looked[module+"/"+commit], nil })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{"a": "b-a", "b": "b-new"}
|
||||
if len(got) != len(want) || got["a"] != want["a"] || got["b"] != want["b"] {
|
||||
t.Fatalf("1145: builds %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
+89
-22
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/chain"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/licences"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
@@ -500,33 +501,75 @@ func declarationWith(ctx context.Context, open *stores, node string,
|
||||
// And which build of each module it carries, for the send to record (novox/hq issue 259, ADR
|
||||
// 0221). Read only on the send path: a question about what would be sent records nothing.
|
||||
if choosing == Allocating {
|
||||
current, err := open.inventory.CurrentBuilds(ctx)
|
||||
carried, err := carriedOn(ctx, open, node, plan, composed.LeftOut, true)
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
before, known, err := open.inventory.SentBuilds(ctx, node)
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
if !known {
|
||||
before = nil
|
||||
}
|
||||
names := make([]string, 0, len(plan.Modules))
|
||||
for _, m := range plan.Modules {
|
||||
names = append(names, m.Module)
|
||||
}
|
||||
out.Builds = carriedBuilds(names, composed.LeftOut, current, before)
|
||||
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0245).
|
||||
kept, err := recordedKept(ctx, open, node)
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
for m, was := range kept {
|
||||
if _, carried := out.Builds[m]; carried {
|
||||
out.Builds[m] = was
|
||||
out.Builds = carried
|
||||
out.Bindings = boundToData(plan, composed.LeftOut)
|
||||
}
|
||||
// And the module of each resource and the build of each module, to a machine whose node-engine said it reads them
|
||||
// (novox/hq issue 1145) and to no other; the same whatever the composition is for, so what a question about the
|
||||
// send digests is what the send digests.
|
||||
reads, err := open.inventory.ReadsModules(ctx, record.ID)
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
if reads {
|
||||
carried := out.Builds
|
||||
if choosing != Allocating {
|
||||
if carried, err = carriedOn(ctx, open, node, plan, composed.LeftOut, false); err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
}
|
||||
out.Bindings = boundToData(plan, composed.LeftOut)
|
||||
runs, err := open.inventory.AssignmentRuns(ctx, node)
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
builds, err := wireBuilds(carried, runs, func(module, commit string) (string, error) {
|
||||
return open.inventory.BuildOf(ctx, module, commit)
|
||||
})
|
||||
if err != nil {
|
||||
return sendable{}, err
|
||||
}
|
||||
out.ReadsModules, out.moduleOf, out.buildIDs = true, composed.Owner, builds
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// carriedOn is the build of each module a declaration for node carries (see carriedBuilds), the module kept at the
|
||||
// build a machine runs included (ADR 0245). Only a send (strict) fails for a kept module that cannot be read: a question
|
||||
// about it is answered without.
|
||||
func carriedOn(ctx context.Context, open *stores, node string, plan catalogue.Resolution, leftOut map[string]string,
|
||||
strict bool) (map[string]string, error) {
|
||||
current, err := open.inventory.CurrentBuilds(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, known, err := open.inventory.SentBuilds(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !known {
|
||||
before = nil
|
||||
}
|
||||
names := make([]string, 0, len(plan.Modules))
|
||||
for _, m := range plan.Modules {
|
||||
names = append(names, m.Module)
|
||||
}
|
||||
out := carriedBuilds(names, leftOut, current, before)
|
||||
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0245).
|
||||
kept, err := recordedKept(ctx, open, node)
|
||||
if err != nil {
|
||||
if strict {
|
||||
return nil, err
|
||||
}
|
||||
kept = nil
|
||||
}
|
||||
for m, was := range kept {
|
||||
if _, carried := out[m]; carried {
|
||||
out[m] = was
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -2109,3 +2152,27 @@ func sortedNames[V any](m map[string]V) []string {
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// wireBuilds is the build of each carried module that goes on the wire (novox/hq issue 1145): the build its assignment
|
||||
// names when that assignment is of the commit carried, else the build made from that commit; a module with neither
|
||||
// is not named.
|
||||
func wireBuilds(carried map[string]string, runs map[string]chain.Ref, lookup func(module, commit string) (string, error)) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
for module, commit := range carried {
|
||||
if commit == "" {
|
||||
continue
|
||||
}
|
||||
if ref, ok := runs[module]; ok && ref.Commit == commit && ref.Build != "" {
|
||||
out[module] = ref.Build
|
||||
continue
|
||||
}
|
||||
build, err := lookup(module, commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if build != "" {
|
||||
out[module] = build
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -38,6 +39,13 @@ type sendable struct {
|
||||
// the send; never on the wire.
|
||||
composedFrom int64
|
||||
actingEpoch uint64
|
||||
// ReadsModules says the machine's node-engine reads a module per resource and a build per module (novox/hq issue
|
||||
// 1145): only then are they sent, because an older node-engine refuses a key it does not know, whole.
|
||||
ReadsModules bool
|
||||
// moduleOf is the composer's owner of each resource id, and buildIDs the build each module carries (its id):
|
||||
// kept beside the resources, and on the wire only to a machine that reads them.
|
||||
moduleOf map[string]string
|
||||
buildIDs map[string]string
|
||||
// modules are the modules this declaration names, for the record of the send; never on the wire.
|
||||
modules []string
|
||||
// localNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR
|
||||
@@ -111,6 +119,28 @@ type adoptionEnvelope struct {
|
||||
// Body is the declaration's bytes, as sent and as digested.
|
||||
func (s sendable) Body() ([]byte, error) {
|
||||
envelope := map[string]any{"declaration": 1, "resources": s.Resources}
|
||||
if s.ReadsModules {
|
||||
// The module of each resource the composer owns (what the mesh adds of its own has none), and the build of each
|
||||
// module (novox/hq issue 1145): to a machine that said it reads them, and no other.
|
||||
named := make([]map[string]any, len(s.Resources))
|
||||
for n, r := range s.Resources {
|
||||
module, owned := s.moduleOf[fmt.Sprint(r["id"])]
|
||||
if !owned {
|
||||
named[n] = r
|
||||
continue
|
||||
}
|
||||
with := make(map[string]any, len(r)+1)
|
||||
for k, v := range r {
|
||||
with[k] = v
|
||||
}
|
||||
with["module"] = module
|
||||
named[n] = with
|
||||
}
|
||||
envelope["resources"] = named
|
||||
if len(s.buildIDs) > 0 {
|
||||
envelope["builds"] = s.buildIDs
|
||||
}
|
||||
}
|
||||
if s.Adoption != nil {
|
||||
envelope["adoption"] = s.Adoption
|
||||
}
|
||||
|
||||
@@ -36,4 +36,4 @@ require (
|
||||
// committed. Every build (the build agent's `go build`, the Dockerfile) compiles from vendor/ and
|
||||
// fetches nothing; go refuses to build when vendor/ and this file disagree, so a pin moved without
|
||||
// `go mod vendor` fails loudly, at once, everywhere.
|
||||
replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261017162006-6469e8238443
|
||||
replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261018171005-0b391f49a25f
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261017162006-6469e8238443 h1:zUtpGbiH9DLjCPY+KJsm+PZ25yW6k/NmGVS3haGFfMM=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261017162006-6469e8238443/go.mod h1:5QOm3cIfu/k9fN6QqgDbKGCcDst7Ul1UlcfsiMVm9j4=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261018171005-0b391f49a25f h1:dOIHI3EmIbqRDkw7kj86XCHpG4rVC5SiZCjDhfEQF9g=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261018171005-0b391f49a25f/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018164924-2b420603fe83 h1:BrHyE3ifFMUQLIza6I/iNlEgdcysB9/Y+1FO6Q9xws8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018164924-2b420603fe83/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -88,16 +88,21 @@ func TestTheSourceScanFindsATablePlanted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **The last migration drops the tables of the chain, behind its guard** (issue 1096): it names each table, the column and
|
||||
// **Migration 105 drops the tables of the chain, behind its guard** (issue 1096; it was the last until issue 1145 added 106): it names each table, the column and
|
||||
// the function, drops no build_gate, and raises before it drops anything.
|
||||
func TestTheLastMigrationDropsTheTablesOfTheChain(t *testing.T) {
|
||||
func TestMigration105DropsTheTablesOfTheChain(t *testing.T) {
|
||||
migrations, err := Migrations()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
last := migrations[len(migrations)-1]
|
||||
if last.Number < 105 {
|
||||
t.Fatalf("the last migration is %d", last.Number)
|
||||
var last struct{ Number int }
|
||||
for _, m := range migrations {
|
||||
if m.Number == 105 {
|
||||
last.Number = m.Number
|
||||
}
|
||||
}
|
||||
if last.Number != 105 {
|
||||
t.Fatal("there is no migration 105")
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("migrations", migrationFile(t, last.Number)))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Whether the machine's node-engine said it reads a module on each resource of a declaration and the build of each
|
||||
-- module (its reports' `reads_modules`, novox/hq issue 1145): until it has, it is sent neither, because an older
|
||||
-- node-engine refuses a key it does not know, whole. As reads_generation (migration 0094).
|
||||
alter table node add column reads_modules boolean not null default false;
|
||||
@@ -75,6 +75,21 @@ func (i *Inventory) RecordReadsGeneration(ctx context.Context, id string, reads
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadsModules says a machine's node-engine said it reads a module per resource and a build per module (issue 1145).
|
||||
func (i *Inventory) ReadsModules(ctx context.Context, id string) (bool, error) {
|
||||
var reads bool
|
||||
if err := i.store.Pool().QueryRow(ctx, `select reads_modules from node where id = $1`, id).Scan(&reads); err != nil {
|
||||
return false, fmt.Errorf("reading whether %s reads modules: %w", id, err)
|
||||
}
|
||||
return reads, nil
|
||||
}
|
||||
|
||||
// RecordReadsModules keeps what a machine's latest report said of reading modules.
|
||||
func (i *Inventory) RecordReadsModules(ctx context.Context, id string, reads bool) error {
|
||||
_, err := i.store.Pool().Exec(ctx, `update node set reads_modules = $2 where id = $1`, id, reads)
|
||||
return err
|
||||
}
|
||||
|
||||
// Send is one declaration sent to a machine, as the mesh records it.
|
||||
type Send struct {
|
||||
// Node is the machine's id; NodeName its name, filled where it is read back.
|
||||
|
||||
@@ -323,3 +323,20 @@ func TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend(t *testing.T) {
|
||||
t.Fatalf("an ordered report about sequence 21 is not its answer: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
// 1145/AC3: whether a machine reads modules is its latest word, as whether it reads a generation is.
|
||||
func TestWhetherAMachineReadsModulesIsItsLatestWord(t *testing.T) {
|
||||
inv, node := aNodeWithModules(t)
|
||||
record, err := inv.NodeByName(t.Context(), node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, reads := range []bool{false, true, false} {
|
||||
if err := inv.RecordReadsModules(t.Context(), record.ID, reads); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := inv.ReadsModules(t.Context(), record.ID); err != nil || got != reads {
|
||||
t.Fatalf("1145/AC3: recorded %v, read %v (%v)", reads, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +460,10 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
|
||||
if err := e.Inventory.RecordReadsGeneration(ctx, node.ID, report.ReadsGeneration); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// And whether it reads a module per resource and a build per module (novox/hq issue 1145), the same way.
|
||||
if err := e.Inventory.RecordReadsModules(ctx, node.ID, report.ReadsModules); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if report.Ordered() {
|
||||
account := AccountOf(report)
|
||||
|
||||
@@ -31,3 +31,18 @@ func TestTheGenerationOnTheWire(t *testing.T) {
|
||||
t.Fatalf("a node-engine that never said it reads a generation reads as one that does: %+v", older)
|
||||
}
|
||||
}
|
||||
|
||||
// 1145/AC3: a report's word that the node-engine reads a module per resource and a build per module, as mesh-host's
|
||||
// report writes it: `reads_modules`, on every report of a node-engine that reads them.
|
||||
func TestWhetherTheNodeEngineReadsModulesIsOnItsReport(t *testing.T) {
|
||||
var reads, older Report
|
||||
if err := json.Unmarshal([]byte(`{"node":"anchor","applied":["a"],"report_sequence":8,"reads_modules":true}`), &reads); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`{"node":"anchor","applied":["a"],"report_sequence":3}`), &older); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reads.ReadsModules || older.ReadsModules {
|
||||
t.Fatalf("1145/AC3: reads %v, an older node-engine reads %v", reads.ReadsModules, older.ReadsModules)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,10 @@ type Report struct {
|
||||
// so, because an older node-engine refuses a key it does not know, whole. Mirrors mesh-host
|
||||
// internal/link/messages.go.
|
||||
ReadsGeneration bool `json:"reads_generation,omitempty"`
|
||||
// ReadsModules says the node-engine reads a module on each resource of a declaration and the build of each module
|
||||
// (novox/hq issue 1145), on every report it makes: the mesh sends them only to a machine that said so. Mirrors
|
||||
// mesh-host internal/link/messages.go.
|
||||
ReadsModules bool `json:"reads_modules,omitempty"`
|
||||
// OlderGeneration is set on a report refusing a declaration composed from an older assignment
|
||||
// generation than the machine applied (novox/hq issue 234). The refused declaration is the report's
|
||||
// own Declared and Order, and its sender is read from the record of sends by that sequence.
|
||||
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ private addresses. That is acceptable while mesh-controller is private; if it ev
|
||||
a second place to clean besides the catalogue itself.
|
||||
|
||||
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
||||
mesh-host bff1ac14a198a1e2181053582728024c758bb800 examples/foundation-first-node.lock
|
||||
mesh-host 0b391f49a25f9923b348bb26157e99d78d2eed4a examples/foundation-first-node.lock
|
||||
mesh-sdk 2b420603fe835d6b80e2c98080ab3094c6896c50 conformance/events/module-event.json
|
||||
|
||||
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||
|
||||
+126
-16
@@ -103,6 +103,8 @@ type Resource interface {
|
||||
// and not a hash of the content: it is what lets the store say *this is the same resource
|
||||
// I applied last time*, which is what makes removal possible at all.
|
||||
Identity() string
|
||||
// DeclaredModule is the module the controller says the resource belongs to, or "" (issue 1132).
|
||||
DeclaredModule() string
|
||||
// Kind is the resource's type, for the store and for reporting.
|
||||
Kind() Type
|
||||
// Target is what the resource acts on, for a person reading a report.
|
||||
@@ -115,12 +117,30 @@ type Resource interface {
|
||||
// strict decoder would otherwise refuse. `Kind()` returns the constant and is what anything
|
||||
// else should read.
|
||||
|
||||
// DeclaredModule is on every kind: the module the controller says the resource belongs to, or "" when it does not say
|
||||
// (an older controller). Read through Declaration.ModuleOfID, which falls back to the guess.
|
||||
func (d *Directory) DeclaredModule() string { return d.Module }
|
||||
func (f *File) DeclaredModule() string { return f.Module }
|
||||
func (u *User) DeclaredModule() string { return u.Module }
|
||||
func (n *Network) DeclaredModule() string { return n.Module }
|
||||
func (a *Access) DeclaredModule() string { return a.Module }
|
||||
func (a *Archive) DeclaredModule() string { return a.Module }
|
||||
func (d *Process) DeclaredModule() string { return d.Module }
|
||||
func (s *Service) DeclaredModule() string { return s.Module }
|
||||
func (o *Opening) DeclaredModule() string { return o.Module }
|
||||
func (p *Package) DeclaredModule() string { return p.Module }
|
||||
func (c *Container) DeclaredModule() string { return c.Module }
|
||||
func (a *Action) DeclaredModule() string { return a.Module }
|
||||
|
||||
// Directory is a directory that should exist, with a mode.
|
||||
type Directory struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
// Owner is the user this belongs to, by name. Absent means root, which is what everything
|
||||
// managed was until users existed.
|
||||
Owner string `json:"owner,omitempty"`
|
||||
@@ -201,8 +221,11 @@ func (d *Directory) validate(where string, _ bool) []string {
|
||||
|
||||
// File is a file with literal content. The host renders nothing.
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
@@ -520,7 +543,10 @@ func (f *File) checkOwnership(where string) []string {
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Name string `json:"name"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Name string `json:"name"`
|
||||
|
||||
// Shell this user logs in with. Absent means the host asserts nothing and leaves whatever is
|
||||
// there — the same rule Service.Boot follows, for the same reason: a field that always
|
||||
@@ -571,7 +597,10 @@ const RootNever = "never"
|
||||
type Network struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Name string `json:"name"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (n *Network) Identity() string { return n.ID }
|
||||
@@ -616,7 +645,10 @@ const (
|
||||
type Access struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Path string `json:"path"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Path string `json:"path"`
|
||||
// Mode is how this module reaches the path: read or read-write. Absent narrows to read.
|
||||
Mode string `json:"mode,omitempty"`
|
||||
|
||||
@@ -743,6 +775,9 @@ func (u *User) validate(where string, _ bool) []string {
|
||||
type Archive struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
// Source is where to fetch it from.
|
||||
Source string `json:"source"`
|
||||
// Digest is sha256 of the archive, as "sha256:<hex>".
|
||||
@@ -796,6 +831,9 @@ func (a *Archive) validate(where string, _ bool) []string {
|
||||
type Process struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
// Name is what the unit is called, and what an operator will see in the process table.
|
||||
Name string `json:"name"`
|
||||
// Source is where to fetch the bundle from, and Digest is what it must hash to. The same
|
||||
@@ -985,7 +1023,10 @@ func (d *Process) validate(where string, _ bool) []string {
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Unit string `json:"unit"`
|
||||
// State is "running" or "stopped" — or absent, and then **the unit's lifecycle is the
|
||||
// machine's; the mesh only reflects its triggers** (novox/hq ADR 0117). The uplink modules
|
||||
// declare the machine's own network manager this way: the mesh writes into its configuration
|
||||
@@ -1157,8 +1198,11 @@ func BlockMarkers(id string) (begin, end string) {
|
||||
// firewall sees a published port after the runtime has translated it, so a forwarded opening names
|
||||
// the container's own port in To as well as the machine's in Port.
|
||||
type Opening struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
From string `json:"from"`
|
||||
@@ -1223,8 +1267,11 @@ func (o *Opening) validate(where string, _ bool) []string {
|
||||
// Present is the whole of what it asserts, never a version: version is the package manager's
|
||||
// business and the mesh does not hold a second opinion about it.
|
||||
type Package struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Package string `json:"package"`
|
||||
// Absent declares that the package is NOT installed (novox/hq ADR 0180): the host removes it
|
||||
// when it is, and leaves a machine that never had it alone. For the one case a module replaces
|
||||
@@ -1249,7 +1296,10 @@ func (p *Package) validate(where string, _ bool) []string {
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Name string `json:"name"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Name string `json:"name"`
|
||||
// Image is pinned by digest (novox/hq ADR 0006) — a tag moves and a digest does not.
|
||||
Image string `json:"image"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
@@ -1555,8 +1605,11 @@ func (c *Container) validate(where string, _ bool) []string {
|
||||
|
||||
// Action runs something the bundle declared, and the host never learns what it means.
|
||||
type Action struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent
|
||||
// is an older controller, and the module is then guessed from the id (ModuleGuess).
|
||||
Module string `json:"module,omitempty"`
|
||||
Command []string `json:"command"`
|
||||
// Verify is not optional and is not a courtesy. It is the read-back AND the idempotency
|
||||
// check: the host does not know what a database is, so "is it already there" is a question
|
||||
@@ -1727,6 +1780,58 @@ type Declaration struct {
|
||||
// the only account whose write of a filled file the node-engine's local write socket accepts. Absent is a
|
||||
// controller that names none, and every such write is refused.
|
||||
Provisioner string
|
||||
|
||||
// Builds is the build id of every module this declaration names, by module (novox/hq ADR 0429 §4, issue 1132).
|
||||
Builds map[string]string
|
||||
|
||||
// declared is what each resource says of its module, by id; nil on a declaration built by hand.
|
||||
declared map[string]string
|
||||
}
|
||||
|
||||
// ModuleGuess is the module a resource id names when the declaration does not say: everything before its last dot.
|
||||
//
|
||||
// The mesh composes a module's resource ids as `<module>.<its own id>`, and **a module's name may contain a dot**
|
||||
// (`novox.be` is one on this mesh) while a resource's own id never does, so the owner is everything before the *last*
|
||||
// dot. False for what the mesh declares in its own right: the foundation's resources carry no dot, and the
|
||||
// adoption's are named for the mesh. This is the guess an older controller's declaration is read by; a declaration
|
||||
// that says `module` is read by ModuleOfID, which never guesses for a resource that says (issue 1132).
|
||||
func ModuleGuess(id string) (string, bool) {
|
||||
if strings.HasPrefix(id, AdoptionPrefix) {
|
||||
return "", false
|
||||
}
|
||||
at := strings.LastIndex(id, ".")
|
||||
if at <= 0 {
|
||||
return "", false
|
||||
}
|
||||
return id[:at], true
|
||||
}
|
||||
|
||||
// ModuleOfID is the module a resource of this declaration belongs to: the module it says (novox/hq ADR 0429 §4,
|
||||
// issue 1132), and otherwise the guess from its id (ModuleGuess), so a declaration from an older controller reads as
|
||||
// it always did. False for what belongs to no module.
|
||||
func (d *Declaration) ModuleOfID(id string) (string, bool) {
|
||||
if d != nil {
|
||||
if d.declared == nil {
|
||||
for _, r := range d.Resources {
|
||||
if r.Identity() == id && r.DeclaredModule() != "" {
|
||||
return r.DeclaredModule(), true
|
||||
}
|
||||
}
|
||||
} else if m, ok := d.declared[id]; ok {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return ModuleGuess(id)
|
||||
}
|
||||
|
||||
// indexModules keeps what each resource says of its module, so ModuleOfID is a lookup.
|
||||
func (d *Declaration) indexModules() {
|
||||
d.declared = map[string]string{}
|
||||
for _, r := range d.Resources {
|
||||
if m := r.DeclaredModule(); m != "" {
|
||||
d.declared[r.Identity()] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkProvisioner refuses a provisioner client's account that is no account name, or is root: the account whose
|
||||
@@ -2126,6 +2231,10 @@ type envelope struct {
|
||||
// Provisioner is optional too (novox/hq ADR 0349 §4). **An older host refuses this key**; a controller sends it
|
||||
// only to a host whose health says link.ProvisionerContract.
|
||||
Provisioner string `json:"provisioner_account,omitempty"`
|
||||
// Builds is optional too (novox/hq ADR 0429 §4, issue 1132): the build id of each module the declaration names, and
|
||||
// every resource's `module`. **An older host refuses these keys**; a controller sends them only to a host whose
|
||||
// reports say `reads_modules`.
|
||||
Builds map[string]string `json:"builds,omitempty"`
|
||||
}
|
||||
|
||||
func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
@@ -2145,7 +2254,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence,
|
||||
Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack,
|
||||
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries, Filled: env.Filled,
|
||||
Provisioner: env.Provisioner}
|
||||
Provisioner: env.Provisioner, Builds: env.Builds}
|
||||
problems := checkIdentities(env.Identities, env.Groups)
|
||||
problems = append(problems, checkProvisioner(env.Provisioner)...)
|
||||
problems = append(problems, checkLibraries(env.Groups, env.Libraries)...)
|
||||
@@ -2264,6 +2373,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
if len(problems) > 0 {
|
||||
return nil, &RefusalError{Problems: problems}
|
||||
}
|
||||
d.indexModules()
|
||||
return d, nil
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -80,7 +80,7 @@ github.com/nats-io/nkeys
|
||||
# github.com/nats-io/nuid v1.0.1
|
||||
## explicit
|
||||
github.com/nats-io/nuid
|
||||
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261017162006-6469e8238443
|
||||
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261018171005-0b391f49a25f
|
||||
## explicit; go 1.26.0
|
||||
github.com/novox/mesh-host/internal/declaration
|
||||
github.com/novox/mesh-host/rootsearch
|
||||
@@ -140,4 +140,4 @@ golang.org/x/text/width
|
||||
# golang.org/x/time v0.15.0
|
||||
## explicit; go 1.25.0
|
||||
golang.org/x/time/rate
|
||||
# github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261017162006-6469e8238443
|
||||
# github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261018171005-0b391f49a25f
|
||||
|
||||
Reference in New Issue
Block a user