From bdbca4b92183b0682972e66fd278ca5ee9972465 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 02:37:28 +0200 Subject: [PATCH 1/6] mesh-config drops an ended assignment as a removed one (issue 1098) --- .../cmd/mesh-config/config_test.go | 40 +++++++++++++++++++ .../mesh-config/cmd/mesh-config/declared.go | 20 ++++++++-- .../mesh-config/cmd/mesh-config/sources.go | 3 ++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/modules/mesh-config/cmd/mesh-config/config_test.go b/modules/mesh-config/cmd/mesh-config/config_test.go index e2418422..91a14c71 100644 --- a/modules/mesh-config/cmd/mesh-config/config_test.go +++ b/modules/mesh-config/cmd/mesh-config/config_test.go @@ -551,3 +551,43 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) { t.Errorf("read %+v (%v)", call, err) } } + +// Issue 1098: a value of the assignments bucket with `ended` set, or with a `state`, is not an assignment: it +// resolves nothing and takes the module's resolved values away, as a removed key does. +func TestAnEndedAssignmentIsNotAnAssignment(t *testing.T) { + c, m := aMesh(t) + m.assign(t, "ace", "dunst") + if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 { + t.Fatalf("the assignment moved %+v (%v)", r.Moved, err) + } + for _, ended := range []string{`"ended":"2026-10-18T01:00:00Z"`, `"state":"pending"`} { + m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ended+`}`) + r, err := c.Reconcile() + if err != nil || len(r.Moved) != 4 || !r.Moved[0].Gone { + t.Errorf("%s moved %+v (%v)", ended, r.Moved, err) + } + if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil { + t.Errorf("%s: the module keeps its resolved values", ended) + } + m.assign(t, "ace", "dunst") + if r, _ = c.Reconcile(); len(r.Moved) != 4 { + t.Fatalf("the assignment standing again moved %+v", r.Moved) + } + } +} + +// Issue 1098: the fields the controller adds to an assignment change nothing, under the same key. +func TestAnAssignmentCarriesBuildCommitAndRolloutWithoutChangingItsKey(t *testing.T) { + c, m := aMesh(t) + m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ + `"build":"b-1","artifact":{"name":"dunst","version":"1.2"},"commit":"abc123","rollout":"r-9",`+ + `"since":"2026-10-18T00:00:00Z","ended":"","state":"","future":{"x":1}}`) + r, err := c.Reconcile() + if err != nil || len(r.Moved) != 4 { + t.Fatalf("an assignment with the new fields moved %+v (%v)", r.Moved, err) + } + expect(t, m, "ace", "10", SourceDefault) + if r, _ = c.Reconcile(); len(r.Moved) != 0 { + t.Errorf("a quiet pass moved %+v", r.Moved) + } +} diff --git a/modules/mesh-config/cmd/mesh-config/declared.go b/modules/mesh-config/cmd/mesh-config/declared.go index 6cf9f63f..372179a9 100644 --- a/modules/mesh-config/cmd/mesh-config/declared.go +++ b/modules/mesh-config/cmd/mesh-config/declared.go @@ -121,10 +121,24 @@ type Published struct { Commit string `json:"commit"` } -// Assignment is one key of the assignments mirror: a module on a node. +// Assignment is one key of the assignments bucket: a module on a node. The value also carries the build, commit and +// rollout that put it there and more; those are read where they are reported, and an unknown field never fails a +// parse. **Ended** and **State** are set on a value that is not a standing assignment (novox/hq ADR 0408 clause 4): +// it reads exactly as a removed key (issue 1098). type Assignment struct { - Node string `json:"node"` - Module string `json:"module"` + Node string `json:"node"` + Module string `json:"module"` + Ended json.RawMessage `json:"ended,omitempty"` + State json.RawMessage `json:"state,omitempty"` +} + +// Standing is whether the value is a standing assignment: neither ended nor in a state. +func (a Assignment) Standing() bool { return !fieldSet(a.Ended) && !fieldSet(a.State) } + +// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty. +func fieldSet(f json.RawMessage) bool { + s := strings.TrimSpace(string(f)) + return s != "" && s != "null" && s != `""` } // rule is the validation a value is held to, from a module's declaration or a predefined key. diff --git a/modules/mesh-config/cmd/mesh-config/sources.go b/modules/mesh-config/cmd/mesh-config/sources.go index 13cd30f1..221d1294 100644 --- a/modules/mesh-config/cmd/mesh-config/sources.go +++ b/modules/mesh-config/cmd/mesh-config/sources.go @@ -19,6 +19,9 @@ func (c *Config) sources() (Sources, error) { if err := json.Unmarshal(raw, &a); err != nil || a.Node == "" || a.Module == "" { return fmt.Errorf("the assignment %s is not one: %s", key, raw) } + if !a.Standing() { + return nil // ended or pending: read as a removed key (issue 1098) + } s.Assignments = append(s.Assignments, a) return nil }); err != nil { -- 2.54.0 From 60a3823f0eee65920f9f117ec2db39dfba7fb0ba Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 02:37:28 +0200 Subject: [PATCH 2/6] mesh-vault reads an ended assignment as a removed key (issue 1098) --- modules/mesh-vault/cmd/mesh-vault/askers.go | 31 ++++++++++++- .../mesh-vault/cmd/mesh-vault/ended_test.go | 43 +++++++++++++++++++ modules/mesh-vault/cmd/mesh-vault/pair.go | 3 ++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 modules/mesh-vault/cmd/mesh-vault/ended_test.go diff --git a/modules/mesh-vault/cmd/mesh-vault/askers.go b/modules/mesh-vault/cmd/mesh-vault/askers.go index 037bce99..e8eab5ab 100644 --- a/modules/mesh-vault/cmd/mesh-vault/askers.go +++ b/modules/mesh-vault/cmd/mesh-vault/askers.go @@ -75,6 +75,35 @@ const AssignmentsRead = "mesh-controller.assignments" // AssignmentReader reads one value of the assignments mirror, `.`: nil when there is none. type AssignmentReader func(key string) ([]byte, error) +// standing wraps an assignment reader so a value that is not a standing assignment reads as a removed key: one with +// `ended` set (the module was taken off its node, novox/hq ADR 0408 clause 4), or with a `state` (a pending one). The +// other fields of a value, new or unknown, change nothing (issue 1098). +func standing(read AssignmentReader) AssignmentReader { + if read == nil { + return nil + } + return func(key string) ([]byte, error) { + raw, err := read(key) + if err != nil || raw == nil { + return raw, err + } + var v struct { + Ended json.RawMessage `json:"ended"` + State json.RawMessage `json:"state"` + } + if json.Unmarshal(raw, &v) == nil && (fieldSet(v.Ended) || fieldSet(v.State)) { + return nil, nil + } + return raw, nil + } +} + +// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty. +func fieldSet(f json.RawMessage) bool { + s := strings.TrimSpace(string(f)) + return s != "" && s != "null" && s != `""` +} + // Declared is the rule the running vault keeps: a node's provisioner client, for a secret of a module on its node // that the module's plan there names. type Declared struct { @@ -141,7 +170,7 @@ func (d Declared) May(caller, name string) (bool, string) { if d.Read == nil { return false, "the vault cannot read the assignments, so it cannot tell what " + node + "'s declaration names" } - raw, err := d.Read(node + "." + module) + raw, err := standing(d.Read)(node + "." + module) if err != nil { return false, fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read, so what it names is not known: %v", node, module, err) diff --git a/modules/mesh-vault/cmd/mesh-vault/ended_test.go b/modules/mesh-vault/cmd/mesh-vault/ended_test.go new file mode 100644 index 00000000..e82d5e5a --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/ended_test.go @@ -0,0 +1,43 @@ +package main + +import "testing" + +// Issue 1098: a value of the assignments bucket that is not a standing assignment (one with `ended` set, or with a +// `state`) reads as a removed key, whichever way the vault reads it; the fields the controller adds change nothing. +func TestAnEndedAssignmentIsNotAnAssignmentToTheVault(t *testing.T) { + plan := `"provisioning":{"templates":{"source":"x","digest":"y"},"files":[],"secrets":[{"name":"token"}],` + + `"provisions":[{"provision":"gitea-read","provider":"novox","provider-module":"gitea"},` + + `{"name":"gitea-read","provision":"gitea-read","provider":"novox","provider-module":"gitea"}]}` + head := `{"node":"g14","module":"dunst","assigned":"2026-10-14T20:00:00Z",` + mirror := assignments{ + "g14.dunst": head + plan + `}`, + "g14.ended": `{"node":"g14","module":"ended","ended":"2026-10-18T01:00:00Z",` + plan + `}`, + "g14.wait": `{"node":"g14","module":"wait","state":"pending",` + plan + `}`, + "g14.new": `{"node":"g14","module":"new","build":"b1","artifact":{"name":"x","version":"1"},"commit":"c1",` + + `"rollout":"r1","since":"2026-10-18T00:00:00Z","unknown-field":[1],"ended":"","state":"",` + plan + `}`, + } + rule := Declared{Read: mirror.read} + for module, want := range map[string]bool{"dunst": true, "new": true, "ended": false, "wait": false} { + if ok, why := rule.May("provisioner.g14", "own/g14/"+module+"/token"); ok != want { + t.Errorf("own secret of %s: allowed %v (%s), want %v", module, ok, why, want) + } + if ok, why := rule.May("provisioner.g14", "provision/novox/gitea-read/g14/"+module); ok != want { + t.Errorf("credential of %s: allowed %v (%s), want %v", module, ok, why, want) + } + if why := pairRowRefusal(mirror.read, "g14", []string{"pair", "novox", "gitea-read", "g14", module}); (why == "") != want { + t.Errorf("pair row of %s: refusal %q, want allowed %v", module, why, want) + } + } + seen, err := readBindings(Mirror{Keys: mirror.keys, Read: mirror.read}) + if err != nil { + t.Fatal(err) + } + for module, want := range map[string]bool{"dunst": true, "new": true, "ended": false, "wait": false} { + if seen.assigned["g14."+module] != want || seen.handed["g14."+module] != want { + t.Errorf("binding of %s: assigned %v, want %v", module, seen.assigned["g14."+module], want) + } + } + if got := seen.bound[pairKey{"novox", "gitea-read"}]; got == nil || len(got.holders) != 2 { + t.Errorf("the bound holders of the provision: %+v", got) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/pair.go b/modules/mesh-vault/cmd/mesh-vault/pair.go index 40221732..933fd895 100644 --- a/modules/mesh-vault/cmd/mesh-vault/pair.go +++ b/modules/mesh-vault/cmd/mesh-vault/pair.go @@ -223,6 +223,7 @@ func readBindings(m Mirror) (bindings, error) { if m.Keys == nil || m.Read == nil { return b, errors.New("the vault cannot read the assignments, so it cannot see who binds a provider") } + m.Read = standing(m.Read) keys, err := m.Keys() if err != nil { return b, fmt.Errorf("the assignments' keys could not be read: %w", err) @@ -704,6 +705,7 @@ func sharedRefusal(store *Store, read AssignmentReader, node, provider, provisio if read == nil { return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name" } + read = standing(read) if node == provider && sh.ProviderModule != "" { for _, m := range strings.Split(sh.ProviderModule, ",") { if raw, err := read(node + "." + m); err == nil && raw != nil { @@ -787,6 +789,7 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string { if read == nil { return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name" } + read = standing(read) raw, err := read(consumer + "." + module) if err != nil { return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", consumer, module, err) -- 2.54.0 From 9edd16eaeba09a3940ee938ca0e3c0fc12a93797 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 02:37:28 +0200 Subject: [PATCH 3/6] mesh-provisioner-client takes a module off the node when its assignment is ended (issue 1098) --- .../cmd/mesh-provisioner-client/client.go | 3 + .../cmd/mesh-provisioner-client/ended_test.go | 58 +++++++++++++++++++ .../cmd/mesh-provisioner-client/plan.go | 19 ++++++ 3 files changed, 80 insertions(+) create mode 100644 modules/mesh-provisioner-client/cmd/mesh-provisioner-client/ended_test.go diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go index 42b87570..7d31b841 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go @@ -445,6 +445,9 @@ func (c *Client) take(s *state, u update) []string { // A kind's answer moves what every module waits on. return sortedKeys(s.plans) case AssignmentsBucket: + // A value that is not a standing assignment (ended, or in a state: ADR 0408 clause 4) is read as a removed + // key, so the module leaves the machine as it does when the key goes (issue 1098). + gone = gone || !standingValue(e.Value()) // Whom it binds, whichever machine's it is: a provider here listing its consumers is filled again. var providers []string if takeBinds(s, key, e.Value(), gone) { diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/ended_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/ended_test.go new file mode 100644 index 00000000..c9e2d4b3 --- /dev/null +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/ended_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "fmt" + "testing" + "time" +) + +// Issue 1098: a value of the assignments bucket with `ended` set takes the module off the node exactly as a removed +// key does, and a value with the fields the controller adds to an assignment (build, commit, rollout, unknown ones) +// is read under the same key. +func TestNatsAnEndedAssignmentTakesAModuleOffTheNodeLikeARemovedKey(t *testing.T) { + b := startBus(t) + heard := b.listen(t) + e := startEngine(t, map[string]string{dunstrc: "owned"}) + b.config(t, "dunst", "font-size", "13") + p := dunst(t) + b.assign(t, "dunst", p) + aClient(t, b, e, t.TempDir(), time.Hour) + eventually(t, "the needs are recorded", func() bool { return len(b.keys(t, NeedsBucket, node+".dunst.")) == 2 }) + + b.put(t, AssignmentsBucket, node+".dunst", fmt.Sprintf(`{"node":%q,"module":"dunst","assigned":"2026-10-14T20:00:00Z",`+ + `"build":"b-1","commit":"abc","rollout":"r-1","ended":"2026-10-18T01:00:00Z",`+ + `"provisioning":%s}`, node, planJSON(t, p))) + eventually(t, "its needs go", func() bool { return len(b.keys(t, NeedsBucket, node+".dunst.")) == 0 }) + eventually(t, "its report goes", func() bool { return len(b.keys(t, AppliedBucket, node+".dunst")) == 0 }) + eventually(t, "prov.applied says it is removed", func() bool { + r, _ := heard.last("dunst") + return r.Status == StatusRemoved + }) +} + +func TestNatsAnAssignmentWithTheNewFieldsIsFilledUnderTheSameKey(t *testing.T) { + b := startBus(t) + e := startEngine(t, map[string]string{dunstrc: "owned"}) + b.config(t, "dunst", "font-size", "13") + p := dunst(t) + b.assign(t, "dunst", p) + aClient(t, b, e, t.TempDir(), time.Hour) + eventually(t, "the needs are recorded", func() bool { return len(b.keys(t, NeedsBucket, node+".dunst.")) == 2 }) + b.put(t, AssignmentsBucket, node+".dunst", fmt.Sprintf(`{"node":%q,"module":"dunst","assigned":"2026-10-14T20:00:00Z",`+ + `"build":"b-1","artifact":{"name":"dunst","version":"1"},"commit":"abc","rollout":"r-1",`+ + `"since":"2026-10-18T00:00:00Z","ended":"","state":"","future":1,"provisioning":%s}`, node, planJSON(t, p))) + time.Sleep(300 * time.Millisecond) + if got := b.keys(t, NeedsBucket, node+".dunst."); len(got) != 2 { + t.Errorf("a value with the new fields took the module's needs away: %v", got) + } +} + +func planJSON(t *testing.T, p Plan) []byte { + t.Helper() + raw, err := json.Marshal(p) + if err != nil { + t.Fatal(err) + } + return raw +} diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go index 8caf8968..82ee8977 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go @@ -29,6 +29,25 @@ type Assignment struct { Provisioning json.RawMessage `json:"provisioning,omitempty"` } +// standingValue is whether an assignment's value is a standing assignment: neither `ended` (the module was taken off +// its node) nor `state` (a pending one) is set. A value that does not parse is not decided here: planOf refuses it. +func standingValue(value []byte) bool { + var v struct { + Ended json.RawMessage `json:"ended"` + State json.RawMessage `json:"state"` + } + if json.Unmarshal(value, &v) != nil { + return true + } + return !fieldSet(v.Ended) && !fieldSet(v.State) +} + +// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty. +func fieldSet(f json.RawMessage) bool { + s := strings.TrimSpace(string(f)) + return s != "" && s != "null" && s != `""` +} + // Plan is one module's plan on this node. type Plan struct { Templates Templates `json:"templates"` -- 2.54.0 From 84af0ed1805b4767819f4507643dd0a1f78822e7 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 02:37:28 +0200 Subject: [PATCH 4/6] Guard that the bus storage stays declared as data with a backup (issue 1098) --- .../cmd/nats-tools/backup_declared_test.go | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 modules/nats/cmd/nats-tools/backup_declared_test.go diff --git a/modules/nats/cmd/nats-tools/backup_declared_test.go b/modules/nats/cmd/nats-tools/backup_declared_test.go new file mode 100644 index 00000000..4cacebe8 --- /dev/null +++ b/modules/nats/cmd/nats-tools/backup_declared_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "os" + "strings" + "testing" +) + +// The bus's storage stays declared as data protected by a backup (novox/hq ADR 0408 clause 7; issue 1098): the +// assignments, rollouts and builds now live in the bus's buckets, so a manifest that stopped saying so would leave +// the one copy of them unprotected. The storage directory is declared as valuable data with a dump into the +// snapshots directory, which is itself declared. +func TestTheBusStorageIsDeclaredAsDataProtectedByABackup(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Data struct { + Own []struct { + ID string `json:"id"` + Path string `json:"path"` + Class string `json:"class"` + Backup *struct { + Dump string `json:"dump"` + Into string `json:"into"` + } `json:"backup"` + } `json:"own"` + } `json:"data"` + Resources []struct { + ID string `json:"id"` + Type string `json:"type"` + } `json:"resources"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + var storage, snapshots bool + for _, d := range m.Data.Own { + switch d.ID { + case "jetstream": + if d.Path != "${dir:jetstream-data}" || d.Class != "valuable" { + t.Errorf("the bus's storage is declared as %q, class %q", d.Path, d.Class) + } + if d.Backup == nil || strings.TrimSpace(d.Backup.Dump) == "" || d.Backup.Into != "snapshots" { + t.Errorf("the bus's storage has no dump into the snapshots: %+v", d.Backup) + } + storage = true + case "snapshots": + snapshots = true + } + } + if !storage || !snapshots { + t.Errorf("the bus's storage declared: %v, and its snapshots: %v", storage, snapshots) + } + var dirs []string + for _, r := range m.Resources { + if r.Type == "directory" { + dirs = append(dirs, r.ID) + } + } + for _, want := range []string{"jetstream-data", "snapshots"} { + found := false + for _, id := range dirs { + found = found || id == want + } + if !found { + t.Errorf("the directory %s is not a declared resource (directories: %v)", want, dirs) + } + } +} -- 2.54.0 From 68e3326d4cbc1d0be2c9700313e483e5be0d12bc Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 02:40:18 +0200 Subject: [PATCH 5/6] Providers and the provisioner client take the SDK that reads an ended assignment as a removed key (issue 1098) --- modules/gitea/go.mod | 2 +- modules/gitea/go.sum | 46 ++++++++++--------- modules/keycloak/go.mod | 2 +- modules/keycloak/go.sum | 2 + .../cmd/mesh-provisioner-client/client.go | 4 +- .../cmd/mesh-provisioner-client/plan.go | 19 -------- modules/mesh-provisioner-client/go.mod | 2 +- modules/mesh-provisioner-client/go.sum | 2 + modules/postgres/go.mod | 2 +- modules/postgres/go.sum | 2 + modules/route-adapter/go.mod | 2 +- modules/route-adapter/go.sum | 2 + 12 files changed, 40 insertions(+), 47 deletions(-) diff --git a/modules/gitea/go.mod b/modules/gitea/go.mod index 6ac66ac9..9629492d 100644 --- a/modules/gitea/go.mod +++ b/modules/gitea/go.mod @@ -3,7 +3,7 @@ module gitea go 1.25.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.51.0 golang.org/x/crypto v0.50.0 diff --git a/modules/gitea/go.sum b/modules/gitea/go.sum index 83c75127..85f38bd2 100644 --- a/modules/gitea/go.sum +++ b/modules/gitea/go.sum @@ -1,27 +1,29 @@ -github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= -github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= -github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= -github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= -github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= -github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= -github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= -github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 h1:EK84so3Y8JdFT3BHrzj9cPQQrDUPa/d+IdOGnerlmEI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 h1:Sv1Qzix+U1RpaUkw5IbB0IJEyZ+FqgzHmbZrCZkcWf0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/modules/keycloak/go.mod b/modules/keycloak/go.mod index 893fc271..d8fa2ddb 100644 --- a/modules/keycloak/go.mod +++ b/modules/keycloak/go.mod @@ -2,4 +2,4 @@ module keycloak go 1.25.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 diff --git a/modules/keycloak/go.sum b/modules/keycloak/go.sum index 63b4da41..1a769534 100644 --- a/modules/keycloak/go.sum +++ b/modules/keycloak/go.sum @@ -1,2 +1,4 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 h1:EK84so3Y8JdFT3BHrzj9cPQQrDUPa/d+IdOGnerlmEI= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 h1:Sv1Qzix+U1RpaUkw5IbB0IJEyZ+FqgzHmbZrCZkcWf0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go index 7d31b841..3115c58d 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go @@ -14,6 +14,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/provider" ) // The provisioner's client on one node (novox/hq ADR 0349 clauses 4 and 5, design 57 P5, issue 671). @@ -447,7 +449,7 @@ func (c *Client) take(s *state, u update) []string { case AssignmentsBucket: // A value that is not a standing assignment (ended, or in a state: ADR 0408 clause 4) is read as a removed // key, so the module leaves the machine as it does when the key goes (issue 1098). - gone = gone || !standingValue(e.Value()) + gone = gone || !provider.Standing(e.Value()) // Whom it binds, whichever machine's it is: a provider here listing its consumers is filled again. var providers []string if takeBinds(s, key, e.Value(), gone) { diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go index 82ee8977..8caf8968 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go @@ -29,25 +29,6 @@ type Assignment struct { Provisioning json.RawMessage `json:"provisioning,omitempty"` } -// standingValue is whether an assignment's value is a standing assignment: neither `ended` (the module was taken off -// its node) nor `state` (a pending one) is set. A value that does not parse is not decided here: planOf refuses it. -func standingValue(value []byte) bool { - var v struct { - Ended json.RawMessage `json:"ended"` - State json.RawMessage `json:"state"` - } - if json.Unmarshal(value, &v) != nil { - return true - } - return !fieldSet(v.Ended) && !fieldSet(v.State) -} - -// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty. -func fieldSet(f json.RawMessage) bool { - s := strings.TrimSpace(string(f)) - return s != "" && s != "null" && s != `""` -} - // Plan is one module's plan on this node. type Plan struct { Templates Templates `json:"templates"` diff --git a/modules/mesh-provisioner-client/go.mod b/modules/mesh-provisioner-client/go.mod index aaf68d4c..535479a9 100644 --- a/modules/mesh-provisioner-client/go.mod +++ b/modules/mesh-provisioner-client/go.mod @@ -3,7 +3,7 @@ module mesh-provisioner-client go 1.25.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.51.0 ) diff --git a/modules/mesh-provisioner-client/go.sum b/modules/mesh-provisioner-client/go.sum index bf0db5c6..85f38bd2 100644 --- a/modules/mesh-provisioner-client/go.sum +++ b/modules/mesh-provisioner-client/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 h1:EK84so3Y8JdFT3BHrzj9cPQQrDUPa/d+IdOGnerlmEI= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 h1:Sv1Qzix+U1RpaUkw5IbB0IJEyZ+FqgzHmbZrCZkcWf0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/postgres/go.mod b/modules/postgres/go.mod index 1b761fe0..9073f456 100644 --- a/modules/postgres/go.mod +++ b/modules/postgres/go.mod @@ -3,7 +3,7 @@ module postgres go 1.25.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 github.com/jackc/pgx/v5 v5.11.0 ) diff --git a/modules/postgres/go.sum b/modules/postgres/go.sum index 1764370b..a97bf94f 100644 --- a/modules/postgres/go.sum +++ b/modules/postgres/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 h1:EK84so3Y8JdFT3BHrzj9cPQQrDUPa/d+IdOGnerlmEI= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 h1:Sv1Qzix+U1RpaUkw5IbB0IJEyZ+FqgzHmbZrCZkcWf0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/modules/route-adapter/go.mod b/modules/route-adapter/go.mod index 961c03e3..3fcef8ed 100644 --- a/modules/route-adapter/go.mod +++ b/modules/route-adapter/go.mod @@ -2,4 +2,4 @@ module route-adapter go 1.25.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 diff --git a/modules/route-adapter/go.sum b/modules/route-adapter/go.sum index 63b4da41..1a769534 100644 --- a/modules/route-adapter/go.sum +++ b/modules/route-adapter/go.sum @@ -1,2 +1,4 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0 h1:EK84so3Y8JdFT3BHrzj9cPQQrDUPa/d+IdOGnerlmEI= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261016174426-a4e7f480b3b0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6 h1:Sv1Qzix+U1RpaUkw5IbB0IJEyZ+FqgzHmbZrCZkcWf0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261018003858-f185aedbf5d6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= -- 2.54.0 From 3bc4215d872c052bce4af4ce1afc196707bd8e5c Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 18 Oct 2026 03:55:47 +0200 Subject: [PATCH 6/6] Take the bus's backup guard out of modules/nats: it would move the bus (issue 1098, guard follows in issue 1104) --- .../cmd/nats-tools/backup_declared_test.go | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 modules/nats/cmd/nats-tools/backup_declared_test.go diff --git a/modules/nats/cmd/nats-tools/backup_declared_test.go b/modules/nats/cmd/nats-tools/backup_declared_test.go deleted file mode 100644 index 4cacebe8..00000000 --- a/modules/nats/cmd/nats-tools/backup_declared_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "strings" - "testing" -) - -// The bus's storage stays declared as data protected by a backup (novox/hq ADR 0408 clause 7; issue 1098): the -// assignments, rollouts and builds now live in the bus's buckets, so a manifest that stopped saying so would leave -// the one copy of them unprotected. The storage directory is declared as valuable data with a dump into the -// snapshots directory, which is itself declared. -func TestTheBusStorageIsDeclaredAsDataProtectedByABackup(t *testing.T) { - raw, err := os.ReadFile("../../module.json") - if err != nil { - t.Fatal(err) - } - var m struct { - Data struct { - Own []struct { - ID string `json:"id"` - Path string `json:"path"` - Class string `json:"class"` - Backup *struct { - Dump string `json:"dump"` - Into string `json:"into"` - } `json:"backup"` - } `json:"own"` - } `json:"data"` - Resources []struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"resources"` - } - if err := json.Unmarshal(raw, &m); err != nil { - t.Fatal(err) - } - var storage, snapshots bool - for _, d := range m.Data.Own { - switch d.ID { - case "jetstream": - if d.Path != "${dir:jetstream-data}" || d.Class != "valuable" { - t.Errorf("the bus's storage is declared as %q, class %q", d.Path, d.Class) - } - if d.Backup == nil || strings.TrimSpace(d.Backup.Dump) == "" || d.Backup.Into != "snapshots" { - t.Errorf("the bus's storage has no dump into the snapshots: %+v", d.Backup) - } - storage = true - case "snapshots": - snapshots = true - } - } - if !storage || !snapshots { - t.Errorf("the bus's storage declared: %v, and its snapshots: %v", storage, snapshots) - } - var dirs []string - for _, r := range m.Resources { - if r.Type == "directory" { - dirs = append(dirs, r.ID) - } - } - for _, want := range []string{"jetstream-data", "snapshots"} { - found := false - for _, id := range dirs { - found = found || id == want - } - if !found { - t.Errorf("the directory %s is not a declared resource (directories: %v)", want, dirs) - } - } -} -- 2.54.0