Merge pull request 'R4e (mesh-controller): deliver from the builder's keys, ask build-agent only for its own modules, follow the forge's protection for checks, announce the controller's states' (#441) from feat/r4e into main

This commit was merged in pull request #441.
This commit is contained in:
2026-10-22 22:12:58 +00:00
186 changed files with 8256 additions and 4507 deletions
+57
View File
@@ -0,0 +1,57 @@
package artifacts
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
// blobBound is the most GetBlob reads: a templates archive is a few kilobytes; anything near this is no archive a
// take-in reads text from.
const blobBound = 64 << 20
// GetBlob is the archive a recorded reference names (`artifact-store://<repository>/blobs/sha256:<hex>`), read whole
// and checked against the digest it names: bytes that hash to another digest are refused, never read (R4e: the
// controller reads a templates archive by the reference of a builder's done key). Gone when the store does not hold
// it; ErrNotOurs for a reference outside the store or one that is not an archive.
func (s Store) GetBlob(ctx context.Context, reference string) ([]byte, error) {
repository, digest, archive, err := s.archive(reference)
if err != nil {
return nil, err
}
if !archive {
return nil, fmt.Errorf("%w: %s is not an archive", ErrNotOurs, reference)
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, s.url(repository, "blobs", digest), nil)
if err != nil {
return nil, err
}
response, err := s.client().Do(request)
if err != nil {
return nil, fmt.Errorf("cannot reach the artifact store at %s: %w", s.Address, err)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
case http.StatusNotFound:
return nil, Gone
default:
return nil, fmt.Errorf("the artifact store answered %s for %s/blobs/%s", response.Status, repository, digest)
}
body, err := io.ReadAll(io.LimitReader(response.Body, blobBound+1))
if err != nil {
return nil, fmt.Errorf("reading %s/blobs/%s: %w", repository, digest, err)
}
if len(body) > blobBound {
return nil, fmt.Errorf("%s/blobs/%s is larger than %d bytes, which no archive read here is", repository, digest, blobBound)
}
sum := sha256.Sum256(body)
if got := "sha256:" + hex.EncodeToString(sum[:]); got != digest {
return nil, fmt.Errorf("the artifact store answered %s/blobs/%s with bytes of the digest %s: refused", repository,
digest, got)
}
return body, nil
}
+44
View File
@@ -0,0 +1,44 @@
package artifacts
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// A take-in reads a templates archive by its reference, and only bytes of the digest the reference names.
func TestGetBlobReadsOnlyTheBytesOfItsDigest(t *testing.T) {
good := []byte("the archive")
sum := sha256.Sum256(good)
digest := "sha256:" + hex.EncodeToString(sum[:])
liar := "sha256:" + strings.Repeat("0", 64)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(r.URL.Path, "/x/files/blobs/"+digest), strings.HasSuffix(r.URL.Path, "/x/files/blobs/"+liar):
_, _ = w.Write(good)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
s := Store{Address: strings.TrimPrefix(server.URL, "http://")}
ctx := context.Background()
body, err := s.GetBlob(ctx, "artifact-store://x/files/blobs/"+digest)
if err != nil || string(body) != string(good) {
t.Fatalf("the archive: %q %v", body, err)
}
if _, err := s.GetBlob(ctx, "artifact-store://x/files/blobs/"+liar); err == nil || !strings.Contains(err.Error(), "refused") {
t.Fatalf("bytes of another digest are read: %v", err)
}
if _, err := s.GetBlob(ctx, "artifact-store://x/other/blobs/"+digest); !errors.Is(err, Gone) {
t.Fatalf("a blob the store does not hold: %v", err)
}
if _, err := s.GetBlob(ctx, "registry.example/x@"+digest); !errors.Is(err, ErrNotOurs) {
t.Fatalf("a reference outside the store: %v", err)
}
}
+81
View File
@@ -0,0 +1,81 @@
package broker
import (
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/announce"
)
// Who may read a module's state is what the module announced (novox/hq ADR 0495 clauses 2 and 3, ADR 0513 §3; issue
// 1790). A module says on the bus which states it keeps and which others may read (announce.Subject); the controller
// keeps the last word of each (announce.KeptBucket) and grants a reader's `reads: <owner>.<name>` only when the owner
// announced that state readable. An owner that has announced nothing yet is read as before, from the reader's `reads`
// (the transition, until every module announces), and is named as not yet announced.
// 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 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, 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.
func ControllerAnnouncement(node string) announce.States {
s := announce.States{Module: ControllerSeat, Node: node}
readable := map[string]bool{}
for _, n := range controllerReadable {
readable[n] = true
}
for _, bucket := range ControllerBuckets() {
local, mine := strings.CutPrefix(bucket, ControllerSeat+"_")
if !mine {
continue
}
s.States = append(s.States, announce.State{Name: local, Readable: readable[local]})
}
sort.Slice(s.States, func(i, j int) bool { return s.States[i].Name < s.States[j].Name })
return s
}
// ReadsAnnounced splits a reader's `reads` (`<owner>.<name>`) by what each owner announced: granted when the owner
// announced the state readable, or announced nothing yet (the transition); refused when the owner announced and did
// not announce that state readable.
func ReadsAnnounced(reads []string, announced map[string]announce.States) (granted, refused []string) {
for _, r := range reads {
owner, name, ok := strings.Cut(r, ".")
s, said := announced[owner]
switch {
case !ok || !said || s.Readable(name):
granted = append(granted, r)
default:
refused = append(refused, r)
}
}
return granted, refused
}
// NotAnnounced are the owners some module reads the state of and that have announced nothing yet, sorted: read from the
// readers' `reads` as before, until they announce.
func NotAnnounced(r Records) []string {
seen := map[string]bool{}
var out []string
for _, ds := range r.Assigned {
for _, d := range ds {
for _, read := range append(append([]string(nil), d.Reads...), d.ReadsRefused...) {
owner, _, ok := strings.Cut(read, ".")
if !ok || seen[owner] {
continue
}
if _, said := r.Announced[owner]; !said {
seen[owner] = true
out = append(out, owner)
}
}
}
}
sort.Strings(out)
return out
}
+86
View File
@@ -0,0 +1,86 @@
package broker
import (
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/announce"
)
// D3 (ADR 0495 clause 3, ADR 0513 §3; issue 1790): a reader's `reads` is granted where the owner announced the state
// readable, refused where it announced and did not, and granted as before where the owner announced nothing yet.
func TestAReadIsGrantedFromItsOwnersAnnouncement(t *testing.T) {
announced := map[string]announce.States{
"owner": {Module: "owner", States: []announce.State{{Name: "open", Readable: true}, {Name: "closed"}}},
}
granted, refused := ReadsAnnounced([]string{"owner.open", "owner.closed", "owner.unsaid", "quiet.any"}, announced)
if !slices.Equal(granted, []string{"owner.open", "quiet.any"}) || !slices.Equal(refused, []string{"owner.closed", "owner.unsaid"}) {
t.Fatalf("granted %v, refused %v", granted, refused)
}
r := Records{Announced: announced, Assigned: map[string][]Declared{"one": {{Module: "reader", Reads: granted,
ReadsRefused: refused}}}}
if got := NotAnnounced(r); !slices.Equal(got, []string{"quiet"}) {
t.Fatalf("not yet announced: %v", got)
}
}
// 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 {
t.Fatal(err)
}
var readable []string
for _, k := range s.States {
if k.Readable {
readable = append(readable, k.Name)
}
}
want := []string{ChainBuilds, ChecksRunning, MirrorDeclaredConfig, PlansBuilding, MirrorPredefined, ChainRollouts,
MirrorAssignments, MirrorDeclaredPackages}
slices.Sort(want)
if !slices.Equal(readable, want) {
t.Fatalf("readable %v, want %v", readable, want)
}
for _, name := range []string{"lease", "calls", "states-announced", "merge-window"} {
if !s.Keeps(name) || s.Readable(name) {
t.Errorf("%s: kept %v, readable %v", name, s.Keeps(name), s.Readable(name))
}
}
perms, err := PermissionsFor(Principal{Kind: KindController})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(perms.Publish, announce.Subject(ControllerSeat)) {
t.Fatal("the controller may not say what it keeps")
}
}
// D2: every account that keeps state may say so on its own subject, a process account included, and no other's.
func TestAnAccountKeepingStateMaySayWhatItKeeps(t *testing.T) {
keeper, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "keeper", PasswordHash: "x",
State: []string{"things"}})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(keeper.Publish, announce.Subject("keeper")) || slices.Contains(keeper.Publish, announce.Subject("other")) {
t.Fatalf("the keeper's announcement grants: %v", keeper.Publish)
}
process, err := PermissionsFor(Principal{Kind: KindProcess, Node: "one", Module: "keeper", Process: "send",
PasswordHash: "x", State: []string{"sends"}})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(process.Publish, announce.Subject("keeper")) {
t.Fatal("a process keeping state may not say what it keeps")
}
none, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "stateless", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
if slices.Contains(none.Publish, announce.Subject("stateless")) {
t.Fatal("a module keeping nothing may say it keeps something")
}
}
@@ -0,0 +1,13 @@
package broker
// controllerReadableStates are the controller's states its announcement makes readable (ADR 0495 clause 2): what a
// module may name in `reads` after `mesh-controller.`.
func controllerReadableStates() []string {
var out []string
for _, s := range ControllerAnnouncement("").States {
if s.Readable {
out = append(out, s.Name)
}
}
return out
}
@@ -8,8 +8,8 @@ import (
// 1155/AC1: a module whose manifest reads mesh-controller.checks-running may watch and get every key of the bucket and
// write none.
func TestAModuleReadingChecksRunningReadsAndWritesNone(t *testing.T) { // 1155/AC1
if !slices.Contains(ControllerMirrors(), "checks-running") || BucketName(ControllerSeat, "checks-running") != CheckRunningBucket {
t.Fatalf("a module cannot name %s in `reads`: %v", CheckRunningBucket, ControllerMirrors())
if !slices.Contains(controllerReadableStates(), "checks-running") || BucketName(ControllerSeat, "checks-running") != CheckRunningBucket {
t.Fatalf("a module cannot name %s in `reads`: %v", CheckRunningBucket, controllerReadableStates())
}
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "mesh-delivery-watchman", PasswordHash: "x",
Reads: []string{"mesh-controller.checks-running"}})
+14 -8
View File
@@ -155,12 +155,6 @@ const (
// ChainBuckets are the three buckets of the chain: builds, rollouts and assignments.
func ChainBuckets() []string { return []string{BuildsBucket, RolloutsBucket, AssignmentsBucket} }
// ControllerMirrors are the local names of the controller's buckets a module may read: the record mirrors and the
// chain's buckets.
func ControllerMirrors() []string {
return []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts, ChecksRunning, PlansBuilding, MirrorDeclaredPackages}
}
// MirrorsWrittenByAnother are the controller's mirrors whose writer is not the controller, each with its reason: the rule
// that a mirror is written by the controller alone (TestTheMirrorsListAndTheWritersTableAgree) has these exceptions and no
// other.
@@ -230,14 +224,14 @@ func IsControllerBucket(bucket string) bool {
bucket == DeclaredPackagesBucket ||
bucket == MergeWindowBucket || bucket == RolloutSilentBucket || bucket == CheckRunningBucket ||
bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket ||
bucket == PlansBuildingBucket || bucket == delivery.HaveBucket
bucket == PlansBuildingBucket || bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket
}
// 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, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket, CheckRunningBucket,
DeliveryMergesBucket, PlansBuildingBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket}
DeliveryMergesBucket, PlansBuildingBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket}
}
// ControllerBucketsAsserter is what raising the controller's buckets needs of a connection.
@@ -396,6 +390,18 @@ func (j *JetStream) EnsureControllerBuckets() error {
}); err != nil {
return fmt.Errorf("asserting bucket %s: %w", PlansBuildingBucket, err)
}
// **The states each module announced** (novox/hq ADR 0495 clauses 2 and 3, issue 1790): one key a module, its last word.
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: StatesAnnouncedBucket,
Description: "the states each module announced it keeps and which of them others may read (novox/hq ADR 0495): " +
"`<module>` holds its last announcement; what a reader's `reads` is granted from; written by the controller alone",
History: 1,
MaxValueSize: 64 << 10,
MaxBytes: 16 << 20,
Storage: jetstream.FileStorage,
}); err != nil {
return fmt.Errorf("asserting bucket %s: %w", StatesAnnouncedBucket, err)
}
// **The merges mesh-delivery announced: keys that age** (ADR 0420 §7, issue 1025). One message a key; a week.
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: DeliveryMergesBucket,
+3 -3
View File
@@ -100,7 +100,7 @@ func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
// stands for their declaration, so the report of state nothing declares never names one.
func TestAListedMirrorIsNotReportedAsUndeclared(t *testing.T) {
names := []string{"gone_state"}
for _, m := range ControllerMirrors() {
for _, m := range controllerReadableStates() {
names = append(names, BucketName(ControllerSeat, m))
}
undeclared, err := RaiseBuckets(fakeBuckets{names: names}, nil)
@@ -132,7 +132,7 @@ func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
}
}
listed := map[string]bool{}
for _, m := range ControllerMirrors() {
for _, m := range controllerReadableStates() {
bucket := BucketName(ControllerSeat, m)
listed[bucket] = true
if why, other := MirrorsWrittenByAnother[m]; other { // 1155/AC3: the exception, with its reason
@@ -292,7 +292,7 @@ func TestEveryChainBucketIsOnTheControllersListAndHasAWriterRow(t *testing.T) {
t.Errorf("%s is not on the controller's list of buckets", bucket)
}
local := strings.TrimPrefix(bucket, ControllerSeat+"_")
if !slices.Contains(ControllerMirrors(), local) {
if !slices.Contains(controllerReadableStates(), local) {
t.Errorf("a module cannot name %s in `reads`", bucket)
}
rows := 0
@@ -9,9 +9,9 @@ import (
// `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(ControllerMirrors(), "declared-packages") ||
if !slices.Contains(controllerReadableStates(), "declared-packages") ||
BucketName(ControllerSeat, MirrorDeclaredPackages) != DeclaredPackagesBucket {
t.Fatalf("a module cannot name %s in `reads`: %v", DeclaredPackagesBucket, ControllerMirrors())
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")
+3 -1
View File
@@ -2,6 +2,7 @@ package broker
import (
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/announce"
"slices"
"sort"
"strings"
@@ -407,7 +408,8 @@ func withPrefix(patterns []string, prefixes ...string) []string {
func eventsSaid(p Permissions) []string {
var out []string
for _, s := range withPrefix(p.Publish, "mesh.mod.", "mesh.stream.") {
if strings.Contains(s, ".event.") {
// A module keeping state says what it keeps (ADR 0495 clause 2): no event of its declaration.
if strings.Contains(s, ".event.") && !strings.HasSuffix(s, ".event."+announce.Event) {
out = append(out, s)
}
}
+13
View File
@@ -15,6 +15,7 @@ package broker
import (
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/announce"
"regexp"
"slices"
"sort"
@@ -389,6 +390,16 @@ type DegradableGrant struct {
// works. Every other grant stays required — missing one of those is a controller that comes up and is
// refused on the first thing it tries, with no fallback that covers it.
var DegradableGrants = []DegradableGrant{
{
Subject: announce.Subject(ControllerSeat),
Reason: "refused: the controller's own announcement waits for its first push, and until it is kept every reader of " +
"its states is granted from its `reads` as before (novox/hq ADR 0495, issue 1790)",
},
{
Subject: "$KV." + StatesAnnouncedBucket + ".>",
Reason: "refused: no announcement is kept until the controller's first push, and until then every reader is " +
"granted from its `reads` as before (novox/hq ADR 0495, issue 1790)",
},
{
Subject: "mesh.seat." + ControllerSeat + ".tool.push",
Reason: "refused: the ask of a held move (Release) is answered, and the refusal told, while the operator's own " +
@@ -756,6 +767,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
// **And its own buckets** (novox/hq to-be 45 §1): the calls it served and the acts done by
// hand, which it alone writes. A put is a publish to the bucket's subject, which `$JS.API.>`
// does not cover; each bucket named, not `$KV.>`, which would let it write any module's state.
// And says what it keeps, as every module does (novox/hq ADR 0495 clause 2, issue 1790).
pub = append(pub, announce.Subject(ControllerSeat))
for _, bucket := range ControllerBuckets() {
if bucket == delivery.HaveBucket {
continue // each node's node-engine writes its own keys of it, the controller none (ADR 0429 §1)
@@ -8,8 +8,8 @@ import (
// Issue 1687 (ADR 0483 clause 2): a module whose manifest reads mesh-controller.plans-building may name it in `reads`, and
// reads the bucket and writes none; the controller writes it, so it is no mirror written by another.
func TestAModuleReadingPlansBuildingReadsAndWritesNone(t *testing.T) {
if !slices.Contains(ControllerMirrors(), "plans-building") || BucketName(ControllerSeat, PlansBuilding) != PlansBuildingBucket {
t.Fatalf("a module cannot name %s in `reads`: %v", PlansBuildingBucket, ControllerMirrors())
if !slices.Contains(controllerReadableStates(), "plans-building") || BucketName(ControllerSeat, PlansBuilding) != PlansBuildingBucket {
t.Fatalf("a module cannot name %s in `reads`: %v", PlansBuildingBucket, controllerReadableStates())
}
if _, other := MirrorsWrittenByAnother[PlansBuilding]; other {
t.Error("the controller writes the plans building itself")
+2
View File
@@ -48,6 +48,8 @@ func TestAProcessIsGrantedExactlyItsBusDeclaration(t *testing.T) {
"$KV.mesh-mail_sends.>",
"mesh.mod.mesh-mail.event.mail.send.failed",
"mesh.mod.mesh-mail.event.mail.send.sent",
// It keeps state, so it says what it keeps (novox/hq ADR 0495 clause 2, issue 1790).
"mesh.mod.mesh-mail.event.states",
"mesh.seat.operator-channel.call.tell.*.novox~mesh-mail~send",
"mesh.seat.operator-channel.call.tell.novox~mesh-mail~send",
"mesh.seat.operator-channel.tool.tell",
+6
View File
@@ -2,6 +2,7 @@ package broker
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/announce"
"sort"
"strings"
"time"
@@ -147,6 +148,11 @@ func stateGrants(a stateAccess) []string {
read(bucket, "")
out = append(out, "$KV."+bucket+".>")
}
// **And it says what it keeps** (novox/hq ADR 0495 clause 2, issue 1790): a module keeping state announces it on its
// own event subject, from its code; refused, its readers are read as before.
if len(a.Keeps) > 0 && safeSubject.MatchString(a.Module) {
out = append(out, announce.Subject(a.Module))
}
for _, r := range a.Reads {
if bucket, ok := bucketOfRead(r); ok {
read(bucket, "")
-67
View File
@@ -1,67 +0,0 @@
package broker_test
import (
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
)
// The facts the control plane states are named twice — in the grant that permits them and in the code
// that states them — because `link` imports `broker` and the dependency cannot go the other way. So a
// test keeps them agreeing: a subject the grant omits is refused at the moment the mesh has something
// to say, and one the grant adds that nothing states is authority nobody uses.
//
// An external test package, because it may import both while neither imports the other.
func TestTheFactsTheGrantPermitsAreTheFactsTheMeshStates(t *testing.T) {
if broker.ControllerSeat != link.MeshControllerSeat {
t.Fatalf("the grant is written for the %q seat and the mesh states its facts under %q",
broker.ControllerSeat, link.MeshControllerSeat)
}
// And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4).
states := append([]string{link.KeyApplied, link.KeyRefused, link.KeyBuiltBefore}, conditions.Events...)
states = append(states, conditions.HeartbeatEvent)
// And a value given by hand, replaced after its module's first good start (novox/hq ADR 0228).
states = append(states, link.KeySecretReplaced)
// And every act a healer takes (novox/hq to-be 45 §7).
states = append(states, link.KeyHealerActed)
// And a build put back after its gate failed (novox/hq ADR 0236).
states = append(states, link.KeyRolledBack)
// And a pull request's merge check, judged (novox/hq to-be 45 §9).
states = append(states, link.KeyChecked)
// And a walk kept in a new state, for the delivery it walks (novox/hq ADR 0239).
states = append(states, link.KeyPlanMoved)
// And a tier sent to a machine, once the send is kept (novox/hq ADR 0406 §5, issue 974).
states = append(states, link.KeySent)
// And a key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3).
states = append(states, link.KeyAssignmentChanged, link.KeyDeclaredConfigChanged, link.KeyPredefinedChanged)
for _, event := range states {
if !slices.Contains(broker.ControllerStates, event) {
t.Errorf("the mesh states %q and its account may not publish it", event)
}
}
if len(broker.ControllerStates) != len(states) {
t.Errorf("the grant permits %v, which is more than the mesh states", broker.ControllerStates)
}
// The record mirrors a module may read are the ones the bus holds (design 54 §5, design 57 F3).
if !slices.Equal(catalogue.ControllerMirrors, broker.ControllerMirrors()) {
t.Errorf("the catalogue lets a module read the controller's mirrors %v and the bus holds %v",
catalogue.ControllerMirrors, broker.ControllerMirrors())
}
// **And the seat says it.** A seat carries the protocol of its role (novox/hq ADR 0129), so the
// facts the control plane states are the seat's `emits` — which is what lets anything else declare
// that it consumes them, and what the subject-agreement check reads to know they have an owner.
var declared []string
for _, seat := range catalogue.SeatsWithAProtocol() {
if seat.Name == broker.ControllerSeat {
declared = seat.Emits
}
}
if !slices.Equal(declared, broker.ControllerStates) {
t.Errorf("the %s seat emits %v and the grant permits %v", broker.ControllerSeat,
declared, broker.ControllerStates)
}
}
+1 -1
View File
@@ -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_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.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "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" }
} }
+7
View File
@@ -2,6 +2,7 @@ package broker
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/announce"
"slices"
"sort"
)
@@ -56,6 +57,9 @@ type Declared struct {
HearsAStream bool
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0201).
Reads []string
// ReadsRefused are the reads its manifest names that the owner's announcement does not make readable (novox/hq ADR
// 0495 clause 3, issue 1790): granted nothing, kept to be said.
ReadsRefused []string
// KeyedReads are keys of other modules' state it reads, each one key alone: what a seat's holder is
// granted for the pieces the modules beside it offer (novox/hq ADR 0260).
KeyedReads []KeyedRead
@@ -126,6 +130,9 @@ type Records struct {
// account agents run as, judged unable to become root by its node-engine, and serves no login shell
// execute. A machine absent is not free: no record of a pass is no pass.
RootFree map[string]bool
// Announced is each module's last announcement of the states it keeps (novox/hq ADR 0495, issue 1790), as the
// controller keeps it; a module absent has announced nothing yet. Nil where none was read.
Announced map[string]announce.States
}
// Users is every user the composed file should contain, in the order it will be written.
+24
View File
@@ -87,6 +87,22 @@ func ownModule(p Principal, pattern string) bool {
return false
}
// ownAnnouncer is a module, a process of it or the node tools carrying it saying what that module keeps, or the
// controller saying what it keeps itself (novox/hq ADR 0495 clause 2): each under its own name alone.
func ownAnnouncer(p Principal, pattern string) bool {
tokens := strings.Split(pattern, ".")
if len(tokens) < 3 {
return false
}
switch p.Kind {
case KindController:
return tokens[2] == ControllerSeat
case KindProcess:
return p.Module == tokens[2]
}
return ownModule(p, pattern)
}
// ownProvisioner is a machine's provisioner client saying a report about its own machine: the third token of
// `prov.applied.<node>.…` is its machine (novox/hq ADR 0349 §4, design 57 P3).
func ownProvisioner(p Principal, pattern string) bool {
@@ -181,6 +197,14 @@ var WritersTable = []WriterRow{
KeptIn: "key-value " + PlansBuildingBucket + ", aged out when not refreshed",
Others: "the build seat's holders read it and take no new check while a key is there (ADR 0483)",
Subjects: kvOf(PlansBuildingBucket), Writes: isController},
// What a module keeps and who may read it (novox/hq ADR 0495 clauses 2 and 3, issue 1790): said by the module, kept by
// the controller.
{State: "a module's states announced", Writer: "the module itself, from its code, under its own name",
KeptIn: "the bus (EVENTS)", Others: "the controller keeps the last of each and grants readers from it",
Subjects: []string{"mesh.mod.*.event.states"}, Writes: ownAnnouncer},
{State: "the states announced", Writer: "controller, from each module's announcement",
KeptIn: "key-value " + StatesAnnouncedBucket, Others: "the composition of the bus's users reads it",
Subjects: kvOf(StatesAnnouncedBucket), Writes: isController},
{State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)",
KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule},
// A pull request's head, before it merges (novox/hq to-be 45 §9): the same one announcer.
+3
View File
@@ -31,6 +31,9 @@ var designRows = []string{
"a pull request's check running",
// The plans in their build phase, a bucket the build seat's holders read (ADR 0483 clause 2, issue 1687).
"the plans-building mirror",
// What each module keeps and who may read it, said by the module and kept by the controller (ADR 0495, issue 1790).
"a module's states announced",
"the states announced",
"a merge announced",
"a pull request's head announced",
"a pull request's merge check",
+4 -27
View File
@@ -23,6 +23,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-controller/internal/buildreads"
)
// Turning a repository into artifacts the mesh can pin.
@@ -2002,33 +2003,9 @@ func instructions(recipe string) []string {
return out
}
// readBy is every repository other than the module's own that this build's recipes read source from,
// each once and in a fixed order, so two builds of one commit report the same thing the same way.
func readBy(manifest catalogue.Manifest) []catalogue.ArtifactContext {
if manifest.Build == nil {
return nil
}
seen := map[string]bool{}
var out []catalogue.ArtifactContext
for _, a := range manifest.Build.Artifacts {
if a.Context == nil || a.Context.Repository == "" {
continue
}
key := a.Context.Repository + "#" + a.Context.Ref
if seen[key] {
continue
}
seen[key] = true
out = append(out, *a.Context)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Repository != out[j].Repository {
return out[i].Repository < out[j].Repository
}
return out[i].Ref < out[j].Ref
})
return out
}
// readBy is every repository other than the module's own that this build's recipes read source from: the rule
// build-agent and the controller share (internal/buildreads).
func readBy(manifest catalogue.Manifest) []catalogue.ArtifactContext { return buildreads.Of(manifest) }
// binaryName is what a compiled bundle's executable is called: what the artifact says, or the name of
// the package it is built from, which is what a compiler would have chosen anyway.
+3
View File
@@ -234,6 +234,9 @@ func spoken() string {
}
// Languages is every language the mesh can build, for anything that wants to say so.
// AgentModule is build-agent's own module: the machine's builder, whose image is built from another repository's context.
const AgentModule = "build-agent"
func Languages() []string {
names := make([]string, 0, len(toolchains))
for _, t := range toolchains {
+311
View File
@@ -0,0 +1,311 @@
// Package builderkeys is how the controller reads what the builder and the indexer wrote (novox/hq ADR 0501, ADR 0498
// §8, ADR 0453 §5): a module's build at a fingerprint, in the builder's bucket; the indexer's trunk value of a module,
// which names that fingerprint; and a manifest by its digest. From R4e the controller asks no build but the builder's
// own module's: it takes every other module from the builder's done key, and it reads them here.
//
// It only reads. The builder writes its keys and the indexer its values; the controller's account reaches every
// bucket through the JetStream API it is granted (internal/broker: KindController).
package builderkeys
import (
"context"
"errors"
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
)
// Key is a module's build at a fingerprint as the builder's bucket holds it. Found is false when there is no key (or
// no bucket yet): nothing built or queued that fingerprint. Revision is the key's revision, a new one for every
// rebuild of the same fingerprint (ADR 0429 §18).
type Key struct {
Module string
Fingerprint string
Found bool
Revision uint64
Build delivery.BuilderBuild
}
// Done says the key is done.
func (k Key) Done() bool { return k.Found && k.Build.State == delivery.BuildDone }
// Failed says the key failed; its Error is the builder's own words (`digests differ:`, `not stored:`, …).
func (k Key) Failed() bool { return k.Found && k.Build.State == delivery.BuildFailed }
// Unreferenced are the artifacts of a done key that name no reference (ADR 0498 §8): a key done before R4e's
// delivery, or a candidate's image or package not yet built again for the trunk. Such a key is waited for, never
// delivered from.
func (k Key) Unreferenced() []string {
var out []string
for _, a := range k.Build.Artifacts {
if strings.TrimSpace(a.Reference) == "" {
out = append(out, a.Name)
}
}
return out
}
// Says is the key's state in words: what a plan waiting on it says.
func (k Key) Says() string {
if !k.Found {
return fmt.Sprintf("no build of %s at %s yet", k.Module, short(k.Fingerprint))
}
switch k.Build.State {
case delivery.BuildFailed:
return fmt.Sprintf("the build of %s at %s failed: %s", k.Module, short(k.Fingerprint), firstLine(k.Build.Error))
case delivery.BuildDone:
if missing := k.Unreferenced(); len(missing) > 0 {
return fmt.Sprintf("the build of %s at %s is done and waits for the reference of %s", k.Module,
short(k.Fingerprint), strings.Join(missing, ", "))
}
return fmt.Sprintf("the build of %s at %s is done", k.Module, short(k.Fingerprint))
}
if k.Build.By != "" {
return fmt.Sprintf("the build of %s at %s is %s on %s", k.Module, short(k.Fingerprint), k.Build.State, k.Build.By)
}
return fmt.Sprintf("the build of %s at %s is %s", k.Module, short(k.Fingerprint), k.Build.State)
}
// ErrNoValue is a module the indexer holds no value of at the commit asked: not written yet, or older than the
// bucket's history.
var ErrNoValue = errors.New("the indexer holds no value of the module at that commit")
// Reader reads the builder's and the indexer's buckets.
type Reader struct {
JS jetstream.JetStream
}
// bucket is the named bucket, or nil when it is not there yet (the part that writes it not delivered).
func (r Reader) bucket(ctx context.Context, name string) (jetstream.KeyValue, error) {
kv, err := r.JS.KeyValue(ctx, name)
if errors.Is(err, jetstream.ErrBucketNotFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("the bucket %s: %w", name, err)
}
return kv, nil
}
// Build is the builder's key of module at fingerprint fp (`src2:` and 64 hex digits). A missing key, or a missing
// bucket, is a Key not Found and no error; a value that does not decode is an error naming the key.
func (r Reader) Build(ctx context.Context, module, fp string) (Key, error) {
out := Key{Module: module, Fingerprint: fp}
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil {
return out, err
}
kv, err := r.bucket(ctx, delivery.BuilderBuildsBucket)
if err != nil || kv == nil {
return out, err
}
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return out, nil
}
if err != nil {
return out, fmt.Errorf("the builder's key %s: %w", key, err)
}
b, err := delivery.DecodeBuilderBuild(e.Value())
if err != nil {
return out, fmt.Errorf("the builder's key %s is not read: %w", key, err)
}
out.Found, out.Revision, out.Build = true, e.Revision(), b
return out, nil
}
// ModuleAt is the indexer's trunk value of module written for commit (ADR 0501 §2): the newest revision of
// modules.<module> whose Commit is commit, read from the key's history; with commit empty, the value it holds now.
// None held is ErrNoValue.
func (r Reader) ModuleAt(ctx context.Context, module, commit string) (delivery.Module, error) {
key, err := delivery.ModulesKey(module)
if err != nil {
return delivery.Module{}, err
}
kv, err := r.bucket(ctx, delivery.ModulesBucket)
if err != nil {
return delivery.Module{}, err
}
if kv == nil {
return delivery.Module{}, fmt.Errorf("%w (%s: the indexer's bucket is not there)", ErrNoValue, module)
}
var values [][]byte
if commit == "" {
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return delivery.Module{}, fmt.Errorf("%w (%s)", ErrNoValue, module)
}
if err != nil {
return delivery.Module{}, fmt.Errorf("the indexer's value of %s: %w", module, err)
}
values = [][]byte{e.Value()}
} else {
entries, err := kv.History(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return delivery.Module{}, fmt.Errorf("%w (%s at %s)", ErrNoValue, module, short(commit))
}
if err != nil {
return delivery.Module{}, fmt.Errorf("the indexer's values of %s: %w", module, err)
}
for _, e := range entries {
if e.Operation() == jetstream.KeyValuePut {
values = append(values, e.Value())
}
}
}
return pickAt(module, commit, values)
}
// Modules is every module the indexer holds a trunk value of, by module: the value it holds now. A value that does not
// decode is left out and said in unread.
func (r Reader) Modules(ctx context.Context) (values map[string]delivery.Module, unread map[string]error, err error) {
kv, err := r.bucket(ctx, delivery.ModulesBucket)
if err != nil || kv == nil {
return nil, nil, err
}
keys, err := kv.Keys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil, nil
}
if err != nil {
return nil, nil, fmt.Errorf("listing the indexer's modules: %w", err)
}
values, unread = map[string]delivery.Module{}, map[string]error{}
for _, key := range keys {
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
continue
}
if err != nil {
return nil, nil, fmt.Errorf("the indexer's value of %s: %w", key, err)
}
m, err := delivery.DecodeModule(e.Value())
if err != nil {
unread[key] = err
continue
}
values[key] = m
}
return values, unread, nil
}
// pickAt is, of a key's values oldest first, the newest whose commit is commit (any, with commit empty); values that
// do not decode are passed over, and one of them is said when nothing else is found.
func pickAt(module, commit string, values [][]byte) (delivery.Module, error) {
var unread error
for i := len(values) - 1; i >= 0; i-- {
m, err := delivery.DecodeModule(values[i])
if err != nil {
if unread == nil {
unread = err
}
continue
}
if commit == "" || sameCommit(m.Commit, commit) {
return m, nil
}
}
where := module
if commit != "" {
where += " at " + short(commit)
}
if unread != nil {
return delivery.Module{}, fmt.Errorf("%w (%s; a value is not read: %v)", ErrNoValue, where, unread)
}
return delivery.Module{}, fmt.Errorf("%w (%s)", ErrNoValue, where)
}
// sameCommit is two commit ids naming one commit: equal, or one a prefix of the other of at least 7 hex digits.
func sameCommit(a, b string) bool {
if a == b {
return a != ""
}
if len(a) < 7 || len(b) < 7 {
return false
}
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
}
// Manifest is the indexer's manifest of digest: the module and its module.json byte for byte.
func (r Reader) Manifest(ctx context.Context, digest string) (delivery.Manifest, error) {
key, err := delivery.ManifestsKey(digest)
if err != nil {
return delivery.Manifest{}, err
}
kv, err := r.bucket(ctx, delivery.ManifestsBucket)
if err != nil {
return delivery.Manifest{}, err
}
if kv == nil {
return delivery.Manifest{}, fmt.Errorf("the manifest %s: the indexer's manifests bucket is not there", short(digest))
}
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return delivery.Manifest{}, fmt.Errorf("the indexer holds no manifest %s", short(digest))
}
if err != nil {
return delivery.Manifest{}, fmt.Errorf("the manifest %s: %w", short(digest), err)
}
m, err := delivery.DecodeManifest(e.Value())
if err != nil {
return delivery.Manifest{}, fmt.Errorf("the manifest %s is not read: %w", short(digest), err)
}
return m, nil
}
// Watch calls wake with the module of every key the builder or the indexer writes from now on, until ctx ends: what
// wakes a plan waiting on a key, with no timer (ADR 0501; the R4e context's A7). A bucket not there yet is watched once
// it is: the watch looks again each time wake would have been called for the other bucket, and at every retry.
func Watch(ctx context.Context, js jetstream.JetStream, wake func(module string)) error {
for _, name := range []string{delivery.BuilderBuildsBucket, delivery.ModulesBucket} {
if err := watchOne(ctx, js, name, wake); err != nil {
return err
}
}
return nil
}
func watchOne(ctx context.Context, js jetstream.JetStream, name string, wake func(module string)) error {
kv, err := js.KeyValue(ctx, name)
if err != nil {
return fmt.Errorf("the bucket %s: %w", name, err)
}
w, err := kv.WatchAll(ctx, jetstream.UpdatesOnly())
if err != nil {
return fmt.Errorf("watching %s: %w", name, err)
}
go func() {
defer w.Stop()
for {
select {
case <-ctx.Done():
return
case e, ok := <-w.Updates():
if !ok {
return
}
if e == nil || e.Operation() != jetstream.KeyValuePut {
continue
}
module, _, _ := strings.Cut(e.Key(), ".")
wake(module)
}
}
}()
return nil
}
func short(s string) string {
s = strings.TrimPrefix(s, "src2:")
if len(s) > 12 {
return s[:12]
}
return s
}
func firstLine(s string) string {
line, _, _ := strings.Cut(strings.TrimSpace(s), "\n")
return line
}
@@ -0,0 +1,143 @@
//go:build integration
package builderkeys
import (
"context"
"errors"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/testbus"
)
func aBus(t *testing.T) jetstream.JetStream {
t.Helper()
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
js, err := jetstream.New(conn)
if err != nil {
t.Fatal(err)
}
return js
}
func put(t *testing.T, kv jetstream.KeyValue, key string, value []byte) {
t.Helper()
if _, err := kv.Put(context.Background(), key, value); err != nil {
t.Fatal(err)
}
}
// The controller reads the builder's keys, the indexer's values by commit and its manifests (A1, A2), and a key the
// builder writes wakes it with the key's module (A1's watch, no timer).
func TestTheControllerReadsTheBuildersKeysAndTheIndexersValues(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
js := aBus(t)
r := Reader{JS: js}
// Nothing there yet: no bucket is no key and no error; no value is ErrNoValue.
if k, err := r.Build(ctx, "x", fp); err != nil || k.Found {
t.Fatalf("no bucket: %+v %v", k, err)
}
if _, err := r.ModuleAt(ctx, "x", "abcdef1"); !errors.Is(err, ErrNoValue) {
t.Fatalf("no bucket: %v", err)
}
builds, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.BuilderBuildsBucket,
History: delivery.BuilderBuildsHistory})
if err != nil {
t.Fatal(err)
}
modules, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ModulesBucket,
History: delivery.ModulesHistory})
if err != nil {
t.Fatal(err)
}
manifests, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ManifestsBucket})
if err != nil {
t.Fatal(err)
}
woke := make(chan string, 8)
if err := Watch(ctx, js, func(m string) { woke <- m }); err != nil {
t.Fatal(err)
}
raw := `{"module":"x","version":"1"}`
digest := delivery.ManifestDigest([]byte(raw))
value, err := delivery.EncodeManifest(delivery.Manifest{Module: "x", JSON: raw})
if err != nil {
t.Fatal(err)
}
put(t, manifests, digest, value)
key, _ := delivery.BuilderBuildKey("x", fp)
building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace"})
put(t, builds, key, building)
select {
case m := <-woke:
if m != "x" {
t.Fatalf("woken for %q", m)
}
case <-ctx.Done():
t.Fatal("the builder's write woke nothing")
}
done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest,
Artifacts: []delivery.BuiltArtifact{{Name: "a", Kind: "bundle", Digest: "sha256:1",
Reference: "artifact-store://x/a/blobs/sha256:1"}}})
put(t, builds, key, done)
k, err := r.Build(ctx, "x", fp)
if err != nil || !k.Done() || len(k.Unreferenced()) != 0 || k.Build.Artifacts[0].Reference == "" {
t.Fatalf("the done key: %+v %v", k, err)
}
m, err := r.Manifest(ctx, k.Build.Manifest)
if err != nil || m.JSON != raw {
t.Fatalf("the manifest: %+v %v", m, err)
}
other := "src2:" + strings.Repeat("e", 64)
for _, v := range []struct{ commit, fp2 string }{{"1111111111", other}, {"2222222222", fp}, {"3333333333", other}} {
raw, err := delivery.EncodeModule(delivery.Module{FP: "src1:" + strings.Repeat("a", 64), FP2: v.fp2,
Manifest: digest, Repository: "novox/r", Commit: v.commit})
if err != nil {
t.Fatal(err)
}
put(t, modules, "x", raw)
}
at, err := r.ModuleAt(ctx, "x", "2222222222")
if err != nil || at.FP2 != fp {
t.Fatalf("FP2 at the plan's commit, overwritten by a later merge: %+v %v", at, err)
}
if now, err := r.ModuleAt(ctx, "x", ""); err != nil || now.Commit != "3333333333" {
t.Fatalf("the value now: %+v %v", now, err)
}
if _, err := r.ModuleAt(ctx, "x", "9999999999"); !errors.Is(err, ErrNoValue) {
t.Fatalf("a commit the indexer never wrote: %v", err)
}
put(t, modules, "broken", []byte("{not json"))
all, unread, err := r.Modules(ctx)
if err != nil || len(all) != 1 || all["x"].Commit != "3333333333" || unread["broken"] == nil {
t.Fatalf("every module's value now: %+v %v %v", all, unread, err)
}
}
// The controller's account reaches the builder's and the indexer's buckets: it is granted the JetStream API whole.
func TestTheControllersAccountReachesTheBuckets(t *testing.T) {
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(perms.Publish, "$JS.API.>") || !slices.Contains(perms.Subscribe, "$JS.API.>") {
t.Fatalf("the controller is not granted the JetStream API: %v / %v", perms.Publish, perms.Subscribe)
}
}
+86
View File
@@ -0,0 +1,86 @@
package builderkeys
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const fp = "src2:" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
func moduleValue(t *testing.T, commit, fp2 string) []byte {
t.Helper()
raw, err := delivery.EncodeModule(delivery.Module{FP: "src1:" + strings.Repeat("a", 64), FP2: fp2,
Manifest: strings.Repeat("b", 64), Repository: "novox/mesh-catalog", Commit: commit})
if err != nil {
t.Fatal(err)
}
return raw
}
// A plan's module is the indexer's value written for the merge the plan carries, not a later merge's (ADR 0501 §2).
func TestPickAtIsTheNewestValueOfTheCommit(t *testing.T) {
other := "src2:" + strings.Repeat("c", 64)
values := [][]byte{
moduleValue(t, "1111111111", other),
moduleValue(t, "2222222222", other),
moduleValue(t, "2222222222", fp), // the same merge, computed again when a toolchain appeared
moduleValue(t, "3333333333", other),
}
m, err := pickAt("x", "2222222222", values)
if err != nil {
t.Fatal(err)
}
if m.FP2 != fp {
t.Fatalf("FP2 at 2222222222 is %s, want the newest of that commit %s", m.FP2, fp)
}
// An abbreviated commit names the same one.
if m, err := pickAt("x", "2222222", values); err != nil || m.FP2 != fp {
t.Fatalf("an abbreviated commit: %v %v", m.FP2, err)
}
// No commit is the newest value.
if m, err := pickAt("x", "", values); err != nil || m.Commit != "3333333333" {
t.Fatalf("the newest: %v %v", m.Commit, err)
}
}
func TestPickAtSaysNoValueOfACommitNotWritten(t *testing.T) {
_, err := pickAt("x", "4444444444", [][]byte{moduleValue(t, "1111111111", fp), []byte("{not json")})
if !errors.Is(err, ErrNoValue) {
t.Fatalf("want ErrNoValue, got %v", err)
}
if !strings.Contains(err.Error(), "x at 4444444444") || !strings.Contains(err.Error(), "not read") {
t.Fatalf("the words name neither the module and commit nor the unread value: %v", err)
}
}
// A done key with references is delivered from; a done key with an artifact without one is waited for, said
// (ADR 0498 §8); a failed key says the builder's own words.
func TestAKeySaysWhatItIs(t *testing.T) {
missing := Key{Module: "x", Fingerprint: fp}
if missing.Done() || missing.Failed() || !strings.Contains(missing.Says(), "no build of x") {
t.Fatalf("a missing key: %q", missing.Says())
}
done := Key{Module: "x", Fingerprint: fp, Found: true, Build: delivery.BuilderBuild{State: delivery.BuildDone,
Artifacts: []delivery.BuiltArtifact{
{Name: "a", Digest: "sha256:1", Reference: "artifact-store://x/a/blobs/sha256:1"},
{Name: "img", Kind: "image", Digest: "sha256:2"},
}}}
if !done.Done() || len(done.Unreferenced()) != 1 || done.Unreferenced()[0] != "img" {
t.Fatalf("unreferenced: %v", done.Unreferenced())
}
if !strings.Contains(done.Says(), "waits for the reference of img") {
t.Fatalf("a done key without a reference: %q", done.Says())
}
failed := Key{Module: "x", Fingerprint: fp, Found: true, Build: delivery.BuilderBuild{State: delivery.BuildFailed,
Error: "digests differ: ace sha256:1, g14 sha256:2\nmore"}}
if !failed.Failed() || !strings.HasSuffix(failed.Says(), "failed: digests differ: ace sha256:1, g14 sha256:2") {
t.Fatalf("a failed key: %q", failed.Says())
}
building := Key{Module: "x", Fingerprint: fp, Found: true, Build: delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace"}}
if building.Done() || !strings.Contains(building.Says(), "building on ace") {
t.Fatalf("a building key: %q", building.Says())
}
}
+39
View File
@@ -0,0 +1,39 @@
// Package buildreads is which repositories other than a module's own its build reads source from, as its manifest
// names them (novox/hq 04-ISSUES/131): the artifacts' `context` repositories. One rule for build-agent, which says it
// in a build's outcome, and for the controller, which reads it from a done key's manifest (R4e, issue 1851): it lives
// here, not in internal/builder, so it outlasts build-agent (R4g).
package buildreads
import (
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Of is every repository other than the module's own that this build's recipes read source from, each once and in a
// fixed order, so two builds of one commit report the same thing the same way.
func Of(manifest catalogue.Manifest) []catalogue.ArtifactContext {
if manifest.Build == nil {
return nil
}
seen := map[string]bool{}
var out []catalogue.ArtifactContext
for _, a := range manifest.Build.Artifacts {
if a.Context == nil || a.Context.Repository == "" {
continue
}
key := a.Context.Repository + "#" + a.Context.Ref
if seen[key] {
continue
}
seen[key] = true
out = append(out, *a.Context)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Repository != out[j].Repository {
return out[i].Repository < out[j].Repository
}
return out[i].Ref < out[j].Ref
})
return out
}
+28
View File
@@ -3,6 +3,7 @@ package inventory
import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/announce"
"slices"
"strings"
@@ -16,6 +17,10 @@ import (
// apart for the reason that package keeps its own types: a permission must be a function of what a
// module declared, and a query that decided anything would be a second place authority came from.
// Announcements reads the last announcement of each module (novox/hq ADR 0495, issue 1790), set by the controller that
// reaches the bus; nil, nothing is read and every read is granted from `reads`.
var Announcements func(ctx context.Context) (map[string]announce.States, error)
// BusRecords is every fact the composer needs about who may reach the bus.
//
// **A module's authority comes from the manifest, not from the assignment.** The assignment says
@@ -110,6 +115,29 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
}
}
// **Who may read a state is what its owner announced** (novox/hq ADR 0495 clause 3, issue 1790): a read the owner's
// announcement does not make readable is granted nothing, and said; an owner that announced nothing yet is read as
// before. Announcements that cannot be read are said, and every read is granted as before: the bus's users are
// composed either way.
if Announcements != nil {
announced, err := Announcements(ctx)
if err != nil {
fmt.Printf("the states the modules announced cannot be read, so every read is granted from `reads` as before: %v\n", err)
} else {
out.Announced = announced
for node, ds := range out.Assigned {
for k := range ds {
granted, refused := broker.ReadsAnnounced(ds[k].Reads, announced)
ds[k].Reads, ds[k].ReadsRefused = granted, refused
for _, r := range refused {
fmt.Printf("%s on %s reads %s, which its owner does not announce readable: not granted\n",
ds[k].Module, node, r)
}
}
}
}
}
enrolling, err := i.NodesWithALiveToken(ctx)
if err != nil {
return broker.Records{}, err
+9
View File
@@ -388,6 +388,15 @@ type PlanModule struct {
// data, or "waits: …" with the recreate words, the data change named, the person's or the bus's hold, or why it
// could not be judged.
Recorded map[string]string `json:"recorded,omitempty"`
// Key says the walk takes this module from the builder's done key and asked no build (novox/hq ADR 0501, ADR 0453
// §5; issue 1851): from R4e every module but the builder's own. Its outcome is the key's, never a build-agent outcome.
Key bool `json:"key,omitempty"`
// KeyAt is the commit of its repository whose indexer's trunk value names the fingerprint taken (ADR 0501 §2); empty
// for a module of a repository the walk carries no commit of (a dependent through a base), taken at the value now.
KeyAt string `json:"key_at,omitempty"`
// Fingerprint is the fingerprint of the new form (`src2:`) the walk takes the module at, read from that trunk value;
// empty until the indexer has written it.
Fingerprint string `json:"fingerprint,omitempty"`
}
// PlanGate is one module's rollout record at its gate (to-be 45 §8): the component, the first machine,
-45
View File
@@ -230,48 +230,3 @@ func TestASeatsVerbGainsTheArgumentsTheBinaryNames(t *testing.T) {
}
}
}
// **A stored row's build verb takes on and behind** (novox/hq ADR 0246: arguments added to a verb are
// additive): a row seeded before them described build with a repository it required, so the console
// would refuse both and a call naming neither would be refused for want of a repository. Re-seeding
// gives the row this binary's build.
func TestAStoredRowsBuildVerbTakesOnAndBehind(t *testing.T) {
inv := ForTest(t)
ctx := t.Context()
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
}
old := `[{"name":"build","description":"Have the build machine build a repository.","input":{"type":"object",
"properties":{"repository":{"type":"string"},"path":{"type":"string"},"ref":{"type":"string"}},
"required":["repository"]}}]`
if _, err := inv.store.Pool().Exec(ctx, `update seat set serves = $1 where name = $2`,
[]byte(old), catalogue.ControllerSeatName); err != nil {
t.Fatal(err)
}
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
}
seats, err := inv.Seats(ctx)
if err != nil {
t.Fatal(err)
}
var build catalogue.Verb
for _, s := range seats {
if s.Name == catalogue.ControllerSeatName {
for _, v := range s.Serves {
if v.Name == "build" {
build = v
}
}
}
}
props, _ := build.Input["properties"].(map[string]any)
for _, arg := range []string{"repository", "path", "ref", "on", "behind"} {
if _, has := props[arg]; !has {
t.Errorf("build in the row does not take %s: %v", arg, props)
}
}
if req, _ := build.Input["required"].([]any); len(req) != 0 {
t.Errorf("build in the row still requires %v, so on and behind alone are refused", req)
}
}
+47 -1
View File
@@ -2,6 +2,8 @@ package inventory
import (
"context"
"encoding/json"
"strings"
"github.com/novox/mesh-controller/internal/chain"
)
@@ -43,6 +45,11 @@ func (i *Inventory) sourceRows(ctx context.Context, module string) (map[string][
// standing is, of a module's builds newest first, the index of the build that stands for the build at
// `from`: the oldest of the unbroken run of builds behind it with its source fingerprint.
//
// **Across the switch to the new form** (novox/hq ADR 0453 §6, ADR 0499; R4e A9): a build of the new form (`src2:`, taken
// from the builder's key) continues the run of the build before it of the old form (`src1:` or none, build-agent's)
// when every artifact of the two has the same digest: the fingerprints cannot be compared, the programs can, and equal
// digests are no move. Past it the run goes on by the old fingerprint.
func standing(builds []sourceRow, from int) int {
at := from
fp := builds[from].fingerprint
@@ -50,14 +57,53 @@ func standing(builds []sourceRow, from int) int {
return at
}
for j := from + 1; j < len(builds); j++ {
if builds[j].fingerprint != fp || builds[j].failedGate {
if builds[j].failedGate {
break
}
if builds[j].fingerprint != fp {
if !newForm(fp) || newForm(builds[j].fingerprint) || !sameDigests(builds[at].made, builds[j].made) {
break
}
fp = builds[j].fingerprint
}
at = j
}
return at
}
// newForm says a source fingerprint is of the new form (ADR 0453 §6).
func newForm(fp string) bool { return strings.HasPrefix(fp, "src2:") }
// sameDigests says two builds' artifacts, as recorded (JSON of []Artifact), are the same programs: the same names, each
// with the same digest, wherever each was pushed. A build naming an artifact with no digest is never the same.
func sameDigests(a, b []byte) bool {
digests := func(raw []byte) map[string]string {
var made []Artifact
if json.Unmarshal(raw, &made) != nil || len(made) == 0 {
return nil
}
out := map[string]string{}
for _, m := range made {
d := digestIn(m.Reference)
if d == "" {
return nil
}
out[m.Name] = d
}
return out
}
da, db := digests(a), digests(b)
if da == nil || db == nil || len(da) != len(db) {
return false
}
for name, d := range da {
if db[name] != d {
return false
}
}
return true
}
// StandingBuild is the build that stands for a module's build (novox/hq issue 280): the oldest build of
// the unbroken run of builds with its source fingerprint, at or before it — itself when its fingerprint
// is empty or it starts the run. Answers its id and the manifest it was recorded with; empty when the
@@ -0,0 +1,49 @@
package inventory
import (
"encoding/json"
"testing"
)
func madeOf(t *testing.T, refs map[string]string) []byte {
t.Helper()
var made []Artifact
for name, ref := range refs {
made = append(made, Artifact{Name: name, Kind: "bundle", Reference: ref})
}
raw, err := json.Marshal(made)
if err != nil {
t.Fatal(err)
}
return raw
}
// R4e A9 (ADR 0453 §6, ADR 0499): at the switch, a build taken from the builder's key (`src2:`) whose every artifact has
// the digest of the build-agent build before it (`src1:`) is no move: it stands on that build, wherever each was pushed.
// One digest that differs is a move, as is a build of the old form after one of the new.
func TestAKeysBuildWithTheOldBuildsDigestsIsNoMove(t *testing.T) {
const d1, d2 = "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"sha256:2222222222222222222222222222222222222222222222222222222222222222"
old := sourceRow{id: "build-1", fingerprint: "src1:aa", made: madeOf(t, map[string]string{
"tools": "artifact-store://x/tools/blobs/" + d1, "img": "artifact-store://x/img@" + d2})}
key := sourceRow{id: "key-bb-1", fingerprint: "src2:bb", made: madeOf(t, map[string]string{
"tools": "artifact-store://x/tools/blobs/" + d1, "img": "registry.internal:5000/x/img@" + d2})}
if at := standing([]sourceRow{key, old}, 0); at != 1 {
t.Fatalf("the key's build with the same digests stands on %d, want the old build", at)
}
moved := key
moved.made = madeOf(t, map[string]string{"tools": "artifact-store://x/tools/blobs/" + d2, "img": "x/img@" + d2})
if at := standing([]sourceRow{moved, old}, 0); at != 0 {
t.Fatalf("a key's build with another digest stands on %d, want itself (a move)", at)
}
// Two key builds of one fingerprint are one run, as two builds of one fingerprint always were.
again := key
again.id = "key-bb-2"
if at := standing([]sourceRow{again, key, old}, 0); at != 2 {
t.Fatalf("a run of one new fingerprint on an old build of the same digests stands on %d", at)
}
// An old build after a new one never continues the new one's run.
if at := standing([]sourceRow{old, key}, 0); at != 0 {
t.Fatalf("an old build continues a new one's run: %d", at)
}
}
+8 -3
View File
@@ -28,6 +28,9 @@ var mayNameAMirror = []string{
// The grant of a machine's own keys of the assignments mirror to its provisioner client (design 57 P3).
"internal/broker/provisioner.go",
"internal/broker/writers.go",
// The controller's announcement of what it keeps, the mirrors readable (novox/hq ADR 0495, issue 1790): it names them
// to announce them, never reads them.
"internal/broker/announced.go",
}
// The packages whose names stand for a mirror, by import path, and the names in each.
@@ -38,7 +41,7 @@ const (
var mirrorNames = map[string][]string{
brokerPath: {"AssignmentsBucket", "DeclaredConfigBucket", "MirrorAssignments", "MirrorDeclaredConfig",
"ControllerMirrors"},
"controllerReadable"},
recordmirrorPath: {"Assignments", "DeclaredConfig", "Mirrors"},
}
@@ -54,8 +57,10 @@ var mirrorNames = map[string][]string{
// allowed file. Those are deliberate evasions rather than ordinary code; a reviewer judges them.
func TestOnlyTheAuditAndAMirrorsWriterReadAMirror(t *testing.T) {
var buckets []string
for _, m := range broker.ControllerMirrors() {
buckets = append(buckets, broker.BucketName(broker.ControllerSeat, m))
for _, s := range broker.ControllerAnnouncement("").States {
if s.Readable {
buckets = append(buckets, broker.BucketName(broker.ControllerSeat, s.Name))
}
}
root := filepath.Join("..", "..")
found := mirrorReaders(t, root, buckets)