Merge main (upgrade safety's declared packages, issue 1840) and pin the R4e mesh-sdk member's final head 875c680d, re-vendored here and in modules/route-proxy; the controller announces declared-packages readable (issue 1790, D1; E)
This commit is contained in:
@@ -16,10 +16,10 @@ import (
|
||||
// StatesAnnouncedBucket is the controller's bucket of the last announcement of each module (announce.KeptBucket).
|
||||
const StatesAnnouncedBucket = announce.KeptBucket
|
||||
|
||||
// controllerReadable are the controller's states others may read: the record mirrors, the chain's buckets, the checks
|
||||
// running and the plans building.
|
||||
// controllerReadable are the controller's states others may read: the record mirrors (the packages each node declares
|
||||
// among them, which the pacman module reads, ADR 0507 §5), the chain's buckets, the checks running and the plans building.
|
||||
var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts,
|
||||
ChecksRunning, PlansBuilding}
|
||||
ChecksRunning, PlansBuilding, MirrorDeclaredPackages}
|
||||
|
||||
// ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local
|
||||
// name, the ones others may read marked readable. It announces itself, as any module does from its code.
|
||||
|
||||
@@ -24,8 +24,9 @@ func TestAReadIsGrantedFromItsOwnersAnnouncement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// D1 (ADR 0495 clause 2): the controller announces every bucket of its own, and readable exactly the record mirrors,
|
||||
// the chain's buckets, the checks running and the plans building.
|
||||
// D1 (ADR 0495 clause 2): the controller announces every bucket of its own, and readable exactly the record mirrors (the
|
||||
// declared packages the pacman module reads among them, ADR 0507 §5), the chain's buckets, the checks running and the
|
||||
// plans building.
|
||||
func TestTheControllerAnnouncesWhatItKeeps(t *testing.T) {
|
||||
s := ControllerAnnouncement("novox")
|
||||
if _, err := announce.Encode(s); err != nil {
|
||||
@@ -38,7 +39,7 @@ func TestTheControllerAnnouncesWhatItKeeps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
want := []string{ChainBuilds, ChecksRunning, MirrorDeclaredConfig, PlansBuilding, MirrorPredefined, ChainRollouts,
|
||||
MirrorAssignments}
|
||||
MirrorAssignments, MirrorDeclaredPackages}
|
||||
slices.Sort(want)
|
||||
if !slices.Equal(readable, want) {
|
||||
t.Fatalf("readable %v, want %v", readable, want)
|
||||
|
||||
@@ -104,6 +104,9 @@ var (
|
||||
AssignmentsBucket = BucketName(ControllerSeat, MirrorAssignments)
|
||||
DeclaredConfigBucket = BucketName(ControllerSeat, MirrorDeclaredConfig)
|
||||
PredefinedBucket = BucketName(ControllerSeat, MirrorPredefined)
|
||||
// DeclaredPackagesBucket holds, keyed by the node, each package the modules assigned to it declare, present or absent,
|
||||
// with the declaring modules: what the pacman module's verbs read to say a package is declared (ADR 0507 §5).
|
||||
DeclaredPackagesBucket = BucketName(ControllerSeat, MirrorDeclaredPackages)
|
||||
)
|
||||
|
||||
// The mirrors' local names, as a reader names them in `reads` after `mesh-controller.`.
|
||||
@@ -111,6 +114,8 @@ const (
|
||||
MirrorAssignments = "assignments"
|
||||
MirrorDeclaredConfig = "declared-config"
|
||||
MirrorPredefined = "predefined"
|
||||
// MirrorDeclaredPackages is the local name of the packages each node's modules declare (ADR 0507 §5, issue 1840/S3).
|
||||
MirrorDeclaredPackages = "declared-packages"
|
||||
)
|
||||
|
||||
// The chain's buckets (novox/hq ADR 0408, issue 1089): builds, rollouts and assignments are one chain kept on the bus
|
||||
@@ -216,6 +221,7 @@ func IsControllerBucket(bucket string) bool {
|
||||
return bucket == CallsBucket || bucket == HandActsBucket || bucket == ConditionsBucket ||
|
||||
bucket == ConditionHistoryBucket || bucket == LeaseBucket || bucket == AskedBucket ||
|
||||
bucket == AssignmentsBucket || bucket == DeclaredConfigBucket || bucket == PredefinedBucket ||
|
||||
bucket == DeclaredPackagesBucket ||
|
||||
bucket == MergeWindowBucket || bucket == RolloutSilentBucket || bucket == CheckRunningBucket ||
|
||||
bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket ||
|
||||
bucket == PlansBuildingBucket || bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket
|
||||
@@ -224,7 +230,7 @@ func IsControllerBucket(bucket string) bool {
|
||||
// ControllerBuckets are the controller's own buckets, in the order they are asserted.
|
||||
func ControllerBuckets() []string {
|
||||
return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket,
|
||||
AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, MergeWindowBucket, RolloutSilentBucket, CheckRunningBucket,
|
||||
AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket, CheckRunningBucket,
|
||||
DeliveryMergesBucket, PlansBuildingBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket}
|
||||
}
|
||||
|
||||
@@ -452,6 +458,9 @@ func (j *JetStream) EnsureControllerBuckets() error {
|
||||
{PredefinedBucket, "the list of predefined keys the mesh published, the newest trunk build's of the " +
|
||||
"catalogue, one key `list` (novox/hq ADR 0348 §7, design 57 K3): written by the controller alone from " +
|
||||
"its store when a newer list is kept, read through `reads`"},
|
||||
{DeclaredPackagesBucket, "each package the modules assigned to a node declare, present or absent, with the " +
|
||||
"declaring modules, keyed by the node (novox/hq ADR 0507 §5): written by the controller alone when it " +
|
||||
"composes the node, only on a change, read through `reads`"},
|
||||
} {
|
||||
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
|
||||
Bucket: m.bucket,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Issue 1840/S3 (ADR 0507 §5), 1840/AC5: a module whose manifest reads mesh-controller.declared-packages may name it in
|
||||
// `reads`, and reads the bucket and writes none; the controller writes it, so it is no mirror written by another, and it is
|
||||
// one of the controller's own buckets, asserted at its start.
|
||||
func TestAModuleReadingDeclaredPackagesReadsAndWritesNone(t *testing.T) {
|
||||
if !slices.Contains(controllerReadableStates(), "declared-packages") ||
|
||||
BucketName(ControllerSeat, MirrorDeclaredPackages) != DeclaredPackagesBucket {
|
||||
t.Fatalf("a module cannot name %s in `reads`: %v", DeclaredPackagesBucket, controllerReadableStates())
|
||||
}
|
||||
if _, other := MirrorsWrittenByAnother[MirrorDeclaredPackages]; other {
|
||||
t.Error("the controller writes the declared packages itself")
|
||||
}
|
||||
if !IsControllerBucket(DeclaredPackagesBucket) || !slices.Contains(ControllerBuckets(), DeclaredPackagesBucket) {
|
||||
t.Errorf("%s is not among the controller's buckets", DeclaredPackagesBucket)
|
||||
}
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "pacman", PasswordHash: "x",
|
||||
Reads: []string{"mesh-controller.declared-packages"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stream := "KV_" + DeclaredPackagesBucket
|
||||
has(t, perms.Publish, "$JS.API.STREAM.INFO."+stream)
|
||||
has(t, perms.Publish, "$JS.API.DIRECT.GET."+stream+".>")
|
||||
has(t, perms.Publish, "$JS.API.CONSUMER.CREATE."+stream+".>")
|
||||
for _, p := range perms.Publish {
|
||||
if SubjectsOverlap(p, "$KV."+DeclaredPackagesBucket+".one") {
|
||||
t.Errorf("the reading module may write %s through %q", DeclaredPackagesBucket, p)
|
||||
}
|
||||
}
|
||||
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, controller.Publish, "$KV."+DeclaredPackagesBucket+".>")
|
||||
}
|
||||
@@ -440,6 +440,11 @@ var DegradableGrants = []DegradableGrant{
|
||||
Reason: "refused: the controller then says no plan builds, and no check is held while a delivery builds — a check run " +
|
||||
"wrongly costs a delivery a place, a check held wrongly costs a pull request its turn (ADR 0483 clause 2, issue 1687)",
|
||||
},
|
||||
{
|
||||
Subject: "$KV." + DeclaredPackagesBucket + ".>",
|
||||
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
|
||||
"first push, and keeps the packages each node declares only after that, holding the lease (ADR 0507 §5, issue 1840)",
|
||||
},
|
||||
{
|
||||
Subject: "mesh.seat." + ControllerSeat + ".event.sent",
|
||||
Reason: "refused: the controller says a tier sent to a machine only for outsiders, and a send that could not " +
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package broker
|
||||
|
||||
import "testing"
|
||||
|
||||
// 1840/AC9: a process account declaring the state `x` may watch and list the keys of its module's own bucket (a consumer
|
||||
// created and deleted on KV_<module>_x), read and write it, and has no grant on another module's bucket or on a mirror of the
|
||||
// controller's, whatever it is named after.
|
||||
func TestAProcessMayWatchItsOwnStateAndNoOthers(t *testing.T) {
|
||||
p := Principal{Kind: KindProcess, Node: "novox", Module: "mesh-mail", Process: "send", State: []string{"x"}}
|
||||
got := perms(t, p)
|
||||
for _, s := range []string{
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-mail_x",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-mail_x.watcher",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-mail_x.watcher.$KV.mesh-mail_x.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-mail_x.watcher",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-mail_x.$KV.mesh-mail_x.key",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-mail_x",
|
||||
"$JS.FC.KV_mesh-mail_x.watcher",
|
||||
"$KV.mesh-mail_x.key",
|
||||
} {
|
||||
if !allowed(got.Publish, s) {
|
||||
t.Errorf("the process may not publish %s, so it cannot watch or list its own state", s)
|
||||
}
|
||||
}
|
||||
for _, other := range []string{"mesh-other_x", "mesh-mail_y", DeclaredPackagesBucket, AssignmentsBucket} {
|
||||
for _, s := range []string{
|
||||
"$JS.API.CONSUMER.CREATE.KV_" + other,
|
||||
"$JS.API.CONSUMER.CREATE.KV_" + other + ".watcher",
|
||||
"$JS.API.CONSUMER.CREATE.KV_" + other + ".watcher.$KV." + other + ".>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_" + other + ".watcher",
|
||||
"$JS.API.DIRECT.GET.KV_" + other + ".$KV." + other + ".key",
|
||||
"$JS.API.STREAM.INFO.KV_" + other,
|
||||
"$JS.FC.KV_" + other + ".watcher",
|
||||
"$KV." + other + ".key",
|
||||
} {
|
||||
if allowed(got.Publish, s) {
|
||||
t.Errorf("the process may publish %s, on a bucket that is not its own state", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ accounts {
|
||||
]
|
||||
users = [
|
||||
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
|
||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_checks-running.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_plans-building.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.check.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
|
||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_checks-running.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_plans-building.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.check.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
|
||||
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
|
||||
@@ -260,6 +260,9 @@ var WritersTable = []WriterRow{
|
||||
{State: "the predefined-keys mirror", Writer: "controller (lease holder), from its store, only on a change",
|
||||
KeptIn: "key-value " + PredefinedBucket, Others: "read through `reads` as mesh-controller." + MirrorPredefined,
|
||||
Subjects: kvOf(PredefinedBucket), Writes: isController},
|
||||
{State: "the declared-packages mirror", Writer: "controller (lease holder), from the composition of each node, only on a change",
|
||||
KeptIn: "key-value " + DeclaredPackagesBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredPackages,
|
||||
Subjects: kvOf(DeclaredPackagesBucket), Writes: isController},
|
||||
// What a machine's provisioner client placed of a module's files (novox/hq ADR 0349 §4, design 57 P6): its own
|
||||
// machine's word alone, which a walk's end and a gate wait for (ADR 0362, ADR 0349 §15). Kept newest per module
|
||||
// in the stream ProvAppliedStream, and by the client in the provisioner server's `applied` bucket.
|
||||
|
||||
@@ -52,6 +52,7 @@ var designRows = []string{
|
||||
"a declaration's large files",
|
||||
"the declared-config mirror",
|
||||
"the predefined-keys mirror",
|
||||
"the declared-packages mirror",
|
||||
// The provisioner client's word on what it placed (novox/hq ADR 0349 §4, design 57 P6).
|
||||
"a module's files placed on a machine",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// aNodesPackages is the resources of a node whose modules declare packages: one package two modules declare present, one
|
||||
// declared absent, and a resource that is no package.
|
||||
func aNodesPackages() ([]map[string]any, map[string]string) {
|
||||
return []map[string]any{
|
||||
{"id": "web.pkg-curl", "type": "package", "package": "curl"},
|
||||
{"id": "backup.pkg-curl", "type": "package", "package": "curl"},
|
||||
{"id": "web.pkg-nano", "type": "package", "package": "nano", "absent": true},
|
||||
{"id": "web.conf", "type": "file", "path": "/etc/web.conf"},
|
||||
},
|
||||
map[string]string{"web.pkg-curl": "web", "backup.pkg-curl": "backup", "web.pkg-nano": "web", "web.conf": "web"}
|
||||
}
|
||||
|
||||
// 1840/AC5: the declared-packages mirror's value for a node names each package with the modules that declare it and whether
|
||||
// they declare it absent, and the same declaration is the same bytes however its resources are ordered, so a pass over an
|
||||
// unchanged node writes nothing.
|
||||
func TestTheDeclaredPackagesValueNamesEachPackageWithItsModules(t *testing.T) {
|
||||
resources, owner := aNodesPackages()
|
||||
got, err := DeclaredPackagesValue("one", resources, owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `{"node":"one","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
|
||||
`{"package":"nano","modules":["web"],"absent":true}]}`
|
||||
if string(got) != want {
|
||||
t.Errorf("the value is\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
reversed := slices.Clone(resources)
|
||||
slices.Reverse(reversed)
|
||||
again, err := DeclaredPackagesValue("one", reversed, owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, again) {
|
||||
t.Errorf("the same declaration in another order is other bytes:\n%s\n%s", got, again)
|
||||
}
|
||||
if got := jsonFields(reflect.TypeOf(DeclaredPackagesMirrored{})); !slices.Equal(got, []string{"node", "packages"}) {
|
||||
t.Errorf("the value carries %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A node declaring no package has a value that says so, an empty list and not null.
|
||||
func TestANodeDeclaringNoPackageHasAnEmptyList(t *testing.T) {
|
||||
got, err := DeclaredPackagesValue("one", nil, nil)
|
||||
if err != nil || string(got) != `{"node":"one","packages":[]}` {
|
||||
t.Errorf("the value is %s (%v)", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1840/S3: a node's value stays under the 64 KiB a mirror's key holds at far more packages than a node declares: 600
|
||||
// packages, each declared by four modules, is a few times what the largest node declares today.
|
||||
func TestTheDeclaredPackagesValueOfABigNodeFitsAKey(t *testing.T) {
|
||||
var resources []map[string]any
|
||||
owner := map[string]string{}
|
||||
for p := 0; p < 600; p++ {
|
||||
for m := 0; m < 4; m++ {
|
||||
id := fmt.Sprintf("module-%d.pkg-%d", m, p)
|
||||
resources = append(resources, map[string]any{"id": id, "type": "package", "package": fmt.Sprintf("package-%d", p)})
|
||||
owner[id] = fmt.Sprintf("module-%d", m)
|
||||
}
|
||||
}
|
||||
got, err := DeclaredPackagesValue("one", resources, owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v DeclaredPackagesMirrored
|
||||
if err := json.Unmarshal(got, &v); err != nil || len(v.Packages) != 600 {
|
||||
t.Fatalf("the value holds %d packages (%v)", len(v.Packages), err)
|
||||
}
|
||||
if len(got) >= 64<<10 {
|
||||
t.Errorf("the value is %d bytes, over the 64 KiB a key holds", len(got))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
@@ -191,3 +192,65 @@ func DeclaredOf(settings map[string]catalogue.SettingDeclaration) (map[string]De
|
||||
}
|
||||
return declaredMirrored(raw)
|
||||
}
|
||||
|
||||
// PackageDeclared is one package a node's modules declare, as the declared-packages mirror holds it (ADR 0507 §5): its
|
||||
// name, the modules that declare it that way, sorted, and whether they declare it absent. A package some modules declare
|
||||
// present and others absent is two entries; the composition refuses that pair, so it is seen only while it is refused.
|
||||
type PackageDeclared struct {
|
||||
Package string `json:"package"`
|
||||
Modules []string `json:"modules"`
|
||||
Absent bool `json:"absent"`
|
||||
}
|
||||
|
||||
// DeclaredPackagesMirrored is one node's value of the declared-packages mirror, keyed by the node: its packages sorted by
|
||||
// name (absent after present). Fixed fields and no time, so the same declaration is the same bytes (ADR 0289 §1).
|
||||
type DeclaredPackagesMirrored struct {
|
||||
Node string `json:"node"`
|
||||
Packages []PackageDeclared `json:"packages"`
|
||||
}
|
||||
|
||||
// DeclaredPackagesValue is the mirror's value for one node, from the resources its composition declares and the module that
|
||||
// owns each (Composed.Owner, by resource id). A resource that is no package, or names none, is left out.
|
||||
func DeclaredPackagesValue(node string, resources []map[string]any, owner map[string]string) ([]byte, error) {
|
||||
type key struct {
|
||||
name string
|
||||
absent bool
|
||||
}
|
||||
by := map[key]map[string]bool{}
|
||||
for _, r := range resources {
|
||||
if fmt.Sprint(r["type"]) != "package" {
|
||||
continue
|
||||
}
|
||||
name, _ := r["package"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
k := key{name, r["absent"] == true}
|
||||
if by[k] == nil {
|
||||
by[k] = map[string]bool{}
|
||||
}
|
||||
if module := owner[fmt.Sprint(r["id"])]; module != "" {
|
||||
by[k][module] = true
|
||||
}
|
||||
}
|
||||
keys := make([]key, 0, len(by))
|
||||
for k := range by {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].name != keys[j].name {
|
||||
return keys[i].name < keys[j].name
|
||||
}
|
||||
return !keys[i].absent && keys[j].absent
|
||||
})
|
||||
v := DeclaredPackagesMirrored{Node: node, Packages: []PackageDeclared{}}
|
||||
for _, k := range keys {
|
||||
modules := make([]string, 0, len(by[k]))
|
||||
for m := range by[k] {
|
||||
modules = append(modules, m)
|
||||
}
|
||||
sort.Strings(modules)
|
||||
v.Packages = append(v.Packages, PackageDeclared{Package: k.name, Modules: modules, Absent: k.absent})
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
@@ -57,10 +57,13 @@ var (
|
||||
// mesh-config publishes as its own (design 57 K3).
|
||||
Predefined = Mirror{Name: broker.MirrorPredefined, Bucket: broker.PredefinedBucket,
|
||||
Event: link.KeyPredefinedChanged}
|
||||
// DeclaredPackages is each node's packages its modules declare, keyed by the node (ADR 0507 §5). It raises no event:
|
||||
// the seat's emits, which the SDK states, do not name one, and a reader reads the bucket when it is asked.
|
||||
DeclaredPackages = Mirror{Name: broker.MirrorDeclaredPackages, Bucket: broker.DeclaredPackagesBucket}
|
||||
)
|
||||
|
||||
// Mirrors is every mirror this package writes, in the order they are written.
|
||||
func Mirrors() []Mirror { return []Mirror{DeclaredConfig, Predefined} }
|
||||
func Mirrors() []Mirror { return []Mirror{DeclaredConfig, Predefined, DeclaredPackages} }
|
||||
|
||||
// The changes an event says.
|
||||
const (
|
||||
@@ -251,7 +254,7 @@ func (w *Writer) put(ctx context.Context, key string, value []byte, before kept,
|
||||
|
||||
// say raises one write's event, tried once more when refused and then counted as lost.
|
||||
func (w *Writer) say(ctx context.Context, key, change string, fields []string, revision uint64) {
|
||||
if w.Announce == nil {
|
||||
if w.Announce == nil || w.Mirror.Event == "" {
|
||||
return
|
||||
}
|
||||
e := Event{Mirror: w.Mirror.Name, Key: key, Change: change, Fields: fields, Revision: revision, At: w.now().UTC()}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
@@ -246,3 +247,49 @@ func TestNatsARemovalWithoutTheLeaseRemovesNothing(t *testing.T) {
|
||||
t.Errorf("the key was removed without the lease: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1840/AC5 (ADR 0507 §5, ADR 0289 §1): the declared-packages mirror, keyed by the node, writes a node's value once; a second
|
||||
// pass over the same declaration writes nothing (the bucket takes no message); a package newly declared changes the one key;
|
||||
// a node gone removes its key. It raises no event: the seat's emits name none for it.
|
||||
func TestNatsTheDeclaredPackagesMirrorWritesOnlyOnAChange(t *testing.T) {
|
||||
js, api := aMirrorBus(t)
|
||||
events := listen(t, js.Conn(), "mesh.seat."+broker.ControllerSeat+".event.>")
|
||||
w := aWriter(t, js, api, DeclaredPackages)
|
||||
ctx := t.Context()
|
||||
value := func(node string, resources ...map[string]any) []byte {
|
||||
t.Helper()
|
||||
owner := map[string]string{}
|
||||
for _, r := range resources {
|
||||
owner[r["id"].(string)] = "web"
|
||||
}
|
||||
v, err := inventory.DeclaredPackagesValue(node, resources, owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
curl := map[string]any{"id": "web.curl", "type": "package", "package": "curl"}
|
||||
nano := map[string]any{"id": "web.nano", "type": "package", "package": "nano", "absent": true}
|
||||
|
||||
if r, err := w.Sync(ctx, map[string][]byte{"one": value("one", curl), "two": value("two")}); err != nil ||
|
||||
r != (Result{Written: 2}) {
|
||||
t.Fatalf("the first pass wrote %+v (%v)", r, err)
|
||||
}
|
||||
before := writes(t, api, DeclaredPackages.Bucket)
|
||||
if r, err := w.Sync(ctx, map[string][]byte{"one": value("one", curl), "two": value("two")}); err != nil || r != (Result{}) {
|
||||
t.Errorf("a pass over the same declarations wrote %+v (%v)", r, err)
|
||||
}
|
||||
if got := writes(t, api, DeclaredPackages.Bucket) - before; got != 0 {
|
||||
t.Errorf("a pass over the same declarations put %d messages in the bucket", got)
|
||||
}
|
||||
if r, err := w.Sync(ctx, map[string][]byte{"one": value("one", curl, nano), "two": value("two")}); err != nil ||
|
||||
r != (Result{Written: 1}) {
|
||||
t.Errorf("a package newly declared wrote %+v (%v)", r, err)
|
||||
}
|
||||
if r, err := w.Sync(ctx, map[string][]byte{"one": value("one", curl, nano)}); err != nil || r != (Result{Removed: 1}) {
|
||||
t.Errorf("a node gone wrote %+v (%v)", r, err)
|
||||
}
|
||||
if said := settle(events, 1); len(said) != 0 {
|
||||
t.Errorf("the declared-packages mirror raised %+v", said)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user