diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go index d5a1335d..75346e29 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go @@ -17,8 +17,10 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" + "git.novox.be/novox/mesh-delivery/sdk/trunk" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" @@ -26,6 +28,9 @@ import ( "mesh-delivery-forge-bridge/internal/groups" ) +// gateMergesState is the gate's state merges, which the tools read (reads: mesh-delivery-gate.merges in the manifest). +const gateMergesState = "mesh-delivery-gate.merges" + // Tool names. const ( GroupsTool = "bridge_groups" @@ -125,18 +130,18 @@ func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Tre return nil, fmt.Errorf("%q is not owner/name", repository) } for _, key := range slices.Sorted(maps.Keys(raws)) { - m, err := delivery.DecodeMerge(raws[key]) + m, err := meshdeliveryclient.DecodeMerge(raws[key]) if err != nil { continue } - at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { - return delivery.SameRepository(p.Repository, repository) && p.Commit == merge + at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { + return trunk.SameRepository(p.Repository, repository) && p.Commit == merge }) if at < 0 { continue } p := m.Pulls[at] - msg, err := bridge.MergedMessage(owner, repo, p.Number, m.Group, p.Head.Head, merge, trees, files) + msg, err := bridge.MergedMessage(owner, repo, int(p.Number), m.Group, p.Head, merge, trees, files) if err != nil { return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err) } @@ -144,7 +149,7 @@ func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Tre if err != nil { return nil, err } - if _, approved := m.Approved[delivery.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 { + if _, approved := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 { f.Group, f.Members = m.Group, groups.MembersOf(m) } out, err := forgemsg.EncodeForge(f) @@ -253,7 +258,7 @@ func askCaller(_ context.Context, key string, body any) (json.RawMessage, error) } func main() { - merges, performed, asks := stdio.State(delivery.GateModule+".merges"), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState) + merges, performed, asks := stdio.State(gateMergesState), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState) tools := Tools(askEnd(asks, 25*time.Second), time.Now, func() ([]groups.Row, []string, error) { return read(merges, performed, asks) }, func(repository, merge string) (json.RawMessage, error) { diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go index 46f3af72..3bcd2e10 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-forge-bridge/internal/bridge" @@ -22,19 +23,19 @@ import ( func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40) c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40) - enc := func(group string, state2 delivery.MergeState, c string) []byte { - v, err := delivery.EncodeMerge(delivery.Merge{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2}, - Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 1, Head: h1}, State: delivery.MergeMerged, Commit: c1}, - {Head: delivery.Head{Repository: "novox/b", Number: 2, Head: h2}, State: state2, Commit: c}}}) + enc := func(group string, state2 meshdeliveryclient.MergeState, c string) []byte { + v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2}, + Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 1, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}, + {Repository: "novox/b", Number: 2, Head: h2, State: string(state2), Commit: c}}}) if err != nil { t.Fatal(err) } return v } - lone, _ := delivery.EncodeMerge(delivery.Merge{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1}, - Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 5, Head: h1}, State: delivery.MergeMerged, Commit: c1}}}) - merges := map[string][]byte{"ended": enc("ended", delivery.MergeMerged, c2), "ready": enc("ready", delivery.MergeMerged, c2), - "gathered": enc("gathered", delivery.MergeMerging, ""), "solo": lone, "bad": []byte("{")} + lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1}, + Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}}) + merges := map[string][]byte{"ended": enc("ended", meshdeliveryclient.MergeMerged, c2), "ready": enc("ready", meshdeliveryclient.MergeMerged, c2), + "gathered": enc("gathered", meshdeliveryclient.MergeMerging, ""), "solo": lone, "bad": []byte("{")} performed := map[string]bool{} for _, c := range []string{c1, c2} { performed[groups.PublishedKey("novox/a", 1, c)], performed[groups.PublishedKey("novox/b", 2, c)] = true, true @@ -98,7 +99,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { if a := all["ready"].LastEndAsk; a == nil || a.By != "lead@shanks" || a.Why != "lost" || !a.Written || a.Answer == "" || all["ended"].LastEndAsk != nil { t.Errorf("ready's last end ask = %+v, ended's %+v: want ready's ask and its answer, none for ended", a, all["ended"].LastEndAsk) } - if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != string(delivery.MergeMerging) { + if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != string(meshdeliveryclient.MergeMerging) { t.Errorf("gathered = %+v", r) } if open := rowsOf(map[string]any{"open": true}); len(open) != 2 || open["ended"].Group != "" { @@ -146,11 +147,11 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { } return f } - lone, _ := delivery.EncodeMerge(delivery.Merge{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1}, - Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 5, Head: h1}, State: delivery.MergeMerged, Commit: c1}}}) - group, _ := delivery.EncodeMerge(delivery.Merge{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2}, - Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 6, Head: h1}, State: delivery.MergeMerged, Commit: c1}, - {Head: delivery.Head{Repository: "novox/b", Number: 7, Head: h2}, State: delivery.MergeMerging}}}) + lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1}, + Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}}) + group, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2}, + Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 6, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}, + {Repository: "novox/b", Number: 7, Head: h2, State: string(meshdeliveryclient.MergeMerging)}}}) for _, c := range []struct { name string raws map[string][]byte @@ -162,7 +163,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { {name: "a lone pull request", raws: map[string][]byte{"solo": lone}, repository: "novox/a", merge: c1, want: published("a", 5, h1, "solo", c1)}, {name: "a member of a group", raws: map[string][]byte{"feat~g": group}, repository: "novox/a", merge: c1, want: func() forgemsg.Forge { f := published("a", 6, h1, "feat/g", c1) - f.Group, f.Members = "feat/g", []delivery.Head{{Repository: "novox/a", Number: 6, Head: h1}, {Repository: "novox/b", Number: 7, Head: h2}} + f.Group, f.Members = "feat/g", []meshdeliveryclient.Head{{Repository: "novox/a", Number: 6, Head: h1}, {Repository: "novox/b", Number: 7, Head: h2}} return f }()}, {name: "a merge the gate does not hold", raws: map[string][]byte{"solo": lone, "bad": []byte("{")}, repository: "novox/a", merge: c2, fail: "hold no merge"}, diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate.go index 78ac3613..a005d465 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate.go @@ -9,15 +9,13 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" - "mesh-delivery-forge-bridge/internal/bridge" ) // performedBucket is the module's state bucket performed, `_` as heads is: for each key of the gate's // buckets the bridge has acted on, the revision it acted at (manifest state entry performed, novox/hq ADR 0437 §8). // Its keys are `owed.` and `merges.`; its value is the revision in decimal. -const performedBucket = delivery.ForgeBridgeModule + "_performed" +const performedBucket = forgeBridgeModule + "_performed" // kvRevisions is the bucket performed as the performer's revisions. type kvRevisions struct{ kv jetstream.KeyValue } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go index fae4fb07..7fced163 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go @@ -9,8 +9,9 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" ) const ( @@ -52,13 +53,13 @@ func newPerformer(f *forgeFake) (*performer, *fakePub, memRevisions, *bytes.Buff } // The values are the SDK's own encoding (mesh-sdk go/delivery EncodeOwed, EncodeMerge), the keys its OwedKey. -func owedKV(t *testing.T, repository string, number int, act string, o delivery.Owed) (string, []byte) { +func owedKV(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) { t.Helper() - key, err := delivery.OwedKey(repository, number, headA, act) + key, err := meshdeliveryclient.OwedKey(repository, number, headA, act) if err != nil { t.Fatal(err) } - v, err := delivery.EncodeOwed(o) + v, err := meshdeliveryclient.EncodeOwed(o) if err != nil { t.Fatal(err) } @@ -66,10 +67,11 @@ func owedKV(t *testing.T, repository string, number int, act string, o delivery. } func TestAnOwedStatusIsSetOnceForARevision(t *testing.T) { // 1341/AC1 + t.Run("the names are the manifest's", theNamesAreTheManifests) f := &forgeFake{} p, _, _, _ := newPerformer(f) key, v := owedKV(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate", - delivery.Owed{Act: delivery.OwedStatus, Context: "mesh/merge-gate", State: "success", Text: "all green", Target: "https://forge.example/p/12"}) + seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "success", Text: "all green", Target: "https://forge.example/p/12"}) p.owed(context.Background(), key, 7, v) if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_commit_status" || f.calls[0].body != `{"context":"mesh/merge-gate","description":"all green","owner":"novox","repo":"mesh-delivery","sha":"`+headA+`","state":"success","target_url":"https://forge.example/p/12"}` { @@ -90,11 +92,11 @@ func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2 p, _, _, _ := newPerformer(f) for _, c := range []struct { act string - o delivery.Owed + o seat.OwedByRepositoryNumberHeadActValue }{ - {"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}}, - {"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}}, - {"comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}}, + {"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"}}, + {"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"}}, + {"comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}}, } { key, v := owedKV(t, "novox/mesh-delivery", 12, c.act, c.o) p.owed(context.Background(), key, 1, v) @@ -112,7 +114,7 @@ func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2 func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC3 f := &forgeFake{refuse: map[string]string{"gitea.gitea_add_comment": "the pull request is locked"}} p, pub, _, _ := newPerformer(f) - key, v := owedKV(t, "novox/mesh-delivery", 12, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}) + key, v := owedKV(t, "novox/mesh-delivery", 12, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}) p.owed(context.Background(), key, 3, v) p.owed(context.Background(), key, 3, v) if len(f.calls) != 1 { @@ -147,28 +149,28 @@ func TestAnOwedValueThatCannotBeReadIsLoggedAndNotRetried(t *testing.T) { } } -func mergeKV(t *testing.T, m delivery.Merge) []byte { +func mergeKV(t *testing.T, m seat.MergesByGroupValue) []byte { t.Helper() - v, err := delivery.EncodeMerge(m) + v, err := meshdeliveryclient.EncodeMerge(m) if err != nil { t.Fatal(err) } return v } -func twoMembers(a, b delivery.MergeState) delivery.Merge { - return delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", - Approved: map[string]string{delivery.ApprovedKey("novox/mesh-sdk", 4): headB, delivery.ApprovedKey("novox/mesh-delivery", 12): headA}, - Pulls: []delivery.MergePull{ - {Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: a, Commit: map[bool]string{true: "c1"}[a == delivery.MergeMerged]}, - {Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: b, Commit: map[bool]string{true: "c2"}[b == delivery.MergeMerged]}, +func twoMembers(a, b meshdeliveryclient.MergeState) seat.MergesByGroupValue { + return seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", + Approved: map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA}, + Pulls: []seat.MergesByGroupValuePullsItem{ + {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(a), Commit: map[bool]string{true: "c1"}[a == meshdeliveryclient.MergeMerged]}, + {Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(b), Commit: map[bool]string{true: "c2"}[b == meshdeliveryclient.MergeMerged]}, }} } func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1 f := &forgeFake{} p, _, _, _ := newPerformer(f) - m := twoMembers(delivery.MergeMerging, delivery.MergeMerged) + m := twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged) p.merges(context.Background(), "g", 5, mergeKV(t, m)) want := `{"approved_heads":["novox/mesh-delivery@` + headA + `","novox/mesh-sdk@` + headB + `"],"head":"` + headA + `","number":12,"owner":"novox","repo":"mesh-delivery"}` if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_merge_pull_request" || f.calls[0].body != want { @@ -183,7 +185,7 @@ func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1 func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/AC2 f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "novox/mesh-delivery#12 is not merged: its head moved"}} p, pub, _, _ := newPerformer(f) - v := mergeKV(t, twoMembers(delivery.MergeMerging, delivery.MergeMerged)) + v := mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged)) p.merges(context.Background(), "g", 5, v) p.merges(context.Background(), "g", 5, v) if len(f.calls) != 1 || len(pub.data) != 1 { @@ -204,7 +206,7 @@ func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/ func TestAMergeWithTwoHandedStopsAtTheFirstRefusal(t *testing.T) { // ADR 0441 §3 f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "no"}} p, pub, _, _ := newPerformer(f) - p.merges(context.Background(), "g", 1, mergeKV(t, twoMembers(delivery.MergeMerging, delivery.MergeMerging))) + p.merges(context.Background(), "g", 1, mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerging))) if len(f.calls) != 1 || len(pub.data) != 1 { t.Errorf("calls %d messages %d, want the first refusal to stop the pass", len(f.calls), len(pub.data)) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go index c8026a15..c840f23a 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go @@ -8,8 +8,10 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/trunk" groupkeys "mesh-delivery-forge-bridge/internal/groups" ) @@ -57,29 +59,29 @@ func kvMerges(kv jetstream.KeyValue) mergesRead { // groupOf is the gate's merge of the group a merged message's branch names, when the message's pull request is one of // its approved members and the group has more than one: a lone pull request is no group. -func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (delivery.Merge, uint64, bool) { +func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (seat.MergesByGroupValue, uint64, bool) { if g == nil || g.merges == nil || f.Branch == "" { - return delivery.Merge{}, 0, false + return seat.MergesByGroupValue{}, 0, false } - key, err := delivery.MergesKey(f.Branch) + key, err := meshdeliveryclient.MergesKey(f.Branch) if err != nil { - return delivery.Merge{}, 0, false + return seat.MergesByGroupValue{}, 0, false } value, rev, found, err := g.merges(ctx, key) if err != nil { g.logger.Printf("%s#%d: the gate's merges.%s cannot be read, so its merged message names no group: %v", f.Repository, f.Number, key, err) - return delivery.Merge{}, 0, false + return seat.MergesByGroupValue{}, 0, false } if !found { - return delivery.Merge{}, 0, false + return seat.MergesByGroupValue{}, 0, false } - m, err := delivery.DecodeMerge(value) + m, err := meshdeliveryclient.DecodeMerge(value) if err != nil { g.logger.Printf("%s#%d: the gate's merges.%s is not a merge, so its merged message names no group: %v", f.Repository, f.Number, key, err) - return delivery.Merge{}, 0, false + return seat.MergesByGroupValue{}, 0, false } - if _, ok := m.Approved[delivery.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 { - return delivery.Merge{}, 0, false + if _, ok := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 { + return seat.MergesByGroupValue{}, 0, false } return m, rev, true } @@ -110,7 +112,7 @@ func (g *grouper) published(ctx context.Context, f forgemsg.Forge) { g.logger.Printf("%s#%d: that its merged message is written is not kept, so the group's end waits for the next act: %v", f.Repository, f.Number, err) return } - key, err := delivery.MergesKey(f.Branch) + key, err := meshdeliveryclient.MergesKey(f.Branch) if err != nil { return } @@ -130,7 +132,7 @@ func (g *grouper) ended(ctx context.Context, key string, rev uint64, value []byt } func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, again string) bool { - m, err := delivery.DecodeMerge(value) + m, err := meshdeliveryclient.DecodeMerge(value) if err != nil || len(m.Approved) < 2 { return false } @@ -141,8 +143,8 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, } end := forgemsg.ForgeGroupEnd{Group: m.Group} for _, member := range groupkeys.MembersOf(m) { - at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { - return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number + at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { + return trunk.SameRepository(p.Repository, member.Repository) && int(p.Number) == member.Number }) if at < 0 || m.Pulls[at].Commit == "" { return false // not every member is merged (a merge commit is written only for a merged pull): the group stays gathered @@ -154,8 +156,8 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, } // The members in the order the gate merged them, so the last commit names the end. slices.SortStableFunc(end.Members, func(a, b forgemsg.GroupMerge) int { - return slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == a.Merge }) - - slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == b.Merge }) + return slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == a.Merge }) - + slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == b.Merge }) }) data, err := forgemsg.EncodeForgeGroupEnd(end) if err != nil { diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go index a81c2c41..b4100d67 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" groupkeys "mesh-delivery-forge-bridge/internal/groups" ) @@ -29,19 +30,19 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 ctx := context.Background() h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40) c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40) - merge := func(states ...delivery.MergePull) []byte { - v, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/take-in", Ask: "ask", Pulls: states, + merge := func(states ...seat.MergesByGroupValuePullsItem) []byte { + v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Pulls: states, Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}}) if err != nil { t.Fatal(err) } return v } - sdk := func(state delivery.MergeState, commit, words string) delivery.MergePull { - return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 37, Head: h1}, State: state, Commit: commit, Words: words} + sdk := func(state meshdeliveryclient.MergeState, commit, words string) seat.MergesByGroupValuePullsItem { + return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 37, Head: h1, State: string(state), Commit: commit, Words: words} } - dlv := func(state delivery.MergeState, commit string) delivery.MergePull { - return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 9, Head: h2}, State: state, Commit: commit} + dlv := func(state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem { + return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(state), Commit: commit} } gate := map[string][]byte{} var logs bytes.Buffer @@ -63,17 +64,17 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 t.Errorf("a lone pull request names the group %q", f.Group) } // The first member: its message names the group and both members; the group is not ended. - gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), dlv(delivery.MergeMerging, "")) + gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerging, "")) data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1)) if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 { t.Fatalf("a member's message: %+v %v", back, err) } g.published(ctx, f) // A refusal of the other member is no end (ADR 0441 §3: the group merges on from it later). - gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 9, Head: h2}, State: delivery.MergeRefused, Words: "405"}) + gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(meshdeliveryclient.MergeRefused), Words: "405"}) g.ended(ctx, "feat/take-in", 8, gate["feat/take-in"]) // Merged on at the gate before its merged message is written: still no end; then its message ends the group. - gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), dlv(delivery.MergeMerged, c2)) + gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerged, c2)) g.ended(ctx, "feat/take-in", 9, gate["feat/take-in"]) if len(pub.subjects) != 0 { t.Fatalf("an end before every member's merged message is written: %v", pub.subjects) @@ -97,12 +98,12 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 } // 1585/T19: an ask to say a group's end again is answered by the bridge from the gate's merges, with the ask's time on // the message id; not written while a member is not merged, with the reason; not for a group the gate does not hold. - pull := func(repo string, n int, head string, state delivery.MergeState, commit string) delivery.MergePull { - return delivery.MergePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, State: state, Commit: commit} + pull := func(repo string, n int, head string, state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem { + return seat.MergesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, State: string(state), Commit: commit} } - askMerge := func(second delivery.MergeState, commit2 string) []byte { - v, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}, - Pulls: []delivery.MergePull{pull("novox/mesh-sdk", 37, h1, delivery.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}}) + askMerge := func(second meshdeliveryclient.MergeState, commit2 string) []byte { + v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}, + Pulls: []seat.MergesByGroupValuePullsItem{pull("novox/mesh-sdk", 37, h1, meshdeliveryclient.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}}) if err != nil { t.Fatal(err) } @@ -116,9 +117,9 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 written bool answer string }{ - {"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerged, c2)}, true, true, "written again"}, - {"a member's message not written", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerged, c2)}, false, false, "not written"}, - {"a member not merged", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerging, "")}, true, false, "not written"}, + {"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, true, true, "written again"}, + {"a member's message not written", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, false, false, "not written"}, + {"a member not merged", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerging, "")}, true, false, "not written"}, {"no merge at the gate", map[string][]byte{}, true, false, "holds no merge"}, } { pub, done := &idPub{}, memRevisions{} diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/held.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/held.go index 050935af..a0338539 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/held.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/held.go @@ -5,13 +5,11 @@ import ( "errors" "github.com/nats-io/nats.go/jetstream" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // headsBucket is the module's state bucket heads, `_` as the indexer's buckets are named: the head last // written on FORGE for each pull request (manifest state entry heads, novox/hq ADR 0443 §5). -const headsBucket = delivery.ForgeBridgeModule + "_heads" +const headsBucket = forgeBridgeModule + "_heads" // kvHeads is the bucket heads as the bridge's headStore. type kvHeads struct{ kv jetstream.KeyValue } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go index b07c8d0a..2c54ba13 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go @@ -20,8 +20,10 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -232,8 +234,8 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context, t.Fatal(err) } // Its other state bucket, and the gate's buckets the bridge reads, made by the controller from the gate's manifest - // (mesh-sdk go/delivery MergesBucket, OwedBucket with their histories). - for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, delivery.MergesBucket: delivery.MergesHistory, delivery.OwedBucket: delivery.OwedHistory} { + // (the gate's merges and owed with their histories). + for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, mergesBucket: 10, owedBucket: 5} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil { t.Fatal(err) } @@ -334,7 +336,7 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2 if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, + want := forgemsg.Forge{V: keys.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: "updated", Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) @@ -465,7 +467,7 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5 if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, + want := forgemsg.Forge{V: keys.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"}, Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)}, // No mesh package: only the indexer computes what a fingerprint takes (ADR 0593 §2). @@ -690,15 +692,15 @@ func TestBridgeMergesAGroupOneMemberAtATime(t *testing.T) { // 1342/AC3 t.Fatal(err) } - kv, err := b.js.KeyValue(b.ctx, delivery.MergesBucket) + kv, err := b.js.KeyValue(b.ctx, mergesBucket) if err != nil { t.Fatal(err) } - approved := map[string]string{delivery.ApprovedKey("novox/mesh-delivery", 12): head, delivery.ApprovedKey("novox/mesh-sdk", 4): headB} - pullA := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head}, State: delivery.MergeMerging} - pullB := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerging} - put := func(pulls ...delivery.MergePull) { - v, err := delivery.EncodeMerge(delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls}) + approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): head, meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB} + pullA := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 12, Head: head, State: string(meshdeliveryclient.MergeMerging)} + pullB := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerging)} + put := func(pulls ...seat.MergesByGroupValuePullsItem) { + v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls}) if err != nil { t.Fatal(err) } @@ -726,7 +728,7 @@ func TestBridgeMergesAGroupOneMemberAtATime(t *testing.T) { // 1342/AC3 // The gate hands the next member only after the bridge reported the one before merged (ADR 0441 §5). for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(forgeMerged(t, b)) < 1; time.Sleep(50 * time.Millisecond) { } - pullA.State, pullA.Commit = delivery.MergeMerged, mergeA + pullA.State, pullA.Commit = string(meshdeliveryclient.MergeMerged), mergeA put(pullA, pullB) next("mesh-sdk") var got []string @@ -786,15 +788,15 @@ func TestBridgePerformsNoMeshCheckAct(t *testing.T) { // R2-1, ADR 0521 if err := b.ctl.Flush(); err != nil { t.Fatal(err) } - kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: delivery.CheckerOwedBucket, History: delivery.CheckerOwedHistory}) + kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: "mesh-delivery-checker_owed", History: 5}) if err != nil { t.Fatal(err) } - key, err := delivery.CheckerOwedKey("novox/mesh-delivery", 12, head) + key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, head, "status=mesh/check") if err != nil { t.Fatal(err) } - v, err := delivery.EncodeCheckerOwed(delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.0123456789ab"}) + v, err := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.0123456789ab"}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/kept.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/kept.go index ceaec666..f72cc7b9 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/kept.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/kept.go @@ -10,8 +10,6 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" - "mesh-delivery-forge-bridge/internal/bridge" ) @@ -19,7 +17,7 @@ import ( // directory, its manifest and lock-file hashes, by the directory's git tree hash, so a merge reads the forge only for the // directories it changed (issue 1126; internal/bridge kept.go). A tree hash names its bytes, so a value is never stale; // a merge adds a value only for each directory it changed (a few kilobytes each). -const inputsBucket = delivery.ForgeBridgeModule + "_inputs" +const inputsBucket = forgeBridgeModule + "_inputs" // keptTimeout bounds one read or write of the bucket: the bucket is a saving, never a reason to fail a merge. const keptTimeout = 2 * time.Second diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go index 3c6860fc..e412765d 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go @@ -1,7 +1,7 @@ // Command mesh-delivery-forge-bridge is delivery's forge bridge (novox/hq ADR 0437 §4, issue 1235): it hears the forge // module's pull request events and writes each as a pull message on its stream FORGE. It runs beside today's // mesh-delivery and merges nothing. It serves no request subject and holds no timer: every reaction is an event -// (ADR 0437 §8); a refused act is tried again only on the next event touching its delivery. +// (ADR 0437 §8); a refused act is tried again only on the next event touching its meshdeliveryclient. package main import ( @@ -22,7 +22,6 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-forge-bridge/internal/bridge" groupkeys "mesh-delivery-forge-bridge/internal/groups" @@ -35,7 +34,7 @@ const eventsStream = "EVENTS" // The bridge's own stream FORGE takes `mesh.stream..event.` (mesh-control internal/broker // modstreams.go ModuleStreamSubject). func forgeSubject(event string) string { - return process.ModuleStreamSubject(delivery.ForgeBridgeModule, event) + return process.ModuleStreamSubject(forgeBridgeModule, event) } func main() { @@ -158,7 +157,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro for _, w := range []struct { bucket string react func(ctx context.Context, key string, rev uint64, value []byte) - }{{delivery.OwedBucket, perf.owed}, {delivery.MergesBucket, func(ctx context.Context, key string, rev uint64, value []byte) { + }{{owedBucket, perf.owed}, {mergesBucket, func(ctx context.Context, key string, rev uint64, value []byte) { perf.merges(ctx, key, rev, value) groups.ended(ctx, key, rev, value) }}} { @@ -168,7 +167,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if kv == nil { continue } - if w.bucket == delivery.MergesBucket { + if w.bucket == mergesBucket { groups.merges = kvMerges(kv) } go func() { diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names.go new file mode 100644 index 00000000..4e59a705 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names.go @@ -0,0 +1,16 @@ +package main + +import groupkeys "mesh-delivery-forge-bridge/internal/groups" + +// The holders and buckets the forge bridge names, taken from its own manifest (module.json: its name, its `reads`, its +// states); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder +// (novox/hq ADR 0625 §4, ADR 0634 §1). +const ( + forgeBridgeModule = groupkeys.Module + gateModule = "mesh-delivery-gate" + + // mergesBucket and owedBucket are the gate's states merges and owed, read by the bridge (reads: mesh-delivery-gate.merges, + // mesh-delivery-gate.owed). + mergesBucket = gateModule + "_merges" + owedBucket = gateModule + "_owed" +) diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names_test.go new file mode 100644 index 00000000..95746226 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/names_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "os" + "slices" + "testing" +) + +// theNamesAreTheManifests (a subtest of TestAnOwedStatusIsSetOnceForARevision): the holders and buckets the bridge names +// are its manifest's (its module, its `reads`, its states), since no contract names a holder (novox/hq ADR 0634 §1). +func theNamesAreTheManifests(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Module string `json:"module"` + Reads []string `json:"reads"` + State []struct { + Name string `json:"name"` + } `json:"state"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + if m.Module != forgeBridgeModule { + t.Errorf("the manifest's module is %q, the bridge names itself %q", m.Module, forgeBridgeModule) + } + for _, read := range []string{gateModule + ".merges", gateModule + ".owed"} { + if !slices.Contains(m.Reads, read) { + t.Errorf("the bridge reads %s, its manifest's reads are %v", read, m.Reads) + } + } + var own []string + for _, s := range m.State { + own = append(own, forgeBridgeModule+"_"+s.Name) + } + for _, b := range []string{headsBucket, performedBucket, inputsBucket} { + if !slices.Contains(own, b) { + t.Errorf("the bridge keeps %s, its manifest's states are %v", b, own) + } + } +} diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index a79a64b0..f51b6c71 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,6 +3,9 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go index 32aae566..65e58eff 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go @@ -5,8 +5,9 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Call is a call of a forge tool: the tool by its manifest key and its arguments. @@ -26,7 +27,7 @@ type Act struct { Call Call } -// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: delivery.ts +// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: meshdeliveryclient.ts // commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view // (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo, // number, body), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads). @@ -50,15 +51,15 @@ func ownerRepo(repository string) (owner, repo string) { // it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment // on no pull request are errors, each saying why. func OwedAct(key string, value []byte) (Act, error) { - repository, number, head, name, err := delivery.ParseOwedKey(key) + repository, number, head, name, err := meshdeliveryclient.ParseOwedKey(key) if err != nil { return Act{}, err } - o, err := delivery.DecodeOwed(value) + o, err := meshdeliveryclient.DecodeOwed(value) if err != nil { return Act{}, fmt.Errorf("the owed act %s: %w", key, err) } - kind, context, err := delivery.ParseOwedAct(name) + kind, context, err := meshdeliveryclient.ParseOwedAct(name) if err != nil { return Act{}, err } @@ -74,26 +75,26 @@ func OwedAct(key string, value []byte) (Act, error) { return nil } switch kind { - case delivery.OwedStatus: + case meshdeliveryclient.OwedStatus: if o.Context != context { return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context) } - if o.Context == delivery.StatusContextCheck { + if o.Context == meshdeliveryclient.StatusContextCheck { // No tool writes mesh/check (R4e finding K1), and the bridge performs no act of it: the forge module writes it from // the checker's state mesh-delivery-checker.owed, read by a watch (ADR 0521, finding R2-1). return Act{}, fmt.Errorf("the owed status %s is %s, which the bridge does not perform: the forge module writes it from mesh-delivery-checker.owed", - key, delivery.StatusContextCheck) + key, meshdeliveryclient.StatusContextCheck) } a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context, "state": o.State, "description": o.Text, "target_url": o.Target}} - case delivery.OwedView: + case meshdeliveryclient.OwedView: if err := needsPull(); err != nil { return Act{}, err } a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}} - case delivery.OwedNote: + case meshdeliveryclient.OwedNote: a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}} - case delivery.OwedComment: + case meshdeliveryclient.OwedComment: if err := needsPull(); err != nil { return Act{}, err } @@ -102,19 +103,19 @@ func OwedAct(key string, value []byte) (Act, error) { return a, nil } -// MergeActs turns the value of a merges key (mesh-sdk go/delivery EncodeMerge) into the merges to perform: one for each +// MergeActs turns the value of a merges key (the delivery client's EncodeMerge) into the merges to perform: one for each // pull request in the state merging, in the order the value names them, at the head it expects and with the approved // heads of every member of the group, `/@` sorted (the merge tool's approved_heads). A pull // request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool // needs them for a group. func MergeActs(value []byte) ([]Act, error) { - m, err := delivery.DecodeMerge(value) + m, err := meshdeliveryclient.DecodeMerge(value) if err != nil { return nil, err } - var handed []delivery.MergePull + var handed []seat.MergesByGroupValuePullsItem for _, p := range m.Pulls { - if p.State == delivery.MergeMerging { + if meshdeliveryclient.MergeState(p.State) == meshdeliveryclient.MergeMerging { handed = append(handed, p) } } @@ -136,8 +137,8 @@ func MergeActs(value []byte) ([]Act, error) { var acts []Act for _, p := range handed { owner, repo := ownerRepo(p.Repository) - acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: p.Number, Head: p.Head.Head, - Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": p.Number, "head": p.Head.Head, + acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: int(p.Number), Head: p.Head, + Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": int(p.Number), "head": p.Head, "approved_heads": approved}}}) } return acts, nil diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go index c9b9fe2d..a1cb4669 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go @@ -5,8 +5,10 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) const ( @@ -15,13 +17,13 @@ const ( ) // owedValue is an owed value as the SDK writes it (mesh-sdk go/delivery EncodeOwed), and its key as OwedKey spells it. -func owedValue(t *testing.T, repository string, number int, act string, o delivery.Owed) (string, []byte) { +func owedValue(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) { t.Helper() - key, err := delivery.OwedKey(repository, number, headA, act) + key, err := meshdeliveryclient.OwedKey(repository, number, headA, act) if err != nil { t.Fatal(err) } - b, err := delivery.EncodeOwed(o) + b, err := meshdeliveryclient.EncodeOwed(o) if err != nil { t.Fatal(err) } @@ -30,7 +32,7 @@ func owedValue(t *testing.T, repository string, number int, act string, o delive func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1 key, v := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate", - delivery.Owed{Act: delivery.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"}) + seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"}) a, err := OwedAct(key, v) if err != nil { t.Fatal(err) @@ -46,26 +48,26 @@ func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1 // R4e findings K1 and R2-1 (issue 1301 S4; ADR 0503 §3, ADR 0521): no tool writes mesh/check, since a tool is told no // caller and node-tools invokes every tool; the forge module writes it from the checker's owed state, and the bridge // performs no mesh/check act. - ck, cv := checkerKV(t, delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.1"}) + ck, cv := checkerKV(t, seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.1"}) if a, err := OwedAct(ck, cv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") { t.Fatalf("OwedAct(mesh/check) = %+v, %v; want refused, naming where the forge module reads it", a, err) } // Nor through the gate's own owed bucket: the context is refused whichever key carries it. gk, gv := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/check", - delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://x.example"}) + seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://x.example"}) if a, err := OwedAct(gk, gv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") { t.Fatalf("OwedAct(gate's mesh/check) = %+v, %v; want refused", a, err) } } -// checkerKV is the status the checker owes, as it writes it (mesh-sdk go/delivery CheckerOwedKey, EncodeCheckerOwed). -func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) { +// checkerKV is the status the checker owes, as it writes it (the gate's owed key with the act status=mesh/check). +func checkerKV(t *testing.T, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) { t.Helper() - key, err := delivery.CheckerOwedKey("novox/mesh-delivery", 12, headA) + key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, headA, "status=mesh/check") if err != nil { t.Fatal(err) } - v, err := delivery.EncodeCheckerOwed(o) + v, err := meshdeliveryclient.EncodeOwed(o) if err != nil { t.Fatal(err) } @@ -75,14 +77,14 @@ func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) { func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2 cases := []struct { act string - o delivery.Owed + o seat.OwedByRepositoryNumberHeadActValue want Call }{ - {"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}, + {"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"}, Call{Tool: "gitea.gitea_delivery_view", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "the view"}}}, - {"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}, + {"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"}, Call{Tool: "gitea.gitea_note_append", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, "ref": "mesh-plan", "line": "a line"}}}, - {"comment.2", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}, + {"comment.2", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}, Call{Tool: "gitea.gitea_add_comment", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "hello"}}}, } for _, c := range cases { @@ -98,11 +100,11 @@ func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2 } func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) { - key, v := owedValue(t, "novox/mesh-delivery", 0, "note", delivery.Owed{Act: delivery.OwedNote, Text: "l", Ref: "mesh-plan"}) + key, v := owedValue(t, "novox/mesh-delivery", 0, "note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "l", Ref: "mesh-plan"}) if _, err := OwedAct(key, v); err != nil { t.Errorf("a note on a commit no pull request names: %v", err) } - key, v = owedValue(t, "novox/mesh-delivery", 0, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) + key, v = owedValue(t, "novox/mesh-delivery", 0, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"}) if _, err := OwedAct(key, v); err == nil { t.Error("a view of no pull request was taken") } @@ -110,12 +112,12 @@ func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) { func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) { // A dotted repository's key is unescaped, and its act names the repository whole. - dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"}) + dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "x"}) if a, err := OwedAct(dk, dv); err != nil || a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" { t.Errorf("a dotted repository's act = %+v, %v", a, err) } - key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) - v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"}) + key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"}) + v, _ := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "v"}) if _, err := OwedAct(key, v); err == nil { t.Error("a comment under a view's key was taken") } @@ -127,9 +129,9 @@ func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) { } } -func mergeValue(t *testing.T, m delivery.Merge) []byte { +func mergeValue(t *testing.T, m seat.MergesByGroupValue) []byte { t.Helper() - b, err := delivery.EncodeMerge(m) + b, err := meshdeliveryclient.EncodeMerge(m) if err != nil { t.Fatal(err) } @@ -137,10 +139,10 @@ func mergeValue(t *testing.T, m delivery.Merge) []byte { } func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { // 1342/AC1 - approved := map[string]string{delivery.ApprovedKey("novox/mesh-sdk", 4): headB, delivery.ApprovedKey("novox/mesh-delivery", 12): headA} - v := mergeValue(t, delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []delivery.MergePull{ - {Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerged, Commit: "c0"}, - {Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}, + approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA} + v := mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []seat.MergesByGroupValuePullsItem{ + {Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerged), Commit: "c0"}, + {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)}, }}) acts, err := MergeActs(v) if err != nil { @@ -158,8 +160,8 @@ func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { / t.Errorf("act = %+v", acts[0]) } // A merge with no approved heads is refused: the forge's tool would refuse it with nothing to say which. - v = mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{ - {Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}}}) + v = mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "a", Pulls: []seat.MergesByGroupValuePullsItem{ + {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)}}}) if _, err := MergeActs(v); err == nil { t.Error("a merge with no approved heads was taken") } @@ -178,7 +180,7 @@ func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3, if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, Act: "merge", Words: "novox/mesh-delivery#12 is not merged: its head moved"} if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v", f) diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go index 6af03fb1..a920e158 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go @@ -17,7 +17,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) // ErrUnreadable is wrapped by every error that says an event cannot be turned into a message; it is never @@ -118,7 +118,7 @@ func branchDeleted(subject string, data []byte) (Message, error) { case branch == Trunk: return Message{}, fmt.Errorf("%w: %s of the trunk %s", ErrNotHandled, subject, Trunk) } - body, err := json.Marshal(BranchDeleted{V: delivery.Version, Kind: ForgeBranchDeleted, Repository: d.Owner + "/" + d.Repo, Branch: branch, Head: d.SHA}) + body, err := json.Marshal(BranchDeleted{V: keys.Version, Kind: ForgeBranchDeleted, Repository: d.Owner + "/" + d.Repo, Branch: branch, Head: d.SHA}) if err != nil { return Message{}, unreadable(subject, "%v", err) } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 0c504bb5..9da7668e 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -7,7 +7,7 @@ import ( "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) const head = "0123456789abcdef0123456789abcdef01234567" @@ -59,7 +59,7 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) @@ -105,7 +105,7 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4 if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullClosed} if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) @@ -252,7 +252,7 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1 if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}} if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) @@ -272,7 +272,7 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2 if err != nil { t.Fatal(err) } - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullClosed} if msg.Event != "forge.novox.mesh-delivery.pull" || !forgemsg.SameForge(f, want) { t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f) @@ -304,7 +304,7 @@ func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) { t.Fatal(err) } f, err := forgemsg.DecodeForge(msg.Data) - want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0"} if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") { t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err) @@ -330,8 +330,8 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/ t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err) } for i, want := range []forgemsg.Forge{ - {V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated}, - {V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated}, + {V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated}, + {V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated}, } { f, err := forgemsg.DecodeForge(msgs[i].Data) if err != nil || !forgemsg.SameForge(f, want) { diff --git a/modules/mesh-delivery-forge-bridge/internal/groups/groups.go b/modules/mesh-delivery-forge-bridge/internal/groups/groups.go index 8b731a47..e3ddbc03 100644 --- a/modules/mesh-delivery-forge-bridge/internal/groups/groups.go +++ b/modules/mesh-delivery-forge-bridge/internal/groups/groups.go @@ -16,17 +16,20 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" ) // The bridge's states the tools read and write. const ( + // Module is the forge bridge's own name (its manifest's module, checked by the bridge's names test). + Module = "mesh-delivery-forge-bridge" // PerformedState is the bridge's state of what it performed and published. PerformedState = "performed" // EndAsksState is the state of asks to say a group's end again, keyed by the gate's merges key of the group. EndAsksState = "end-asks" // EndAsksBucket is its bucket on the bus. - EndAsksBucket = delivery.ForgeBridgeModule + "_" + EndAsksState + EndAsksBucket = Module + "_" + EndAsksState ) // Key is the key of performed that says a group's end is written: `groups.`, its value the revision of the @@ -75,7 +78,7 @@ func NewEndAsk(group, why, by string, at time.Time) (string, []byte, error) { if group == "" { return "", nil, errors.New("name the group whose end is said again (group)") } - key, err := delivery.MergesKey(group) + key, err := meshdeliveryclient.MergesKey(group) if err != nil { return "", nil, err } @@ -134,7 +137,7 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin lastAsk[key] = &a } for key, raw := range merges { - m, err := delivery.DecodeMerge(raw) + m, err := meshdeliveryclient.DecodeMerge(raw) if err != nil { unread = append(unread, key) continue @@ -145,9 +148,9 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin r := Row{Key: key, Group: m.Group} allMerged := true for _, p := range m.Pulls { - pub := p.Commit != "" && performed[PublishedKey(p.Repository, p.Number, p.Commit)] - r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: p.Number, State: string(p.State), Commit: p.Commit, Published: pub}) - if p.State != delivery.MergeMerged || !pub { + pub := p.Commit != "" && performed[PublishedKey(p.Repository, int(p.Number), p.Commit)] + r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: int(p.Number), State: p.State, Commit: p.Commit, Published: pub}) + if meshdeliveryclient.MergeState(p.State) != meshdeliveryclient.MergeMerged || !pub { allMerged = false } } @@ -175,8 +178,8 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin func sortRows(rows []Row) { sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key }) } // MembersOf are a merge's approved members with their heads, in the order of their keys. -func MembersOf(m delivery.Merge) []delivery.Head { - var out []delivery.Head +func MembersOf(m seat.MergesByGroupValue) []meshdeliveryclient.Head { + var out []meshdeliveryclient.Head for _, k := range slices.Sorted(func(yield func(string) bool) { for k := range m.Approved { if !yield(k) { @@ -192,7 +195,7 @@ func MembersOf(m delivery.Merge) []delivery.Head { if err != nil { continue } - out = append(out, delivery.Head{Repository: k[:at], Number: n, Head: m.Approved[k]}) + out = append(out, meshdeliveryclient.Head{Repository: k[:at], Number: n, Head: m.Approved[k]}) } return out } diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go index cbd06fde..a154a11e 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go @@ -10,7 +10,6 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/process" @@ -36,7 +35,7 @@ const ( func consumed(w *watch.Watchman, subject string, body []byte, logf func(string, ...any)) { switch { case subject == meshcatalog.SubjectManifestAccepted: - var a delivery.ManifestAccepted + var a meshcatalog.ManifestAcceptedEvent if err := json.Unmarshal(body, &a); err != nil || a.Module == "" { logf("a manifest-accepted that does not read is left: %v", err) return diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go index 86d96c09..ca458306 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go @@ -17,8 +17,13 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/buscompose" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views/viewstest" @@ -62,8 +67,8 @@ accounts { // families (DLV-M5, ADR 0615): the watchman (n1) follows both, never their buckets. func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) { t.Helper() - viewstest.Serve(t, ctx, js, nc, delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants", "n1_"+delivery.WatchmanModule) - viewstest.Serve(t, ctx, js, nc, delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules", "n1_"+delivery.WatchmanModule) + viewstest.Serve(t, ctx, js, nc, watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants", "n1_"+watch.WatchmanModule) + viewstest.Serve(t, ctx, js, nc, meshcatalogclient.Module, modulesState, meshcatalogclient.Module+".indexer_read_modules", "n1_"+watch.WatchmanModule) } // TestWatchmanWhole starts the program the way the node does (its entry, run, with its environment and a broker file) @@ -111,12 +116,12 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 defer cancel() kvs := map[string]jetstream.KeyValue{} for _, c := range []jetstream.KeyValueConfig{ - {Bucket: delivery.PresenceBucket, History: 20}, - {Bucket: delivery.StuckBucket, History: 20}, - {Bucket: delivery.PulseBucket, LimitMarkerTTL: time.Second}, - {Bucket: delivery.StepsBucket("mesh-build-go"), History: 5}, - {Bucket: delivery.AssignerWantsBucket, History: 20}, - {Bucket: delivery.HaveBucket, History: 20}, + {Bucket: watch.PresenceBucket, History: 20}, + {Bucket: watch.StuckBucket, History: 20}, + {Bucket: watch.PulseBucket, LimitMarkerTTL: time.Second}, + {Bucket: watch.StepsBucket("mesh-build-go"), History: 5}, + {Bucket: watch.AssignerWantsBucket, History: 20}, + {Bucket: watch.HaveBucket, History: 20}, {Bucket: watch.FindingsBucket, History: 5}, {Bucket: firstViewsBucket, History: 1}, } { @@ -174,11 +179,11 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 return e, err == nil } online := func(key string) (bool, bool) { - e, ok := has(delivery.PresenceBucket, key) + e, ok := has(watch.PresenceBucket, key) if !ok { return false, false } - p, err := delivery.DecodePresence(e.Value()) + p, err := meshwatchdogsclient.DecodeNodePresence(e.Value()) return p.Online, err == nil } until := func(what string, ok func() bool) { @@ -191,14 +196,14 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 t.Fatalf("never saw: %s", what) } pulseOf := func(bucket, key string) string { - k, err := delivery.PulseKey(bucket, key) + k, err := meshwatchdogsclient.PulseKey(bucket, key) if err != nil { t.Fatal(err) } return k } stuckOf := func(bucket, key string) string { - k, err := delivery.StuckKey(bucket, key) + k, err := meshwatchdogsclient.StuckKey(bucket, key) if err != nil { t.Fatal(err) } @@ -214,12 +219,12 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 until("node n1 offline", func() bool { on, ok := online("n1"); return ok && !on }) // An ask put on a build seat gets a pulse; one no node takes lets the pulse expire, and that writes a stuck key. - askStream, _ := delivery.AskStream(delivery.AskGo) - steps := kvs[delivery.StepsBucket("mesh-build-go")] + askStream, _ := buildstep.AskStream(buildstep.AskGo) + steps := kvs[watch.StepsBucket("mesh-build-go")] // A node takes an ask: it writes the step key running (and passed when the ask ends). putStep := func(id, state string) { t.Helper() - body, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_test", Node: "n1", State: state, Started: time.Now().UTC()}) + body, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: state, Started: time.Now().UTC()}) if err != nil { t.Fatal(err) } @@ -228,16 +233,16 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 } } const ask1, ask2 = "m0000001aaaaaa", "m0000002bbbbbb" - putStep(ask1, delivery.StepRunning) - until("a pulse for the ask a node took", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask1)); return ok }) - var stuck delivery.Stuck + putStep(ask1, buildstep.StepRunning) + until("a pulse for the ask a node took", func() bool { _, ok := has(watch.PulseBucket, pulseOf(askStream, ask1)); return ok }) + var stuck meshwatchdogsclient.Stuck until("a stuck key for the ask no node ended", func() bool { - e, ok := has(delivery.StuckBucket, stuckOf(askStream, ask1)) + e, ok := has(watch.StuckBucket, stuckOf(askStream, ask1)) if !ok { return false } var err error - stuck, err = delivery.DecodeStuck(e.Value()) + stuck, err = meshwatchdogsclient.DecodeStuck(e.Value()) return err == nil }) if stuck.Moved || stuck.Bucket != askStream || stuck.Key != ask1 { @@ -245,7 +250,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 } // The node n1, gone offline and never back, is stuck as well. until("a stuck key for node n1 that did not come back", func() bool { - _, ok := has(delivery.StuckBucket, stuckOf(delivery.PresenceBucket, "n1")) + _, ok := has(watch.StuckBucket, stuckOf(watch.PresenceBucket, "n1")) return ok }) @@ -253,18 +258,18 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 n2 := connect("node.n2") until("node n2 online", func() bool { on, ok := online("n2"); return ok && on }) n2.Close() - until("a pulse for node n2 gone offline", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(delivery.PresenceBucket, "n2")); return ok }) + until("a pulse for node n2 gone offline", func() bool { _, ok := has(watch.PulseBucket, pulseOf(watch.PresenceBucket, "n2")); return ok }) connect("node.n2") until("node n2 online again", func() bool { on, ok := online("n2"); return ok && on }) - until("the pulse of node n2 deleted", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(delivery.PresenceBucket, "n2")); return !ok }) + until("the pulse of node n2 deleted", func() bool { _, ok := has(watch.PulseBucket, pulseOf(watch.PresenceBucket, "n2")); return !ok }) // An ask a node takes at once is the same: no stuck key, however long the bound passes. - putStep(ask2, delivery.StepRunning) - until("a pulse for the second ask", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask2)); return ok }) - putStep(ask2, delivery.StepPassed) - until("the pulse of the second ask deleted", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask2)); return !ok }) + putStep(ask2, buildstep.StepRunning) + until("a pulse for the second ask", func() bool { _, ok := has(watch.PulseBucket, pulseOf(askStream, ask2)); return ok }) + putStep(ask2, buildstep.StepPassed) + until("the pulse of the second ask deleted", func() bool { _, ok := has(watch.PulseBucket, pulseOf(askStream, ask2)); return !ok }) time.Sleep(6 * time.Second) // twice the bound and the marker time - for _, k := range []string{stuckOf(delivery.PresenceBucket, "n2"), stuckOf(askStream, ask2)} { - if _, ok := has(delivery.StuckBucket, k); ok { + for _, k := range []string{stuckOf(watch.PresenceBucket, "n2"), stuckOf(askStream, ask2)} { + if _, ok := has(watch.StuckBucket, k); ok { t.Errorf("a stuck key %s was written for a move that happened before the bound", k) } } @@ -362,12 +367,12 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 defer cancel() kvs := map[string]jetstream.KeyValue{} for _, c := range []jetstream.KeyValueConfig{ - {Bucket: delivery.PresenceBucket, History: 20}, - {Bucket: delivery.StuckBucket, History: 20}, - {Bucket: delivery.PulseBucket, LimitMarkerTTL: time.Second}, - {Bucket: delivery.StepsBucket("mesh-build-go"), History: 5}, - {Bucket: delivery.AssignerWantsBucket, History: 20}, - {Bucket: delivery.HaveBucket, History: 20}, + {Bucket: watch.PresenceBucket, History: 20}, + {Bucket: watch.StuckBucket, History: 20}, + {Bucket: watch.PulseBucket, LimitMarkerTTL: time.Second}, + {Bucket: watch.StepsBucket("mesh-build-go"), History: 5}, + {Bucket: watch.AssignerWantsBucket, History: 20}, + {Bucket: watch.HaveBucket, History: 20}, {Bucket: watch.FindingsBucket, History: 5}, {Bucket: firstViewsBucket, History: 1}, } { @@ -379,11 +384,11 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 } serveOwners(t, ctx, js, ctl) const id = "m0000009ffffff" - body, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_test", Node: "n1", State: delivery.StepRunning, Started: time.Now().UTC()}) + body, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: buildstep.StepRunning, Started: time.Now().UTC()}) if err != nil { t.Fatal(err) } - if _, err := kvs[delivery.StepsBucket("mesh-build-go")].Put(ctx, "n1."+id, body); err != nil { + if _, err := kvs[watch.StepsBucket("mesh-build-go")].Put(ctx, "n1."+id, body); err != nil { t.Fatal(err) } broker := filepath.Join(dir, "broker") @@ -396,13 +401,13 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 done := make(chan error, 1) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) - stream, _ := delivery.AskStream(delivery.AskGo) - pk, err := delivery.PulseKey(stream, id) + stream, _ := buildstep.AskStream(buildstep.AskGo) + pk, err := meshwatchdogsclient.PulseKey(stream, id) if err != nil { t.Fatal(err) } for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { - if _, err := kvs[delivery.PulseBucket].Get(ctx, pk); err == nil { + if _, err := kvs[watch.PulseBucket].Get(ctx, pk); err == nil { return } } @@ -449,12 +454,12 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { defer cancel() kvs := map[string]jetstream.KeyValue{} for _, c := range []jetstream.KeyValueConfig{ - {Bucket: delivery.PresenceBucket, History: 20}, - {Bucket: delivery.StuckBucket, History: 20}, - {Bucket: delivery.PulseBucket, LimitMarkerTTL: time.Second}, - {Bucket: delivery.StepsBucket("mesh-build-go"), History: 5}, - {Bucket: delivery.AssignerWantsBucket, History: 20}, - {Bucket: delivery.HaveBucket, History: 20}, + {Bucket: watch.PresenceBucket, History: 20}, + {Bucket: watch.StuckBucket, History: 20}, + {Bucket: watch.PulseBucket, LimitMarkerTTL: time.Second}, + {Bucket: watch.StepsBucket("mesh-build-go"), History: 5}, + {Bucket: watch.AssignerWantsBucket, History: 20}, + {Bucket: watch.HaveBucket, History: 20}, {Bucket: watch.FindingsBucket, History: 5}, {Bucket: firstViewsBucket, History: 1}, } { @@ -465,27 +470,27 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { kvs[c.Bucket] = kv } serveOwners(t, ctx, js, ctl) - fpNew := delivery.SourcePrefix2 + strings.Repeat("b", 64) - fpOld := delivery.SourcePrefix2 + strings.Repeat("a", 64) + fpNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) + fpOld := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) putWant := func(node, module, fp string) { t.Helper() - body, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: "sha256:" + strings.Repeat("c", 64)}) + body, err := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fp, Composed: "sha256:" + strings.Repeat("c", 64)}) if err != nil { t.Fatal(err) } - key, _ := delivery.WantKey(node, module) - if _, err := kvs[delivery.AssignerWantsBucket].Put(ctx, key, body); err != nil { + key, _ := meshwantsclient.WantKey(node, module) + if _, err := kvs[watch.AssignerWantsBucket].Put(ctx, key, body); err != nil { t.Fatal(err) } } putHave := func(node, module, fp string) { t.Helper() - body, err := delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + body, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) if err != nil { t.Fatal(err) } - key, _ := delivery.HaveKey(node, module) - if _, err := kvs[delivery.HaveBucket].Put(ctx, key, body); err != nil { + key, _ := nodeengineclient.HaveKey(node, module) + if _, err := kvs[watch.HaveBucket].Put(ctx, key, body); err != nil { t.Fatal(err) } } @@ -502,8 +507,8 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) pulse := func(node, module string) string { - wk, _ := delivery.WantKey(node, module) - pk, err := delivery.PulseKey(delivery.AssignerWantsBucket, wk) + wk, _ := meshwantsclient.WantKey(node, module) + pk, err := meshwatchdogsclient.PulseKey(watch.AssignerWantsBucket, wk) if err != nil { t.Fatal(err) } @@ -518,15 +523,15 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { } t.Fatalf("never saw: %s", what) } - has := func(key string) bool { _, err := kvs[delivery.PulseBucket].Get(ctx, key); return err == nil } + has := func(key string) bool { _, err := kvs[watch.PulseBucket].Get(ctx, key); return err == nil } until("a pulse for the want that stood before the start", func() bool { return has(pulse("n2", "web")) }) putWant("n2", "mail", fpNew) until("a pulse for a want written after the start", func() bool { return has(pulse("n2", "mail")) }) - e, err := kvs[delivery.PulseBucket].Get(ctx, pulse("n2", "web")) + e, err := kvs[watch.PulseBucket].Get(ctx, pulse("n2", "web")) if err != nil { t.Fatal(err) } - if p, err := delivery.DecodePulse(e.Value()); err != nil || !strings.Contains(p.Owes, "n2") { + if p, err := meshwatchdogsclient.DecodePulse(e.Value()); err != nil || !strings.Contains(p.Owes, "n2") { t.Errorf("the pulse = %+v, %v; want it owed by the agent of n2", p, err) } putHave("n2", "web", fpNew) @@ -535,9 +540,9 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { t.Errorf("the pulse of another want ended with web's have") } for _, m := range []string{"web"} { - wk, _ := delivery.WantKey("n2", m) - sk, _ := delivery.StuckKey(delivery.AssignerWantsBucket, wk) - if _, err := kvs[delivery.StuckBucket].Get(ctx, sk); err == nil { + wk, _ := meshwantsclient.WantKey("n2", m) + sk, _ := meshwatchdogsclient.StuckKey(watch.AssignerWantsBucket, wk) + if _, err := kvs[watch.StuckBucket].Get(ctx, sk); err == nil { t.Errorf("a stuck key was written for the want of %s that was applied", m) } } diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index f522d41e..b00534cb 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -25,8 +25,10 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/provisions" "mesh-delivery-watchman/internal/socket" @@ -64,7 +66,7 @@ func (o *kvOut) PutFinding(key string, value []byte) error { func (o *kvOut) DeleteFinding(key string) error { return o.findings.Delete(o.ctx, key) } func (o *kvOut) PutPresence(key string, online bool) error { - value, err := delivery.EncodePresence(delivery.Presence{Online: online}) + value, err := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: online}) if err != nil { return err } @@ -184,7 +186,7 @@ func createdOr(e jetstream.KeyValueEntry, at time.Time) time.Time { // The watchman's own states of its first views (views.StatusState); its findings of a first view unanswered past its // bound go to its findings bucket with the others (ADR 0615 §6). The owners' states it follows are the assigner's wants // and the indexer's modules (DLV-M5). -var firstViewsBucket = delivery.WatchmanModule + "_" + views.StatusState +var firstViewsBucket = watch.WatchmanModule + "_" + views.StatusState const ( wantsState = "wants" @@ -219,12 +221,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro for _, b := range []struct { name string into *jetstream.KeyValue - }{{delivery.PresenceBucket, &kvs.presence}, {delivery.PulseBucket, &kvs.pulse}, {delivery.StuckBucket, &kvs.stuck}, {watch.FindingsBucket, &kvs.findings}} { + }{{watch.PresenceBucket, &kvs.presence}, {watch.PulseBucket, &kvs.pulse}, {watch.StuckBucket, &kvs.stuck}, {watch.FindingsBucket, &kvs.findings}} { if *b.into, err = process.EssentialBucket(ctx, js, "watchman", b.name, "say who is online or what stopped moving"); err != nil { return err } } - kvs.haves = process.OptionalBucket(ctx, js, delivery.HaveBucket, "a want makes no pulse, so a move that stops is not seen, until the watchman starts again", logger.Printf) + kvs.haves = process.OptionalBucket(ctx, js, watch.HaveBucket, "a want makes no pulse, so a move that stops is not seen, until the watchman starts again", logger.Printf) // The assigner's wants and the indexer's modules are their owners' states, read by the first view of their read tools and // then their views families, never their buckets (ADR 0615 §1, DLV-M5). An owner that is down never holds the start: each // is read on a goroutine once its first view is answered, and until then the unanswered first view is the watchman's @@ -237,15 +239,15 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The module has one durable consumer of EVENTS: one hub pulls it for the views reader, the indexer's take-in events // and the seats' events, started once every part is registered. hub := process.NewEventHub(logger.Printf) - reader := &views.ProcessReader{Module: delivery.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub, + reader := &views.ProcessReader{Module: watch.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub, Findings: func() views.FindingWriter { return sdkfindings.Writer(ctx, func() jetstream.KeyValue { return kvs.findings }) }} - wants, err := reader.Follow(delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants") + wants, err := reader.Follow(watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants") if err != nil { return err } - modules, err := reader.Follow(delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules") + modules, err := reader.Follow(meshcatalogclient.Module, modulesState, meshcatalogclient.Module+".indexer_read_modules") if err != nil { return err } @@ -253,8 +255,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: kvs.findings}, time.Now) w.Logf(logger.Printf) // What the wants and the modules hold is ended by their own late read (watch.ReadLate), never by the start's end. - w.Awaits(delivery.AssignerWantsBucket) - w.Awaits(delivery.ModulesBucket) + w.Awaits(watch.AssignerWantsBucket) + w.Awaits(watch.ModulesBucket) // A test shortens how long a move may take; the mesh never sets it. if v := strings.TrimSpace(getenv("MESH_DELIVERY_PULSE_BOUND")); v != "" { d, err := time.ParseDuration(v) @@ -270,7 +272,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro each func(jetstream.KeyValueEntry) }{ {kvs.presence, func(e jetstream.KeyValueEntry) { - if p, err := delivery.DecodePresence(e.Value()); err == nil && opOf(e) == watch.Put { + if p, err := meshwatchdogsclient.DecodeNodePresence(e.Value()); err == nil && opOf(e) == watch.Put { w.SeedPresence(e.Key(), p.Online, e.Created()) } }}, @@ -305,27 +307,27 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro var haveSeen jetstream.KeyWatcher if kvs.haves != nil { if haveSeen, err = settle(ctx, kvs.haves, func(e jetstream.KeyValueEntry) { w.Have(e.Key(), e.Value(), opOf(e)) }); err != nil { - process.NotWatched(logger.Printf, delivery.HaveBucket, err, "moves are not watched until the watchman starts again") + process.NotWatched(logger.Printf, watch.HaveBucket, err, "moves are not watched until the watchman starts again") haveSeen = nil } } // The builder's builds (ADR 0603 §8): a key queued behind a package is a wait, mirrored from the time it was written. var buildSeen jetstream.KeyWatcher - if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "a build waiting for a package is not watched until the watchman starts again", logger.Printf); kv != nil { + if kv := process.OptionalBucket(ctx, js, watch.BuilderBuildsBucket, "a build waiting for a package is not watched until the watchman starts again", logger.Printf); kv != nil { buildEach := func(e jetstream.KeyValueEntry) { w.Build(e.Key(), e.Value(), opOf(e), e.Created()) } if buildSeen, err = settle(ctx, kv, buildEach); err != nil { - process.NotWatched(logger.Printf, delivery.BuilderBuildsBucket, err, "a build waiting for a package is not watched until the watchman starts again") + process.NotWatched(logger.Printf, watch.BuilderBuildsBucket, err, "a build waiting for a package is not watched until the watchman starts again") buildSeen = nil } else { - w.ReadWhole(delivery.BuilderBuildsBucket) + w.ReadWhole(watch.BuilderBuildsBucket) } } // The build seats' steps: a step key `.` written is the ask taken (issue 1887 A8b). A seat whose build module // keeps no steps bucket (yet) is said and left; the asks of its stream are mirrored all the same. taken := map[string]jetstream.KeyWatcher{} - for _, lang := range delivery.AskLanguages { - module, _ := delivery.BuildModuleOf(lang) - kv, err := js.KeyValue(ctx, delivery.StepsBucket(module)) + for _, lang := range buildstep.AskLanguages { + module, _ := buildstep.BuildModuleOf(lang) + kv, err := js.KeyValue(ctx, watch.StepsBucket(module)) if err != nil { logger.Printf("the steps of %s cannot be read (%v): its asks are mirrored and never seen taken", module, err) continue @@ -337,7 +339,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro continue } taken[lang] = sw - if bucket, err := delivery.AskStream(lang); err == nil { + if bucket, err := buildstep.AskStream(lang); err == nil { w.ReadWhole(bucket) } } @@ -406,16 +408,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // bucket is there, bound once it appears when it is not; each change is heard on the watch, never polled. healthEach := func(e jetstream.KeyValueEntry) { w.NodeHealth(e.Key(), e.Value(), opOf(e)) } var healthWatch jetstream.KeyWatcher - if kv, err := js.KeyValue(ctx, delivery.NodeHealthBucket); err == nil { + if kv, err := js.KeyValue(ctx, watch.NodeHealthBucket); err == nil { if healthWatch, err = settle(ctx, kv, healthEach); err != nil { - process.NotWatched(logger.Printf, delivery.NodeHealthBucket, err, "no node's own health is said until the watchman starts again") + process.NotWatched(logger.Printf, watch.NodeHealthBucket, err, "no node's own health is said until the watchman starts again") } } else { - process.AwaitBucket(ctx, js, conn, delivery.NodeHealthBucket, "no node's own health is said", + process.AwaitBucket(ctx, js, conn, watch.NodeHealthBucket, "no node's own health is said", logger.Printf).Ready(func(kv jetstream.KeyValue) { hw, err := settle(ctx, kv, healthEach) if err != nil { - process.NotWatched(logger.Printf, delivery.NodeHealthBucket, err, "no node's own health is said until the watchman starts again") + process.NotWatched(logger.Printf, watch.NodeHealthBucket, err, "no node's own health is said until the watchman starts again") return } go func() { _ = follow(ctx, hw, healthEach) }() @@ -453,7 +455,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro w.TakeInCaughtUp() } } - if err := hub.Handle("mesh.mod."+delivery.IndexerModule+".event.take-in.*", func(m jetstream.Msg) { takeInHeard(w, m, logger.Printf) }); err != nil { + if err := hub.Handle("mesh.mod."+meshcatalogclient.Module+".event.take-in.*", func(m jetstream.Msg) { takeInHeard(w, m, logger.Printf) }); err != nil { return err } } @@ -473,13 +475,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro defer hub.Stop() // The indexer's modules (ADR 0578 §1): a module whose value carries a refusal of its manifest is a finding. verdictEach := func(e jetstream.KeyValueEntry) { w.ModuleVerdict(e.Key(), e.Value(), opOf(e)) } - go func() { errs <- readLate(ctx, w, modules, delivery.ModulesBucket, verdictEach) }() + go func() { errs <- readLate(ctx, w, modules, watch.ModulesBucket, verdictEach) }() if haveSeen != nil { go func() { errs <- follow(ctx, haveSeen, func(e jetstream.KeyValueEntry) { w.Have(e.Key(), e.Value(), opOf(e)) }) }() go func() { - errs <- readLate(ctx, w, wants, delivery.AssignerWantsBucket, func(e jetstream.KeyValueEntry) { + errs <- readLate(ctx, w, wants, watch.AssignerWantsBucket, func(e jetstream.KeyValueEntry) { w.Want(e.Key(), e.Value(), opOf(e), createdOr(e, time.Now())) }) }() @@ -533,7 +535,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro const eventsStream = "EVENTS" // pendingTool is the indexer's tool that answers the waits it holds (ADR 0633 §5), as the view asks it. -const pendingTool = delivery.IndexerModule + ".indexer_pending" +const pendingTool = meshcatalogclient.Module + ".indexer_pending" // seedTakeIn asks the indexer the waits it holds now and starts each (Watchman.SeedTakeIn). An indexer that does not // answer is said: the waits are then known only from the events heard after. diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go index c96c4f0d..7e8ab4b3 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views" @@ -82,11 +81,11 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 // The watchman's want wait starts at the entry's time (createdOr): through mesh-sdk's views.ProcessReader it is // the time the owner's bus wrote the key, never the watchman's start. written := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC) - r := &views.ProcessReader{Module: delivery.WatchmanModule, Call: func(string, any) (json.RawMessage, error) { - return json.Marshal(views.Answer{V: views.Version, Owner: delivery.AssignerModule, State: wantsState, Revision: 3, Created: written, + r := &views.ProcessReader{Module: watch.WatchmanModule, Call: func(string, any) (json.RawMessage, error) { + return json.Marshal(views.Answer{V: views.Version, Owner: watch.AssignerModule, State: wantsState, Revision: 3, Created: written, At: written.Add(time.Hour), Values: map[string]json.RawMessage{"ace.mail": json.RawMessage(`{}`)}, Written: map[string]time.Time{"ace.mail": written}}) }} - p, err := r.Follow(delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants") + p, err := r.Follow(watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants") if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index defa3868..2899b58e 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,6 +3,13 @@ module mesh-delivery-watchman go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-delivery-watchman/internal/watch/busstate.go b/modules/mesh-delivery-watchman/internal/watch/busstate.go index 8828c3e5..8b1b66b4 100644 --- a/modules/mesh-delivery-watchman/internal/watch/busstate.go +++ b/modules/mesh-delivery-watchman/internal/watch/busstate.go @@ -7,7 +7,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" ) // **A refused or overdue creation of bus state is loud** (novox/hq ADR 0595 §4; design 25 § 12.2; DLV-W1). The watchman @@ -60,7 +61,7 @@ func refusedKey(module, local string) string { // Accepted is the indexer's manifest-accepted heard: each entry it asks for waits OverdueBound for the bus module's word, // and every entry of the module this watchman waits on or holds a finding of that it no longer asks for is ended. -func (w *Watchman) Accepted(a delivery.ManifestAccepted) { +func (w *Watchman) Accepted(a meshcatalog.ManifestAcceptedEvent) { w.mu.Lock() defer w.mu.Unlock() asked := map[string]bool{} @@ -100,7 +101,7 @@ func (w *Watchman) Accepted(a delivery.ManifestAccepted) { func (w *Watchman) markCreated(module, local string) { pk := signalKey(rowBusCreated, busStateID(module, local)) owes, _ := json.Marshal(signalDue{Do: dueCreated}) - value, err := delivery.EncodePulse(delivery.Pulse{Bucket: signalBucket, Key: strings.TrimPrefix(pk, signalBucket+"."), Owes: string(owes)}) + value, err := meshwatchdogsclient.EncodePulse(meshwatchdogsclient.Pulse{Bucket: signalBucket, Key: strings.TrimPrefix(pk, signalBucket+"."), Owes: string(owes)}) if err == nil { err = w.out.PutPulse(pk, value, 0) } diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 5662391c..789ca57a 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -7,9 +7,14 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-delivery/sdk/heard" @@ -25,9 +30,9 @@ func mustEncode(t *testing.T, f Finding) []byte { return v } -func haveOf(t *testing.T, phase delivery.Phase, health delivery.Health, detail string) []byte { +func haveOf(t *testing.T, phase string, health string, detail string) []byte { t.Helper() - b, err := delivery.EncodeHave(delivery.Have{FP: fpNew, Phase: phase, Health: health, Detail: detail}) + b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpNew, Phase: phase, Health: health, Detail: detail}) if err != nil { t.Fatal(err) } @@ -136,15 +141,15 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{FindingKey(ScopeDelivery, "m.00000000000000000043", KindTakeInWaits): Warning}}, {"ADR 0578 §1: a module whose value carries a refusal of its manifest is a finding until it is accepted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { value := func(verdict string, problems ...string) []byte { - b, err := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), + b, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), Repository: "novox/mesh-catalog", Commit: "c", Verdict: verdict, Problems: problems}) if err != nil { t.Fatal(err) } return b } - w.ModuleVerdict("nats", value(delivery.VerdictRefused, "removes a stream"), Put) - w.ModuleVerdict("power", value(delivery.VerdictRefused, "shows nothing"), Put) + w.ModuleVerdict("nats", value(meshcatalogclient.VerdictRefused, "removes a stream"), Put) + w.ModuleVerdict("power", value(meshcatalogclient.VerdictRefused, "shows nothing"), Put) k := FindingKey(ScopeModule, "nats", KindManifestRefused) if f := o.findings[k]; !strings.Contains(f.Summary, "refused the manifest of nats: removes a stream") { t.Fatalf("findings = %v", o.findings) @@ -152,7 +157,7 @@ func TestTheWatchmansFindings(t *testing.T) { w.ModuleVerdict("nats", value(""), Put) w.ModuleVerdict("power", nil, Delete) // ADR 0579 §1: an accepted manifest a stricter judge outgrew is a warning until mended. - outgrown, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), + outgrown, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), Repository: "novox/mesh-tools", Commit: "c", Warnings: []string{"says no credential form"}}) w.ModuleVerdict("node-tools", outgrown, Put) if f := o.findings[FindingKey(ScopeModule, "node-tools", KindManifestOutgrown)]; f.Severity != Warning || !strings.Contains(f.Summary, "says no credential form") { @@ -161,8 +166,8 @@ func TestTheWatchmansFindings(t *testing.T) { w.ModuleVerdict("node-tools", value(""), Put) // Clause 3 of the record on a release's Approve: a manifest that waits for a judge that knows its fields is its own // warning (a refusal in the value, naming the fields), not manifest-refused, and goes when it is accepted. - waiting, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), - Repository: "novox/mesh-catalog", Commit: "c", Verdict: delivery.VerdictRefused, Problems: []string{"waits"}, Unknown: []string{"core-credentials"}}) + waiting, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), + Repository: "novox/mesh-catalog", Commit: "c", Verdict: meshcatalogclient.VerdictRefused, Problems: []string{"waits"}, Unknown: []string{"core-credentials"}}) w.ModuleVerdict("nats", waiting, Put) if f := o.findings[FindingKey(ScopeModule, "nats", KindManifestWaits)]; f.Severity != Warning || !strings.Contains(f.Summary, "core-credentials") { t.Fatalf("findings = %v", o.findings) @@ -173,10 +178,10 @@ func TestTheWatchmansFindings(t *testing.T) { w.ModuleVerdict("nats", value(""), Put) }, map[string]string{}}, {"DLV-W1, 1966/AC37-AC38 (ADR 0595 §4, design 25 § 12.2): an accepted entry the bus module says nothing of in five minutes is overdue until created; a refused one stands until created; a later verdict no longer asking ends both", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - accepted := func(locals ...string) delivery.ManifestAccepted { - a := delivery.ManifestAccepted{Module: "mesh-knowledge", Digest: strings.Repeat("d", 64)} + accepted := func(locals ...string) meshcatalog.ManifestAcceptedEvent { + a := meshcatalog.ManifestAcceptedEvent{Module: "mesh-knowledge", Digest: strings.Repeat("d", 64)} for _, l := range locals { - a.BusState = append(a.BusState, catalogue.BusStateEntry{Name: l, Storage: catalogue.StorageLedger}) + a.BusState = append(a.BusState, meshcatalog.ManifestAcceptedEventBusStateItem{Name: l, Storage: "ledger"}) } return a } @@ -282,7 +287,7 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{}}, {"ADR 0601 §4: what a node keeps of a module as it runs, for a manifest that does not say it yet, is a warning until the have says none", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { have := func(kept ...string) []byte { - b, err := delivery.EncodeHave(delivery.Have{FP: "src2:" + strings.Repeat("a", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK, Kept: kept}) + b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: "src2:" + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, Kept: kept}) if err != nil { t.Fatal(err) } @@ -310,7 +315,7 @@ func TestTheWatchmansFindings(t *testing.T) { if f := o.findings[k]; f.Kind != KindStuck || !strings.Contains(f.Summary, "the ask ask1 to end on n1") { t.Fatalf("findings = %v", o.findings) } - w.Step("go", "n1.ask1", endedStep("ask1", delivery.StepPassed, t0), Put) + w.Step("go", "n1.ask1", endedStep("ask1", buildstep.StepPassed, t0), Put) }, map[string]string{}}, {"WM2 S1: a node offline past its bound is silent, and back online it is not", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Connect("node.ace") @@ -406,7 +411,7 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{}}, {"WM3 S21: a heard machine's have failed for one apply interval is apply-failing, naming the package manager; applied again it ends", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Connect("node.ace") - w.Have("ace.web", haveOf(t, delivery.PhaseFailed, delivery.HealthUnknown, "refused: the package manager (pacman) does not answer"), Put) + w.Have("ace.web", haveOf(t, nodeengineclient.PhaseFailed, nodeengineclient.HealthUnknown, "refused: the package manager (pacman) does not answer"), Put) if len(o.findings) != 0 { t.Fatalf("raised before its bound: %v", o.findings) } @@ -414,7 +419,7 @@ func TestTheWatchmansFindings(t *testing.T) { if f := o.findings["machine.ace.apply-failing"]; !strings.Contains(f.Summary, "pacman does not answer") || f.Headline != "ace cannot install packages" { t.Fatalf("findings = %v", o.findings) } - w.Have("ace.web", haveOf(t, delivery.PhaseRunning, delivery.HealthOK, ""), Put) + w.Have("ace.web", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), Put) }, map[string]string{}}, {"WM3 S21: a provisioner client's failed files are apply-failing past the bound; a silent machine's are not", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Connect("node.ace") @@ -426,23 +431,23 @@ func TestTheWatchmansFindings(t *testing.T) { w.Provisioned(ProvReport{Node: "ace", Module: "web", Status: "applied", At: t0}) }, map[string]string{}}, {"WM3: unhealthy once a failed have is confirmed after a minute (written once and steady); recovered within it none; urgent after four hours", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.Have("ace.web", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "the unit failed"), Put) + w.Have("ace.web", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "the unit failed"), Put) if len(o.findings) != 0 { t.Fatalf("raised before its bound: %v", o.findings) } - w.Have("ace.quick", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "blip"), Put) - w.Have("ace.quick", haveOf(t, delivery.PhaseRunning, delivery.HealthOK, ""), Put) // recovered within the bound + w.Have("ace.quick", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "blip"), Put) + w.Have("ace.quick", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), Put) // recovered within the bound expire(w, c, signalKey("unhealthy-confirm", "quick.ace"), UnhealthyConfirmAfter) expire(w, c, signalKey("unhealthy-confirm", "web.ace"), UnhealthyConfirmAfter) // the one write, never repeated if o.findings["module.web.ace.unhealthy"].Severity != Warning || len(o.findings) != 1 { t.Fatalf("findings = %v", o.findings) } expire(w, c, signalKey("unhealthy", "web.ace"), UnhealthyUrgentAfter) - w.Have("ace.mail", haveOf(t, delivery.PhaseRunning, delivery.HealthWaits, "a part waits for the secret token"), Put) // needs-operator is not observed here + w.Have("ace.mail", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthWaits, "a part waits for the secret token"), Put) // needs-operator is not observed here }, map[string]string{"module.web.ace.unhealthy": Urgent}}, {"WM3: a warning unhealthy finding read back at the start is kept as it was, urgent when its four hours from Since are out", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { seedUnhealthy(t, w, o, Warning, t0.Add(-time.Hour)) - w.Have("ace.web", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "x"), Put) + w.Have("ace.web", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "x"), Put) w.Reconcile() if o.findings["module.web.ace.unhealthy"].Severity != Warning { t.Fatalf("cleared: findings %v", o.findings) @@ -457,14 +462,14 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{"module.web.ace.unhealthy": Urgent}}, {"WM3: an urgent unhealthy finding read back at the start stays urgent, with no signal restarted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { seedUnhealthy(t, w, o, Urgent, t0.Add(-5*time.Hour)) - w.Have("ace.web", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "x"), Put) + w.Have("ace.web", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "x"), Put) w.Reconcile() if o.findings["module.web.ace.unhealthy"].Severity != Urgent || len(w.signals) != 0 { t.Fatalf("findings %v signals %v", o.findings, w.signals) } }, map[string]string{"module.web.ace.unhealthy": Urgent}}, {"WM3: a restart replays a have already failed: it is confirmed after a minute like a new one", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.Have("novox.fail2ban", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "down since 19:40"), Put) + w.Have("novox.fail2ban", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "down since 19:40"), Put) expire(w, c, signalKey("unhealthy-confirm", "fail2ban.novox"), UnhealthyConfirmAfter) }, map[string]string{"module.fail2ban.novox.unhealthy": Warning}}, {"1954/D2: a provision in waits is its consumer's needs-operator; provided, it ends", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { @@ -486,22 +491,22 @@ func TestTheWatchmansFindings(t *testing.T) { w.Provision("ace.web.oidc", nil, Delete) }, map[string]string{}}, {"774: a node's own report: its network, the resolver file rewritten, failed units, a relogin, a directory used as found, a login fallen back", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - report := func(h delivery.NodeHealth) []byte { - b, err := delivery.EncodeNodeHealth(h) + report := func(h nodeengine.HealthByNodeValue) []byte { + b, err := nodeengineclient.EncodeHealth(h) if err != nil { t.Fatal(err) } return b } - w.Have("novox.nats", haveOf(t, delivery.PhaseRunning, delivery.HealthOK, ""), Put) // the control node: the bus node - w.NodeHealth("novox", report(delivery.NodeHealth{Node: "novox", Network: &delivery.NetworkHealth{State: "unhealthy", Parts: []delivery.NetworkPart{ + w.Have("novox.nats", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), Put) // the control node: the bus node + w.NodeHealth("novox", report(nodeengine.HealthByNodeValue{Node: "novox", Network: &nodeengine.NetworkHealth{State: "unhealthy", Parts: []nodeengine.NetworkPart{ {Part: "resolv-conf", State: "unhealthy", Writer: "NetworkManager", Owner: "systemd-resolved", Said: "rewritten"}, {Part: "names", State: "unhealthy", Reason: "names do not resolve.", Said: "x"}, {Part: "route", State: "unhealthy", Reason: "no default route", Said: "y"}}}, - Units: &delivery.UnitsHealth{State: "degraded", Failed: []delivery.FailedUnit{{Unit: "cups.service", Scope: "system", Result: "exit-code"}}}, - Relogin: []delivery.NodeAccount{{Account: "jochen", Module: "docker", Reason: "a group"}}, - Found: []delivery.NodeDirectory{{Module: "photos", Resource: "data", Path: "/srv/photos", Reason: "owner differs"}}, - Fallback: &delivery.LoginFallback{Node: "novox", From: "node.novox--b", Why: "refused twice", At: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)}}), Put) + Units: &nodeengine.UnitsHealth{State: "degraded", Failed: []nodeengine.FailedUnit{{Unit: "cups.service", Scope: "system", Result: "exit-code"}}}, + Relogin: []nodeengine.NodeAccount{{Account: "jochen", Module: "docker", Reason: "a group"}}, + Found: []nodeengine.NodeDirectory{{Module: "photos", Resource: "data", Path: "/srv/photos", Reason: "owner differs"}}, + Fallback: &nodeengine.LoginFallback{Node: "novox", From: "node.novox--b", Why: "refused twice", At: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)}}), Put) for key, sev := range map[string]string{"machine.novox.network": Urgent, "machine.novox.systemd-resolved.rewritten": Urgent, "machine.novox.units": Warning, "module.docker.novox.relogin-needed": Warning, "module.photos.novox.directory-used-as-found": Warning, "machine.novox.node-account-fallback": Warning} { @@ -512,12 +517,12 @@ func TestTheWatchmansFindings(t *testing.T) { if f := o.findings["machine.novox.network"]; strings.Contains(f.Summary, "names") { t.Errorf("the names failing through the rewritten file are said twice: %s", f.Summary) } - w.NodeHealth("novox", report(delivery.NodeHealth{Node: "novox"}), Put) // all well again + w.NodeHealth("novox", report(nodeengine.HealthByNodeValue{Node: "novox"}), Put) // all well again }, map[string]string{}}, {"774: a failing part is held at the machines it points at, as the controller holds it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { toward := func(node, part string, targets ...string) []byte { - b, _ := delivery.EncodeNodeHealth(delivery.NodeHealth{Node: node, Network: &delivery.NetworkHealth{State: "unhealthy", - Parts: []delivery.NetworkPart{{Part: part, State: "unhealthy", Reason: part + " fails", Said: "x", Targets: targets}}}}) + b, _ := nodeengineclient.EncodeHealth(nodeengine.HealthByNodeValue{Node: node, Network: &nodeengine.NetworkHealth{State: "unhealthy", + Parts: []nodeengine.NetworkPart{{Part: part, State: "unhealthy", Reason: part + " fails", Said: "x", Targets: targets}}}}) return b } // own part: one machine alone failing toward a healthy one is its own. @@ -555,11 +560,11 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{}}, {"774: a node two nodes' tunnels no longer see is unreachable, said once there; not while it is silent", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { for node, key := range map[string]string{"ace": "kA", "g14": "kG", "lap": "kL"} { - b, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, TunnelKey: key}}) + b, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: node, TunnelKey: key}}) w.Have(node+".node", b, Put) } peers := func(from string, seen bool) []byte { - b, _ := delivery.EncodeNodeHealth(delivery.NodeHealth{Node: from, Peers: []delivery.PeerSeen{{Key: "kL", Seen: seen}}}) + b, _ := nodeengineclient.EncodeHealth(nodeengine.HealthByNodeValue{Node: from, Peers: []nodeengine.PeerSeen{{Key: "kL", Seen: seen}}}) return b } w.NodeHealth("ace", peers("ace", false), Put) @@ -580,21 +585,21 @@ func TestTheWatchmansFindings(t *testing.T) { w.NodeHealth("g14", peers("g14", true), Put) }, map[string]string{}}, {"WM3: a have removed ends its findings", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.Have("ace.mail", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "x"), Put) + w.Have("ace.mail", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "x"), Put) expire(w, c, signalKey("unhealthy-confirm", "mail.ace"), UnhealthyConfirmAfter) - w.Have("ace.mail", haveOf(t, delivery.PhaseRemoved, delivery.HealthUnknown, ""), Put) + w.Have("ace.mail", haveOf(t, nodeengineclient.PhaseRemoved, nodeengineclient.HealthUnknown, ""), Put) }, map[string]string{}}, {"WM2 S1: a control node silent is urgent after half an hour", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { // 1126 (ADR 0590): the control node is the bus node, the one whose have holds the bus module; a have of the // ended controller names none. - w.Have("g14.mesh-controller", haveOf(t, delivery.PhaseRunning, delivery.HealthOK, ""), Put) + w.Have("g14.mesh-controller", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), Put) w.Connect("node.g14") w.Disconnect("node.g14") expire(w, c, pulseKeyOf(t, presenceNS, "g14"), 120*time.Second) if _, ok := w.signals[signalKey("S1", "g14")]; ok { t.Fatalf("a node running the controller's have is held a control node") } - w.Have("novox.nats", haveOf(t, delivery.PhaseRunning, delivery.HealthOK, ""), Put) + w.Have("novox.nats", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), Put) w.Connect("node.novox") w.Disconnect("node.novox") expire(w, c, pulseKeyOf(t, presenceNS, "novox"), 120*time.Second) @@ -638,7 +643,7 @@ func TestTheWatchmansFindings(t *testing.T) { } }, map[string]string{}}, {"WM4: resync after a cleared bucket writes the same findings", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.Have("ace.mail", haveOf(t, delivery.PhaseRunning, delivery.HealthFailed, "x"), Put) + w.Have("ace.mail", haveOf(t, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "x"), Put) expire(w, c, signalKey("unhealthy-confirm", "mail.ace"), UnhealthyConfirmAfter) o.findings = map[string]Finding{} if n := w.Rewrite(); n != 1 { @@ -667,26 +672,26 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{"bus.SEAT_X.y_worker.max-deliveries": Warning, "bus.DEAD_LETTER_NOTICES.controller.notices-waiting": Warning}}, {"ADR 0603 §8: a want held by a rule, a provision asked and a build queued behind a package each raise a finding naming the wait past their bound, and clear when it ends", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.SetStepBound(30 * time.Minute) - held, _ := delivery.EncodeWant(delivery.Want{FP: fpOld, Self: true, Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace does not run it yet"}) - released, _ := delivery.EncodeWant(delivery.Want{FP: fpNew, Self: true}) + held, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fpOld, Self: true, Newest: fpNew, Rule: meshwantsclient.RuleCanary, Words: "the canary ace does not run it yet"}) + released, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fpNew, Self: true}) asked, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Asked, At: t0}) provided, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Provided, At: t0}) - bkey, _ := delivery.BuilderBuildKey("web", fpNew) - queued, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "mesh-sdk@v0.1.14"}) - building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "n1"}) + bkey, _ := builds.BuilderBuildKey("web", fpNew) + queued, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildQueued, Waits: "mesh-sdk@v0.1.14"}) + building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "n1"}) w.Have("ace.web", haveValue(t, fpOld), Put) // the node runs what the held want names: the hold is still the wait w.Want("want.ace.web", held, Put, t0) w.Provision("ace.photos.oidc", asked, Put) w.Build(bkey, queued, Put, t0) - waits := map[string]string{delivery.AssignerWantsBucket: "want.ace.web", provisions.ProvisionsBucket: "ace.photos.oidc", delivery.BuilderBuildsBucket: bkey} + waits := map[string]string{AssignerWantsBucket: "want.ace.web", provisions.ProvisionsBucket: "ace.photos.oidc", BuilderBuildsBucket: bkey} if len(o.pulses) != 3 || len(o.findings) != 0 { t.Fatalf("pulses %v findings %v", o.pulses, o.findings) } // The keys are written again as their parts move on, with new words and a new time: the wait still began at t0. c.t = c.t.Add(20 * time.Minute) - held2, _ := delivery.EncodeWant(delivery.Want{FP: fpOld, Self: true, Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace runs it, not healthy yet"}) + held2, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fpOld, Self: true, Newest: fpNew, Rule: meshwantsclient.RuleCanary, Words: "the canary ace runs it, not healthy yet"}) asked2, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Asked, Why: "the provider is applying", At: c.t}) w.Want("want.ace.web", held2, Put, c.t) diff --git a/modules/mesh-delivery-watchman/internal/watch/mirror.go b/modules/mesh-delivery-watchman/internal/watch/mirror.go index 76fd2cf5..532583a5 100644 --- a/modules/mesh-delivery-watchman/internal/watch/mirror.go +++ b/modules/mesh-delivery-watchman/internal/watch/mirror.go @@ -4,13 +4,19 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // A build ask taken by a node is a move (novox/hq ADR 0525 §4, issues 1887 A8b, 1908, 1909): the node writes the ask's step -// key `.` running in the build module's steps (delivery.StepsBucket) when it takes the ask, and writes it passed, +// key `.` running in the build module's steps (StepsBucket) when it takes the ask, and writes it passed, // failed or refused when the ask ends. The pulse of the move is the pulse of the bucket named as the seat's stream -// (delivery.AskStream), under the ask's id, and it begins when the node started the step. +// (buildstep.AskStream), under the ask's id, and it begins when the node started the step. // // **An ask still waiting in the stream is a backlog, not a stall** (issue 1908): the rebuild after the switch puts about 170 // asks on a few nodes' slots, and most wait for hours with every worker healthy. No pulse is made for a waiting ask, so none @@ -22,12 +28,12 @@ import ( // stand, then each change). A running step makes the pulse of its ask, from the step's own start; a step that ended, or a // key deleted, ends it. The node's `paused.` key, a key that is no step key and a value that cannot be read are left. func (w *Watchman) Step(language, key string, value []byte, op Op) { - bucket, err := delivery.AskStream(language) + bucket, err := buildstep.AskStream(language) if err != nil { w.say("no pulse for the step %s: %v", key, err) return } - node, id, err := delivery.ParseStepKey(key) + node, id, err := buildstep.ParseStepKey(key) if err != nil { return } @@ -41,12 +47,12 @@ func (w *Watchman) Step(language, key string, value []byte, op Op) { return } w.stands(bucket, id) - step, err := delivery.DecodeStep(value) + step, err := buildstep.DecodeStep(value) if err != nil { w.say("the step %s cannot be read, and is left: %v", key, err) return } - if step.State != delivery.StepRunning { + if step.State != buildstep.StepRunning { w.moved(bucket, id) return } @@ -67,7 +73,7 @@ func (w *Watchman) Step(language, key string, value []byte, op Op) { // Want is a change of a key of the assigner's wants bucket, read from the watch (at the start the keys that stand, then each // change), at the time the entry was written. func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { - node, module, err := delivery.ParseWantKey(key) + node, module, err := meshwantsclient.ParseWantKey(key) if err != nil { return } @@ -79,11 +85,11 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { case Delete: delete(w.wants, key) delete(w.held, key) - w.moved(delivery.AssignerWantsBucket, key) + w.moved(AssignerWantsBucket, key) return } - w.stands(delivery.AssignerWantsBucket, key) - want, err := delivery.DecodeWant(value) + w.stands(AssignerWantsBucket, key) + want, err := meshwantsclient.DecodeWant(value) if err != nil { w.say("the want %s cannot be read, and is left: %v", key, err) return @@ -91,10 +97,10 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { if want.Removed { delete(w.wants, key) delete(w.held, key) - w.moved(delivery.AssignerWantsBucket, key) + w.moved(AssignerWantsBucket, key) return } - w.wants[key] = want.FP + w.wants[key] = want.Fp if want.Newest != "" || want.Rule != 0 { // A want held by a rule (ADR 0603 §8) waits on the rule, not on the node: its wait is the hold's, from the time the // assigner wrote it, and the node running what the want names does not end it. @@ -102,27 +108,27 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { at = w.now() } w.held[key] = true - w.mirrorKept(delivery.AssignerWantsBucket, key, - describe("the hold on %s on %s at %s (rule %d) to end, bound %s: %s", module, node, shortFP(want.FP), want.Rule, w.stepBound, want.Words), - "held|"+want.FP+"|"+want.Newest, at) + w.mirrorKept(AssignerWantsBucket, key, + describe("the hold on %s on %s at %s (rule %d) to end, bound %s: %s", module, node, shortFP(want.Fp), want.Rule, w.stepBound, want.Words), + "held|"+want.Fp+"|"+want.Newest, at) return } if w.held[key] { delete(w.held, key) - if pk, err := delivery.PulseKey(delivery.AssignerWantsBucket, key); err == nil { + if pk, err := meshwatchdogsclient.PulseKey(AssignerWantsBucket, key); err == nil { delete(w.starts, pk) } - w.stuckMoved(delivery.AssignerWantsBucket, key) // the hold ended: its finding is cleared + w.stuckMoved(AssignerWantsBucket, key) // the hold ended: its finding is cleared } - if w.have(node, module) == want.FP { - w.moved(delivery.AssignerWantsBucket, key) + if w.have(node, module) == want.Fp { + w.moved(AssignerWantsBucket, key) return } if at.IsZero() { at = w.now() } - w.mirrorWithin(w.stepBound, delivery.AssignerWantsBucket, key, - describe("the node agent of %s to run %s at %s", node, module, shortFP(want.FP)), want.FP, at, 0) + w.mirrorWithin(w.stepBound, AssignerWantsBucket, key, + describe("the node agent of %s to run %s at %s", node, module, shortFP(want.Fp)), want.Fp, at, 0) } // A build key queued behind a package is a wait of the builder's (ADR 0603 §8, ADR 0500 clause 6): its Waits names the package @@ -133,7 +139,7 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { // Build is a change of a key of the builder's builds bucket, read from the watch (at the start the keys that stand, then // each change), at the time the entry was written. func (w *Watchman) Build(key string, value []byte, op Op, at time.Time) { - module, fp, err := delivery.ParseBuilderBuildKey(key) + module, fp, err := builds.ParseBuilderBuildKey(key) if err != nil { return } @@ -143,30 +149,30 @@ func (w *Watchman) Build(key string, value []byte, op Op, at time.Time) { return } if op == Delete { - w.moved(delivery.BuilderBuildsBucket, key) + w.moved(BuilderBuildsBucket, key) return } - w.stands(delivery.BuilderBuildsBucket, key) - b, err := delivery.DecodeBuilderBuild(value) + w.stands(BuilderBuildsBucket, key) + b, err := builds.DecodeBuilderBuild(value) if err != nil { w.say("the build %s cannot be read, and is left: %v", key, err) return } - if b.State != delivery.BuildQueued || b.Waits == "" { - w.moved(delivery.BuilderBuildsBucket, key) + if b.State != builds.BuildQueued || b.Waits == "" { + w.moved(BuilderBuildsBucket, key) return } if at.IsZero() { at = w.now() } - w.mirrorKept(delivery.BuilderBuildsBucket, key, + w.mirrorKept(BuilderBuildsBucket, key, describe("the package %s to be published before the build of %s at %s, bound %s", b.Waits, module, shortFP(fp), w.stepBound), b.Waits, at) } // Have is a change of a key of the node-engine's have bucket: the fingerprint a node runs of a module. One that names the // fingerprint of the node's want for the module ends the want's pulse. A node's own key and a key deleted are left. func (w *Watchman) Have(key string, value []byte, op Op) { - node, module, isNode, err := delivery.ParseHaveKey(key) + node, module, isNode, err := nodeengineclient.ParseHaveKey(key) if err != nil { return } @@ -174,9 +180,9 @@ func (w *Watchman) Have(key string, value []byte, op Op) { defer w.mu.Unlock() if isNode { // The node's own record: its tunnel key, whose peers' sight of it is network-unreachable. - var record *delivery.NodeRecord + var record *nodeengine.NodeRecord if op == Put { - if hn, err := delivery.DecodeHaveNode(value); err == nil { + if hn, err := nodeengineclient.DecodeHaveNode(value); err == nil { record = hn.Record } } @@ -191,24 +197,24 @@ func (w *Watchman) Have(key string, value []byte, op Op) { w.haveFindings(key, node, module, nil) return } - have, err := delivery.DecodeHave(value) + have, err := nodeengineclient.DecodeHave(value) if err != nil { w.say("the have %s cannot be read, and is left: %v", key, err) return } - w.haves[key] = have.FP + w.haves[key] = have.Fp w.haveFindings(key, node, module, &have) - wk, err := delivery.WantKey(node, module) + wk, err := meshwantsclient.WantKey(node, module) if err != nil { return } - if fp, ok := w.wants[wk]; ok && fp == have.FP { - w.moved(delivery.AssignerWantsBucket, wk) + if fp, ok := w.wants[wk]; ok && fp == have.Fp { + w.moved(AssignerWantsBucket, wk) } } func (w *Watchman) have(node, module string) string { - key, err := delivery.HaveKey(node, module) + key, err := nodeengineclient.HaveKey(node, module) if err != nil { return "" } @@ -216,7 +222,7 @@ func (w *Watchman) have(node, module string) string { } func shortFP(fp string) string { - hex := strings.TrimPrefix(fp, delivery.SourcePrefix2) + hex := strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2) if len(hex) > 12 { hex = hex[:12] } diff --git a/modules/mesh-delivery-watchman/internal/watch/names.go b/modules/mesh-delivery-watchman/internal/watch/names.go new file mode 100644 index 00000000..8d547735 --- /dev/null +++ b/modules/mesh-delivery-watchman/internal/watch/names.go @@ -0,0 +1,40 @@ +package watch + +import ( + meshwatchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" +) + +// The holders and buckets the watchman names, taken from its own manifest (module.json: its name, its `reads`, its +// states and the tools it invokes) and the seats' state names; a unit row checks them against it (TestManifestStateAndReads). +// No contract names a holder (novox/hq ADR 0625 §4, ADR 0634 §1). +const ( + WatchmanModule = "mesh-delivery-watchman" + AssignerModule = "mesh-delivery-assigner" + builderModule = "mesh-delivery-builder" + + // The watchman's own states of the seat mesh-watchdogs. + PresenceBucket = WatchmanModule + "_" + meshwatchdogs.StatePresence + PulseBucket = WatchmanModule + "_" + meshwatchdogs.StatePulse + StuckBucket = WatchmanModule + "_" + meshwatchdogs.StateStuck + + // HaveBucket and NodeHealthBucket are the node-engine's states have and health (reads: node-engine.have, mesh-host.health). + HaveBucket = "node-engine_have" + NodeHealthBucket = "mesh-host_health" + // BuilderBuildsBucket is the builder's state builds (reads: mesh-delivery-builder.builds). + BuilderBuildsBucket = builderModule + "_builds" + // AssignerWantsBucket is the assigner's state wants, followed by its views family (consumes: mesh-delivery-assigner.views.wants). + AssignerWantsBucket = AssignerModule + "_wants" + // ModulesBucket is the indexer's state modules, followed by its views family (consumes: mesh-delivery-indexer.views.modules). + ModulesBucket = meshcatalogclient.Module + "_modules" +) + +// The findings a value of another part is raised as, named by their words. +const ( + FindingManifestOutgrown = "manifest-outgrown" + FindingManifestWaits = "manifest-waits" + FindingNodeAccountFallback = "node-account-fallback" +) + +// StepsBucket is a build module's state steps (reads: mesh-build-go.steps, mesh-build-image.steps, mesh-build-node.steps). +func StepsBucket(module string) string { return module + "_steps" } diff --git a/modules/mesh-delivery-watchman/internal/watch/nodehealth.go b/modules/mesh-delivery-watchman/internal/watch/nodehealth.go index eda9df49..a53a3d8a 100644 --- a/modules/mesh-delivery-watchman/internal/watch/nodehealth.go +++ b/modules/mesh-delivery-watchman/internal/watch/nodehealth.go @@ -8,7 +8,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ) // A node's own health report (novox/hq ADR 0241, issue 315, the operator's alert table of 2026-10-29): each node's engine @@ -38,7 +39,7 @@ const ( KindMachineUnits = "machine-units" KindReloginNeeded = "relogin-needed" KindUsedAsFound = "directory-used-as-found" - KindAccountFallback = delivery.NodeAccountFallbackFinding + KindAccountFallback = FindingNodeAccountFallback ) // The parts and states a report names (mesh-control internal/link). @@ -57,7 +58,7 @@ func (w *Watchman) NodeHealth(node string, value []byte, op Op) { w.mu.Lock() defer w.mu.Unlock() if op == Put { - h, err := delivery.DecodeNodeHealth(value) + h, err := nodeengineclient.DecodeHealth(value) if err != nil { w.say("the health report of %s cannot be read, and is left: %v", node, err) return @@ -71,7 +72,7 @@ func (w *Watchman) NodeHealth(node string, value []byte, op Op) { } // TunnelKey is a node's own tunnel key, from its record in its have: whose peers see it is read by it. -func (w *Watchman) tunnelKey(node string, record *delivery.NodeRecord) { +func (w *Watchman) tunnelKey(node string, record *nodeengine.NodeRecord) { key := "" if record != nil { key = record.TunnelKey @@ -125,7 +126,7 @@ func (w *Watchman) ownsHealthKey(key, node string) bool { return f.Kind != KindNetworkUnreachable && f.Kind != KindMachineNetwork && f.Kind != KindNetworkRewritten } -func (w *Watchman) rewrittenOf(m string, p delivery.NetworkPart, all []delivery.NetworkPart) Finding { +func (w *Watchman) rewrittenOf(m string, p nodeengine.NetworkPart, all []nodeengine.NetworkPart) Finding { writer := "" if p.Writer != "" { writer = " (" + p.Writer + ")" @@ -156,7 +157,7 @@ func (w *Watchman) rewrittenOf(m string, p delivery.NetworkPart, all []delivery. } // unitsOf is a machine's failed units no module places, one finding. -func unitsOf(node string, u *delivery.UnitsHealth) (Finding, bool) { +func unitsOf(node string, u *nodeengine.UnitsHealth) (Finding, bool) { if u == nil || len(u.Failed) == 0 { return Finding{}, false } @@ -202,8 +203,8 @@ func unitsOf(node string, u *delivery.UnitsHealth) (Finding, bool) { } // reloginOf is one finding per module whose accounts wait for a new login. -func reloginOf(node string, accounts []delivery.NodeAccount) []Finding { - by := map[string][]delivery.NodeAccount{} +func reloginOf(node string, accounts []nodeengine.NodeAccount) []Finding { + by := map[string][]nodeengine.NodeAccount{} for _, a := range accounts { by[a.Module] = append(by[a.Module], a) } @@ -227,8 +228,8 @@ func reloginOf(node string, accounts []delivery.NodeAccount) []Finding { } // foundOf is one finding per module with a directory used as found. -func foundOf(node string, dirs []delivery.NodeDirectory) []Finding { - by := map[string][]delivery.NodeDirectory{} +func foundOf(node string, dirs []nodeengine.NodeDirectory) []Finding { + by := map[string][]nodeengine.NodeDirectory{} for _, d := range dirs { by[d.Module] = append(by[d.Module], d) } @@ -257,7 +258,7 @@ func foundOf(node string, dirs []delivery.NodeDirectory) []Finding { } // fallbackOf is a node's fall back to its old login during a rotation of its account (ADR 0583 §6), one finding. -func fallbackOf(node string, f *delivery.LoginFallback) (Finding, bool) { +func fallbackOf(node string, f *nodeengine.LoginFallback) (Finding, bool) { if f == nil || f.Node != node { return Finding{}, false } @@ -279,19 +280,19 @@ func orModule(m string) string { // pointed is one machine's failing part that points at another machine. type pointed struct { from string - part delivery.NetworkPart + part nodeengine.NetworkPart } // networks is every machine's network findings from every report together (the controller's networkObservations): the // resolver file rewritten, each machine's own unhealthy parts, and the machine others point at, held there. func (w *Watchman) networks() { machines := slices.Sorted(maps.Keys(w.health)) - unhealthy := func(m string) []delivery.NetworkPart { + unhealthy := func(m string) []nodeengine.NetworkPart { h := w.health[m] if h.Network == nil { return nil } - var out []delivery.NetworkPart + var out []nodeengine.NetworkPart for _, p := range h.Network.Parts { if p.State == stateUnhealthy { out = append(out, p) @@ -299,7 +300,7 @@ func (w *Watchman) networks() { } return out } - targets := func(m string, p delivery.NetworkPart) ([]string, bool) { + targets := func(m string, p nodeengine.NetworkPart) ([]string, bool) { var out []string for _, x := range p.Targets { if x == "" || x == m { @@ -329,7 +330,7 @@ func (w *Watchman) networks() { for _, m := range machines { for _, p := range w.health[m].Peers { if x, ok := byKey[p.Key]; ok && x != m && !p.Seen { - pointing[x] = append(pointing[x], pointed{from: m, part: delivery.NetworkPart{Part: partTunnel, State: stateUnhealthy, + pointing[x] = append(pointing[x], pointed{from: m, part: nodeengine.NetworkPart{Part: partTunnel, State: stateUnhealthy, Said: "no handshake with " + x + " within five minutes", Targets: []string{x}}}) } } @@ -364,8 +365,8 @@ func (w *Watchman) networks() { if len(parts) == 0 { continue } - var own []delivery.NetworkPart - var rewritten *delivery.NetworkPart + var own []nodeengine.NetworkPart + var rewritten *nodeengine.NetworkPart for i, p := range parts { if p.Part == partResolvConf { rewritten = &parts[i] diff --git a/modules/mesh-delivery-watchman/internal/watch/presence.go b/modules/mesh-delivery-watchman/internal/watch/presence.go index 369e73c4..5fe16e07 100644 --- a/modules/mesh-delivery-watchman/internal/watch/presence.go +++ b/modules/mesh-delivery-watchman/internal/watch/presence.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" ) // partPrefix is what the watchman keeps presence of among the module users: the parts of delivery. @@ -21,7 +21,7 @@ func presenceOf(user string) (key, waited string, ok bool) { return "", "", false } if first == "node" { - k, err := delivery.PresenceNodeKey(rest) + k, err := meshwatchdogsclient.PresenceNodeKey(rest) if err != nil { return "", "", false } @@ -30,7 +30,7 @@ func presenceOf(user string) (key, waited string, ok bool) { if !strings.HasPrefix(rest, partPrefix) { return "", "", false } - k, err := delivery.PresencePartKey(rest, first) + k, err := meshwatchdogsclient.PresencePartKey(rest, first) if err != nil { return "", "", false } @@ -39,7 +39,7 @@ func presenceOf(user string) (key, waited string, ok bool) { // waitedOf is the words a presence key's return is waited in. func waitedOf(key string) string { - node, part, instance, err := delivery.ParsePresenceKey(key) + node, part, instance, err := meshwatchdogsclient.ParsePresenceKey(key) switch { case err != nil: return key + " to come back online" @@ -87,11 +87,11 @@ func (w *Watchman) settle(key string) { return } w.presence[key] = presence{online: online, since: w.now()} - if node, part, _, err := delivery.ParsePresenceKey(key); err == nil && part == "" && node != "" { + if node, part, _, err := meshwatchdogsclient.ParsePresenceKey(key); err == nil && part == "" && node != "" { defer w.applyCheck(node) // S21 is judged only while the machine is heard } if online { - w.moved(delivery.PresenceBucket, key) + w.moved(PresenceBucket, key) return } w.ensureOffline(key) @@ -100,7 +100,7 @@ func (w *Watchman) settle(key string) { // ensureOffline mirrors the return of a node or part that is offline. func (w *Watchman) ensureOffline(key string) { p := w.presence[key] - w.mirror(delivery.PresenceBucket, key, waitedOf(key), "", p.since, 0) + w.mirror(PresenceBucket, key, waitedOf(key), "", p.since, 0) } // SeedPresence tells the watchman a presence key as it stands in its bucket at start, with the time it was written. diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index 3874b062..1faad419 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -8,7 +8,9 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" "git.novox.be/novox/mesh-sdk/go/provisions" ) @@ -99,7 +101,7 @@ func (w *Watchman) signal(row, id string, ttl time.Duration, due signalDue) { w.say("no signal %s: %v", pk, err) return } - value, err := delivery.EncodePulse(delivery.Pulse{Bucket: signalBucket, Key: strings.TrimPrefix(pk, signalBucket+"."), Owes: string(owes)}) + value, err := meshwatchdogsclient.EncodePulse(meshwatchdogsclient.Pulse{Bucket: signalBucket, Key: strings.TrimPrefix(pk, signalBucket+"."), Owes: string(owes)}) if err != nil { w.say("no signal %s: %v", pk, err) return @@ -128,7 +130,7 @@ func (w *Watchman) unsignal(row, id string) { func (w *Watchman) SeedSignal(key string, value []byte) { w.mu.Lock() defer w.mu.Unlock() - if p, err := delivery.DecodePulse(value); err == nil && p.Bucket == signalBucket { + if p, err := meshwatchdogsclient.DecodePulse(value); err == nil && p.Bucket == signalBucket { var due signalDue if json.Unmarshal([]byte(p.Owes), &due) == nil && due.Do == dueCreated { w.busCreated[key] = true @@ -141,7 +143,7 @@ func (w *Watchman) SeedSignal(key string, value []byte) { } func dueOf(value []byte) (signalDue, bool) { - p, err := delivery.DecodePulse(value) + p, err := meshwatchdogsclient.DecodePulse(value) if err != nil || p.Bucket != signalBucket { return signalDue{}, false } @@ -189,9 +191,9 @@ func (w *Watchman) stillWrong(haveKey, kind string) bool { } switch kind { case KindApplyFailing: - return h.Phase == delivery.PhaseFailed + return h.Phase == nodeengineclient.PhaseFailed case KindUnhealthy: - return h.Health == delivery.HealthFailed + return h.Health == nodeengineclient.HealthFailed } return false } @@ -228,7 +230,7 @@ func (w *Watchman) Power(node, state string) { defer w.mu.Unlock() away := state == "sleeping" || state == "shutting-down" w.away[node] = away - key, err := delivery.PresenceNodeKey(node) + key, err := meshwatchdogsclient.PresenceNodeKey(node) if err != nil { return } @@ -248,9 +250,9 @@ func (w *Watchman) Power(node, state string) { // stuckFinding is the finding of a stuck key as it stands (ADR 0433 §2, §3, now the watchman's own): a node's presence not // moved is S1, the machine silent, unless it said it is away; any other key not moved is `delivery..stuck`; a key // moved ends its finding. -func (w *Watchman) stuckFinding(sk string, s delivery.Stuck) { - if s.Bucket == delivery.PresenceBucket { - if node, part, _, err := delivery.ParsePresenceKey(s.Key); err == nil && part == "" && node != "" { +func (w *Watchman) stuckFinding(sk string, s meshwatchdogsclient.Stuck) { + if s.Bucket == PresenceBucket { + if node, part, _, err := meshwatchdogsclient.ParsePresenceKey(s.Key); err == nil && part == "" && node != "" { key := FindingKey(ScopeMachine, node, KindSilent) defer w.networks() // a silent machine is down on the record: what points at it is held there, said by its silence if s.Moved || w.away[node] { @@ -474,12 +476,12 @@ func ToolsNode(subject string) string { // // needs-operator is not observed from the haves: a module's part that waits for the operator is a provision in the state // `waits` of the seat mesh-provisioner (Provision, ADR 0570 §7). -func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Have) { +func (w *Watchman) haveFindings(key string, node, module string, h *nodeengine.HaveByNodeModuleValue) { unhealthyKey := FindingKey(ScopeModule, module+"."+node, "unhealthy") if module == ControlNodeModule { - w.control[node] = h != nil && h.Phase != delivery.PhaseRemoved + w.control[node] = h != nil && h.Phase != nodeengineclient.PhaseRemoved } - if h == nil || h.Phase == delivery.PhaseRemoved { + if h == nil || h.Phase == nodeengineclient.PhaseRemoved { delete(w.haveState, key) delete(w.unhealthy, key) delete(w.failedSince, key) @@ -495,7 +497,7 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav w.keptFinding(key, node, module, h.Kept) // S21: a have refused or applied in part says phase failed, with the node's words in its detail. - if h.Phase == delivery.PhaseFailed { + if h.Phase == nodeengineclient.PhaseFailed { if _, ok := w.failedSince[key]; !ok { w.failedSince[key] = now } @@ -507,7 +509,7 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav // unhealthy: a have that says failed and still says it after UnhealthyConfirmAfter (ADR 0564 §4's confirmation, by time: // a have written once and then unchanged never gets a second write). The confirm signal's expiry writes the warning; a // signal already standing (kept across a restart) is not renewed, so a steady have does not push its own expiry out. - if h.Health == delivery.HealthFailed { + if h.Health == nodeengineclient.HealthFailed { if w.unhealthy[key] == 0 { w.unhealthy[key] = 1 f := Finding{Scope: ScopeModule, ID: module + "." + node, Token: "unhealthy", Kind: KindUnhealthy, Machine: node, @@ -686,7 +688,7 @@ func (w *Watchman) Provisioned(r ProvReport) { // heard says the watchman hears the machine now: online, and not said away. S21 is judged only while it is (the // controller's watchApplyFailing): S1 says silent, and asleep is not lost. func (w *Watchman) heard(node string) bool { - key, err := delivery.PresenceNodeKey(node) + key, err := meshwatchdogsclient.PresenceNodeKey(node) if err != nil || w.away[node] { return false } @@ -704,7 +706,7 @@ func (w *Watchman) applyCheck(node string) { var applied, placed []failure var since time.Time for hk, at := range w.failedSince { - n, module, _, err := delivery.ParseHaveKey(hk) + n, module, _, err := nodeengineclient.ParseHaveKey(hk) if err != nil || n != node { continue } diff --git a/modules/mesh-delivery-watchman/internal/watch/verdicts.go b/modules/mesh-delivery-watchman/internal/watch/verdicts.go index b31b648e..905db480 100644 --- a/modules/mesh-delivery-watchman/internal/watch/verdicts.go +++ b/modules/mesh-delivery-watchman/internal/watch/verdicts.go @@ -3,7 +3,7 @@ package watch import ( "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" ) // KindManifestRefused is a module whose manifest the indexer's judge refused (novox/hq ADR 0578 §1): every reader holds @@ -12,14 +12,14 @@ const KindManifestRefused = "manifest-refused" // KindManifestOutgrown is a module whose manifest the indexer accepted before and a stricter judge finds problems in // (novox/hq ADR 0579 §1): it stays accepted and runs; the warning stands until the manifest is mended. -const KindManifestOutgrown = delivery.ManifestOutgrown +const KindManifestOutgrown = FindingManifestOutgrown // KindManifestWaits is a module whose manifest carries a field the indexer's judge does not know yet (novox/hq ADR 0578 // §1, amended): every reader holds it as a refused one and keeps what runs, and the warning stands until a judge knows the // field or the manifest is mended. -const KindManifestWaits = delivery.ManifestWaits +const KindManifestWaits = FindingManifestWaits -// ModuleVerdict is a change of the indexer's value of a module (delivery.ModulesBucket): a value carrying a refusal is a +// ModuleVerdict is a change of the indexer's value of a module (ModulesBucket): a value carrying a refusal is a // finding, one that does not (accepted, or not judged yet) or a deleted key ends it. func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { w.mu.Lock() @@ -33,17 +33,17 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { w.unfind(waits) return } - m, err := delivery.DecodeModule(value) + m, err := meshcatalogclient.DecodeModule(value) if err != nil { w.say("the indexer's value of %s cannot be read, so whether its manifest is refused is left as it was: %v", module, err) return } - v := m.VerdictOf(module) - if v != nil && v.Waits() { + v := meshcatalogclient.ModuleVerdict(module, m) + if v != nil && meshcatalogclient.VerdictWaits(*v) { w.unfind(key) w.unfind(outgrown) - w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestWaits, Severity: Warning, Source: delivery.ModulesBucket, - Summary: v.Why(), + w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestWaits, Severity: Warning, Source: ModulesBucket, + Summary: meshcatalogclient.VerdictWhy(*v), Said: strings.Join(v.Unknown, "; "), Headline: "The manifest of " + module + " waits for a judge that knows its fields", Explanation: "The manifest carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet, so every " + @@ -53,13 +53,13 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { return } w.unfind(waits) - if refused, _ := delivery.TakeManifestVerdict(v); !refused { + if refused, _ := meshcatalogclient.TakeVerdict(v); !refused { w.unfind(key) if v == nil || len(v.Warnings) == 0 { w.unfind(outgrown) return } - w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestOutgrown, Severity: Warning, Source: delivery.ModulesBucket, + w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestOutgrown, Severity: Warning, Source: ModulesBucket, Summary: "the manifest of " + module + " breaks rules the indexer's judge holds it to now: " + strings.Join(v.Warnings, "; "), Said: strings.Join(v.Warnings, "; "), Headline: "The manifest of " + module + " needs mending", @@ -69,8 +69,8 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { return } w.unfind(outgrown) - w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestRefused, Severity: Warning, Source: delivery.ModulesBucket, - Summary: v.Why(), + w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestRefused, Severity: Warning, Source: ModulesBucket, + Summary: meshcatalogclient.VerdictWhy(*v), Said: strings.Join(v.Problems, "; "), Headline: "The manifest of " + module + " is refused", Explanation: "The indexer's judge refused it, so every node, the grants part and the assigner hold " + module + ": " + diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index 1b43cbd2..e73bc987 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -8,7 +8,9 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-sdk/go/keys" ) // Op is what happened to a watched key. @@ -59,7 +61,7 @@ type Watchman struct { counts map[string]int // open connections per presence key presence map[string]presence pulses map[string]*pulse // live pulses, by pulse key - stuck map[string]delivery.Stuck + stuck map[string]meshwatchdogsclient.Stuck // keptSince is, by have key, when its have first said what the node keeps of the module as it runs (keptFinding). keptSince map[string]time.Time wants map[string]string // the fingerprint of each want that stands, by want key @@ -83,7 +85,7 @@ type Watchman struct { away map[string]bool // haveState is each module's have as last read, unhealthy its writes in a row that said it failed, advised each // advisory's count while its finding stands and advisedSince since when. - haveState map[string]delivery.Have + haveState map[string]nodeengine.HaveByNodeModuleValue unhealthy map[string]int advised map[string]int advisedSince map[string]time.Time @@ -97,7 +99,7 @@ type Watchman struct { // waiting is, by `.`, each of its provisions in waits, with its words. waiting map[string]map[string]string // health is each node's own health report, and tunnels each node's tunnel key from its record. - health map[string]delivery.NodeHealth + health map[string]nodeengine.HealthByNodeValue tunnels map[string]string // stepBound is how long a build step may run once a node took the ask; a test shortens it. stepBound time.Duration @@ -115,11 +117,11 @@ type Watchman struct { // New is a watchman writing to out, reading the time from now. func New(out Out, now func() time.Time) *Watchman { return &Watchman{out: out, now: now, counts: map[string]int{}, presence: map[string]presence{}, - pulses: map[string]*pulse{}, stuck: map[string]delivery.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, starts: map[string]startMark{}, bound: delivery.PulseMoveBound, stepBound: StepBound, + pulses: map[string]*pulse{}, stuck: map[string]meshwatchdogsclient.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, starts: map[string]startMark{}, bound: meshwatchdogsclient.PulseMoveBound, stepBound: StepBound, findings: map[string][]byte{}, seededFindings: map[string]bool{}, signals: map[string]signalDue{}, busCreated: map[string]bool{}, takeInHeld: map[string]Finding{}, away: map[string]bool{}, - haveState: map[string]delivery.Have{}, unhealthy: map[string]int{}, advised: map[string]int{}, advisedSince: map[string]time.Time{}, + haveState: map[string]nodeengine.HaveByNodeModuleValue{}, unhealthy: map[string]int{}, advised: map[string]int{}, advisedSince: map[string]time.Time{}, control: map[string]bool{}, failedSince: map[string]time.Time{}, provFailed: map[string]map[string]ProvReport{}, closures: map[string]map[string]time.Time{}, waiting: map[string]map[string]string{}, - health: map[string]delivery.NodeHealth{}, tunnels: map[string]string{}, + health: map[string]nodeengine.HealthByNodeValue{}, tunnels: map[string]string{}, standing: map[string]map[string]bool{}, readWhole: map[string]bool{}, awaited: map[string]bool{}} } @@ -138,9 +140,9 @@ func (w *Watchman) mirror(bucket, key, waited, head string, since time.Time, rev w.mirrorWithin(0, bucket, key, waited, head, since, revision) } -// mirrorWithin is mirror for a move that may take bound (zero: the watchman's own bound, delivery.PulseMoveBound). +// mirrorWithin is mirror for a move that may take bound (zero: the watchman's own bound, meshwatchdogsclient.PulseMoveBound). func (w *Watchman) mirrorWithin(bound time.Duration, bucket, key, waited, head string, since time.Time, revision uint64) { - pk, err := delivery.PulseKey(bucket, key) + pk, err := meshwatchdogsclient.PulseKey(bucket, key) if err != nil { w.say("no pulse for %s %s: %v", bucket, key, err) return @@ -161,7 +163,7 @@ func (w *Watchman) mirrorWithin(bound time.Duration, bucket, key, waited, head s w.writeStuck(bucket, key, waited, since) return } - value, err := delivery.EncodePulse(delivery.Pulse{Bucket: bucket, Key: key, Revision: revision, Owes: waited}) + value, err := meshwatchdogsclient.EncodePulse(meshwatchdogsclient.Pulse{Bucket: bucket, Key: key, Revision: int64(revision), Owes: waited}) if err != nil { w.say("no pulse for %s %s: %v", bucket, key, err) return @@ -183,7 +185,7 @@ type startMark struct { // (a provision asked, a want held at the same fingerprints, a build waiting for the same package), a rewrite of the key (new // words, a new time) does not start the bound again; the earliest start is kept. A different mark is a new wait. func (w *Watchman) mirrorKept(bucket, key, waited, mark string, since time.Time) { - if pk, err := delivery.PulseKey(bucket, key); err == nil { + if pk, err := meshwatchdogsclient.PulseKey(bucket, key); err == nil { if cur, ok := w.starts[pk]; ok && cur.mark == mark && cur.since.Before(since) { since = cur.since } @@ -202,7 +204,7 @@ func (w *Watchman) deletePulse(pk string) { // moved is the mirrored key moving: its pulse is deleted, so it never expires, and a stuck key it left is written as // moved (never deleted). func (w *Watchman) moved(bucket, key string) { - pk, err := delivery.PulseKey(bucket, key) + pk, err := meshwatchdogsclient.PulseKey(bucket, key) if err != nil { return } @@ -215,7 +217,7 @@ func (w *Watchman) moved(bucket, key string) { // stuckMoved writes a stuck key of the key as moved, if it stands and is not yet. func (w *Watchman) stuckMoved(bucket, key string) { - sk, err := delivery.StuckKey(bucket, key) + sk, err := meshwatchdogsclient.StuckKey(bucket, key) if err != nil { return } @@ -228,7 +230,7 @@ func (w *Watchman) stuckMoved(bucket, key string) { } func (w *Watchman) writeStuck(bucket, key, waited string, since time.Time) { - sk, err := delivery.StuckKey(bucket, key) + sk, err := meshwatchdogsclient.StuckKey(bucket, key) if err != nil { w.say("no stuck key for %s %s: %v", bucket, key, err) return @@ -236,11 +238,11 @@ func (w *Watchman) writeStuck(bucket, key, waited string, since time.Time) { if cur, ok := w.stuck[sk]; ok && cur.Since.Equal(since) { return } - w.putStuck(sk, delivery.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: since}) + w.putStuck(sk, meshwatchdogsclient.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: since}) } -func (w *Watchman) putStuck(sk string, s delivery.Stuck) { - value, err := delivery.EncodeStuck(s) +func (w *Watchman) putStuck(sk string, s meshwatchdogsclient.Stuck) { + value, err := meshwatchdogsclient.EncodeStuck(s) if err != nil { w.say("cannot encode the stuck key %s: %v", sk, err) return @@ -249,7 +251,7 @@ func (w *Watchman) putStuck(sk string, s delivery.Stuck) { w.say("cannot write the stuck key %s: %v", sk, err) return } - s.V = delivery.Version + s.V = keys.Version w.stuck[sk] = s w.stuckFinding(sk, s) } @@ -278,7 +280,7 @@ func (w *Watchman) Pulse(key string, op Op, at time.Time) { func (w *Watchman) SeedStuck(key string, value []byte) { w.mu.Lock() defer w.mu.Unlock() - if s, err := delivery.DecodeStuck(value); err == nil { + if s, err := meshwatchdogsclient.DecodeStuck(value); err == nil { w.stuck[key] = s w.stuckFinding(key, s) } @@ -289,7 +291,7 @@ func (w *Watchman) SeedStuck(key string, value []byte) { func (w *Watchman) SeedPulse(key string) { w.mu.Lock() defer w.mu.Unlock() - bucket, k, err := delivery.ParsePulseKey(key) + bucket, k, err := meshwatchdogsclient.ParsePulseKey(key) if err != nil || bucket == signalBucket { return } @@ -351,7 +353,7 @@ func (w *Watchman) closeGone() { w.standing, w.readWhole, w.reconciled = standing, map[string]bool{}, true } -func (w *Watchman) closeStuck(sk string, s delivery.Stuck) { +func (w *Watchman) closeStuck(sk string, s meshwatchdogsclient.Stuck) { s.Moved = true w.say("the stuck key %s is closed: its key %s is no longer in %s (it aged out of the bucket or was deleted while nobody heard), so nothing waits for it since %s", sk, s.Key, s.Bucket, s.Since.UTC().Format(time.RFC3339)) @@ -444,7 +446,7 @@ func (w *Watchman) SetStepBound(d time.Duration) { w.stepBound = d } -// SetBound sets how long a move, and a running step, may take; it is delivery.PulseMoveBound unless a test shortens it. +// SetBound sets how long a move, and a running step, may take; it is meshwatchdogsclient.PulseMoveBound unless a test shortens it. func (w *Watchman) SetBound(d time.Duration) { w.mu.Lock() defer w.mu.Unlock() @@ -452,10 +454,10 @@ func (w *Watchman) SetBound(d time.Duration) { } // ttl is the time to live of a pulse for a move of the given age, and whether the move is stalled already -// (delivery.PulseTTL, with the bound this watchman keeps). +// (meshwatchdogsclient.PulseTTL, with the bound this watchman keeps). func (w *Watchman) ttl(age time.Duration) (time.Duration, bool) { - if w.bound == delivery.PulseMoveBound { - return delivery.PulseTTL(age) + if w.bound == meshwatchdogsclient.PulseMoveBound { + return meshwatchdogsclient.PulseTTL(age) } if age >= w.bound { return 0, true diff --git a/modules/mesh-delivery-watchman/internal/watch/watch_test.go b/modules/mesh-delivery-watchman/internal/watch/watch_test.go index 29c2759a..1fd555ea 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch_test.go @@ -8,23 +8,28 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) type fakeOut struct { presence map[string]bool presenceWrite int - pulses map[string]delivery.Pulse + pulses map[string]meshwatchdogsclient.Pulse ttls map[string]time.Duration pulseDeletes []string - stuck map[string]delivery.Stuck + stuck map[string]meshwatchdogsclient.Stuck stuckWrites int findings map[string]Finding findingWrites int } func newOut() *fakeOut { - return &fakeOut{presence: map[string]bool{}, pulses: map[string]delivery.Pulse{}, ttls: map[string]time.Duration{}, stuck: map[string]delivery.Stuck{}, + return &fakeOut{presence: map[string]bool{}, pulses: map[string]meshwatchdogsclient.Pulse{}, ttls: map[string]time.Duration{}, stuck: map[string]meshwatchdogsclient.Stuck{}, findings: map[string]Finding{}} } @@ -53,7 +58,7 @@ func (o *fakeOut) PutPresence(key string, online bool) error { } func (o *fakeOut) PutPulse(key string, value []byte, ttl time.Duration) error { - p, err := delivery.DecodePulse(value) + p, err := meshwatchdogsclient.DecodePulse(value) if err != nil { return err } @@ -68,7 +73,7 @@ func (o *fakeOut) DeletePulse(key string) error { } func (o *fakeOut) PutStuck(key string, value []byte) error { - s, err := delivery.DecodeStuck(value) + s, err := meshwatchdogsclient.DecodeStuck(value) if err != nil { return err } @@ -86,7 +91,7 @@ func (c *clock) now() time.Time { return c.t } func setup() (*Watchman, *fakeOut, *clock) { o, c := newOut(), &clock{t: t0} w := New(o, c.now) - w.SetStepBound(delivery.PulseMoveBound) // the older tests move a step as they move any key: stuck at the pulse's bound + w.SetStepBound(meshwatchdogsclient.PulseMoveBound) // the older tests move a step as they move any key: stuck at the pulse's bound return w, o, c } @@ -99,12 +104,12 @@ func checkValue(started time.Time, head string) []byte { const ( checksKey = "SEAT_NODE_BUILD_GO" - presenceNS = delivery.PresenceBucket + presenceNS = PresenceBucket ) // runningStep is a build module's step key as a node writes it when it takes an ask: the ask's id, running, since started. func runningStep(id string, started time.Time) []byte { - b, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_test", Node: "n1", State: delivery.StepRunning, Started: started}) + b, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: buildstep.StepRunning, Started: started}) if err != nil { panic(err) } @@ -112,7 +117,7 @@ func runningStep(id string, started time.Time) []byte { } func endedStep(id, state string, started time.Time) []byte { - b, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_test", Node: "n1", State: state, Started: started, Ended: started.Add(time.Second)}) + b, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: state, Started: started, Ended: started.Add(time.Second)}) if err != nil { panic(err) } @@ -121,7 +126,7 @@ func endedStep(id, state string, started time.Time) []byte { func pulseKeyOf(t *testing.T, bucket, key string) string { t.Helper() - k, err := delivery.PulseKey(bucket, key) + k, err := meshwatchdogsclient.PulseKey(bucket, key) if err != nil { t.Fatal(err) } @@ -130,7 +135,7 @@ func pulseKeyOf(t *testing.T, bucket, key string) string { func stuckKeyOf(t *testing.T, bucket, key string) string { t.Helper() - k, err := delivery.StuckKey(bucket, key) + k, err := meshwatchdogsclient.StuckKey(bucket, key) if err != nil { t.Fatal(err) } @@ -501,7 +506,7 @@ func TestAnAskTakenByANodeHasAPulseFromWhenItStartedAndEndsWithTheStep(t *testin if len(o.pulses) != 1 { t.Fatalf("pulses = %v, want the taken ask's", o.pulses) } - for _, state := range []string{delivery.StepPassed, delivery.StepFailed, delivery.StepRefused} { + for _, state := range []string{buildstep.StepPassed, buildstep.StepFailed, buildstep.StepRefused} { w2, o2, c2 := setup() c2.t = t0.Add(10 * time.Second) w2.Step("go", "n1.askid1", runningStep("askid1", t0), Put) @@ -573,11 +578,11 @@ func TestARunningStepPastItsBoundIsStuck(t *testing.T) { // 1908 // its fingerprint. func wantValue(t *testing.T, fp string, removed bool) []byte { t.Helper() - w := delivery.Want{FP: fp, Composed: "sha256:" + strings.Repeat("c", 64), Removed: removed} + w := meshwantsclient.Want{Fp: fp, Composed: "sha256:" + strings.Repeat("c", 64), Removed: removed} if removed { w.Composed = "" } - b, err := delivery.EncodeWant(w) + b, err := meshwantsclient.EncodeWant(w) if err != nil { t.Fatal(err) } @@ -586,7 +591,7 @@ func wantValue(t *testing.T, fp string, removed bool) []byte { func haveValue(t *testing.T, fp string) []byte { t.Helper() - b, err := delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) if err != nil { t.Fatal(err) } @@ -594,15 +599,15 @@ func haveValue(t *testing.T, fp string) []byte { } var ( - fpOld = delivery.SourcePrefix2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - fpNew = delivery.SourcePrefix2 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + fpOld = meshcatalogclient.SourcePrefix2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + fpNew = meshcatalogclient.SourcePrefix2 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ) func TestAWantIsAPulseOwedByTheNodeAndTheHaveNamingItEndsIt(t *testing.T) { w, o, c := setup() c.t = t0.Add(10 * time.Second) w.Want("want.ace.web", wantValue(t, fpNew, false), Put, t0) - pk := pulseKeyOf(t, delivery.AssignerWantsBucket, "want.ace.web") + pk := pulseKeyOf(t, AssignerWantsBucket, "want.ace.web") p, ok := o.pulses[pk] if !ok || p.Key != "want.ace.web" || !strings.Contains(p.Owes, "ace") || !strings.Contains(p.Owes, "web") { t.Fatalf("pulses = %v", o.pulses) @@ -642,10 +647,10 @@ func TestAWantNotAppliedByItsBoundIsStuckAndAKeyDeletedEndsIt(t *testing.T) { o, c := newOut(), &clock{t: t0} w := New(o, c.now) w.Want("want.ace.web", wantValue(t, fpNew, false), Put, t0) - pk := pulseKeyOf(t, delivery.AssignerWantsBucket, "want.ace.web") + pk := pulseKeyOf(t, AssignerWantsBucket, "want.ace.web") c.t = t0.Add(31 * time.Minute) w.Pulse(pk, Expired, c.t) - if s, ok := o.stuck[stuckKeyOf(t, delivery.AssignerWantsBucket, "want.ace.web")]; !ok || s.Moved || !s.Since.Equal(t0) { + if s, ok := o.stuck[stuckKeyOf(t, AssignerWantsBucket, "want.ace.web")]; !ok || s.Moved || !s.Since.Equal(t0) { t.Fatalf("stuck = %v", o.stuck) } w2, o2, c2 := setup() @@ -673,18 +678,18 @@ func TestAStuckKeyWhoseKeyIsGoneIsClosedAtTheStart(t *testing.T) { {"an ask's step aged out of the steps bucket", checksKey, "askold1", func(*Watchman) {}, true, true, false}, {"an ask's step still running", checksKey, "askold1", func(w *Watchman) { w.Step("go", "n1.askold1", runningStep("askold1", t0), Put) }, true, false, false}, {"the steps bucket could not be read", checksKey, "askold1", func(*Watchman) {}, false, false, false}, - {"a want deleted while nobody heard", delivery.AssignerWantsBucket, wantKey, func(*Watchman) {}, true, true, false}, - {"a want still standing", delivery.AssignerWantsBucket, wantKey, func(w *Watchman) { w.Want(wantKey, wantValue(t, fpNew, false), Put, t0) }, true, false, false}, + {"a want deleted while nobody heard", AssignerWantsBucket, wantKey, func(*Watchman) {}, true, true, false}, + {"a want still standing", AssignerWantsBucket, wantKey, func(w *Watchman) { w.Want(wantKey, wantValue(t, fpNew, false), Put, t0) }, true, false, false}, // DLV-M5 (ADR 0615): the wants are read from the assigner's first view, which may come after the start's end; what // they hold waits for it, and is then reconciled as at the start. - {"a want deleted while nobody heard, read from a late first view", delivery.AssignerWantsBucket, wantKey, func(*Watchman) {}, true, true, true}, - {"a want still standing, read from a late first view", delivery.AssignerWantsBucket, wantKey, func(w *Watchman) { w.Want(wantKey, wantValue(t, fpNew, false), Put, t0) }, true, false, true}, + {"a want deleted while nobody heard, read from a late first view", AssignerWantsBucket, wantKey, func(*Watchman) {}, true, true, true}, + {"a want still standing, read from a late first view", AssignerWantsBucket, wantKey, func(w *Watchman) { w.Want(wantKey, wantValue(t, fpNew, false), Put, t0) }, true, false, true}, } { t.Run(row.name, func(t *testing.T) { w, o, c := setup() c.t = t0.Add(9 * 24 * time.Hour) sk := stuckKeyOf(t, row.bucket, row.key) - v, _ := delivery.EncodeStuck(delivery.Stuck{Bucket: row.bucket, Key: row.key, Waited: "a move", Since: t0}) + v, _ := meshwatchdogsclient.EncodeStuck(meshwatchdogsclient.Stuck{Bucket: row.bucket, Key: row.key, Waited: "a move", Since: t0}) w.SeedStuck(sk, v) if _, ok := o.findings[FindingKey(ScopeDelivery, sk, KindStuck)]; !ok { t.Fatal("the seeded stuck key raised no finding") diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index 0724860c..cff47de1 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -10,6 +10,10 @@ import ( "slices" "strings" "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + + "mesh-delivery-watchman/internal/watch" ) // The controller's manifest reader is mesh-controller internal/catalogue (ParseManifest), an internal package of another @@ -130,6 +134,40 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 if m.SecretsOwner != "mesh-delivery-watchman" { t.Errorf("secrets-owner = %q", m.SecretsOwner) } + // D-11 (ADR 0634 §1): the holders and buckets the watchman names are its manifest's: a state of the seat it holds, a + // `reads` entry, a `consumes` views family or an `invokes` tool, each as "_". + // bucket is "_" of a manifest entry "." or ".views.". + bucket := func(list []string, entry string) string { + if !slices.Contains(list, entry) { + return "(not in the manifest: " + entry + ")" + } + parts := strings.Split(entry, ".") + return parts[0] + "_" + parts[len(parts)-1] + } + entry := func(list []string, e string) string { + if !slices.Contains(list, e) { + return "(not in the manifest: " + e + ")" + } + return e + } + for name, row := range map[string]struct{ got, want string }{ + "module": {watch.WatchmanModule, m.Module}, + "presence": {watch.PresenceBucket, m.Module + "_" + m.State[0].Name}, + "pulse": {watch.PulseBucket, m.Module + "_" + m.State[1].Name}, + "stuck": {watch.StuckBucket, m.Module + "_" + m.State[2].Name}, + "have": {watch.HaveBucket, bucket(m.Reads, "node-engine.have")}, + "health": {watch.NodeHealthBucket, bucket(m.Reads, "mesh-host.health")}, + "builds": {watch.BuilderBuildsBucket, bucket(m.Reads, "mesh-delivery-builder.builds")}, + "steps": {watch.StepsBucket("mesh-build-go"), bucket(m.Reads, "mesh-build-go.steps")}, + "wants": {watch.AssignerWantsBucket, bucket(m.Consumes, "mesh-delivery-assigner.views.wants")}, + "modules": {watch.ModulesBucket, bucket(m.Consumes, "mesh-delivery-indexer.views.modules")}, + "assigner": {watch.AssignerModule + ".assigner_read_wants", entry(m.Invokes, "mesh-delivery-assigner.assigner_read_wants")}, + "indexer": {meshcatalogclient.Module + ".indexer_pending", entry(m.Invokes, "mesh-delivery-indexer.indexer_pending")}, + } { + if row.got != row.want { + t.Errorf("the name for %s is %q, its manifest's is %q", name, row.got, row.want) + } + } } // The manifest is whole in its repository: the program, and the tools bundle the runtime loads (1952/WM1). diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/end.go b/modules/mesh-enrolment/cmd/mesh-enrolment/end.go index 47f045ad..3bf64a7b 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/end.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/end.go @@ -7,7 +7,6 @@ import ( "strings" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Revoke ends an enrolment at any stage (ADR 0587 §5), by a person's call: at asked it withdraws the ask; at redeeming it also @@ -126,7 +125,7 @@ func (h *Holder) reconcileLocked() error { if err != nil || !found { continue } - r, err := delivery.DecodeEnrolmentAddress(row.Value) + r, err := decodeReservation(row.Value) // An address with no ask is reserved by an issue under way: its enrolment key is written next, then the ask. if err != nil || r.Ask == "" || open[r.Node] { continue @@ -164,11 +163,7 @@ func (h *Holder) endExpired(e Enrolment) error { if err := h.unplace(e, "expired"); err != nil { return fmt.Errorf("%s, whose enrolment expired, is not taken out of placement (tried again on the next pass): %w", e.Node, err) } - body, err := delivery.EncodeEnrolmentExpiry(delivery.EnrolmentExpiry{Node: e.Node, IssuedID: delivery.EnrolmentIssuedID(e.Node, e.Issued)}) - if err != nil { - return err - } - if err := h.say(EventExpired, json.RawMessage(body), eventID(EventExpired, e)); err != nil { + if err := h.say(EventExpired, expiredEvent(e), eventID(EventExpired, e)); err != nil { return fmt.Errorf("enrolment-expired of %s cannot be said (tried again on the next pass): %w", e.Node, err) } if err := h.Vault.Drop(e.Node); err != nil { @@ -177,7 +172,7 @@ func (h *Holder) endExpired(e Enrolment) error { if row, found, err := h.Store.Get(AddressKey(e.Address)); err != nil { return fmt.Errorf("the address key %s cannot be read (tried again on the next pass): %w", e.Address, err) } else if found { - if r, err := delivery.DecodeEnrolmentAddress(row.Value); err == nil && r.Node == e.Node && r.Ask == e.Ask { + if r, err := decodeReservation(row.Value); err == nil && r.Node == e.Node && r.Ask == e.Ask { if err := h.Store.Delete(AddressKey(e.Address)); err != nil { return fmt.Errorf("the address key %s is not deleted (tried again on the next pass): %w", e.Address, err) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go index bd06efd4..1daccae9 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go @@ -3,8 +3,12 @@ package main import ( "encoding/json" "errors" + "fmt" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "reflect" "slices" "strings" @@ -29,7 +33,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { } // The node the yes placed, never confirmed, leaves placement on the enrolment's warrant before the end is said. if _, placed := r.rd["mesh-delivery-assigner.placement"]["ace.node"]; placed || len(r.added) == 0 || - !reflect.DeepEqual(r.added[len(r.added)-1].Change, delivery.PlacementProposalChange{Op: delivery.OpRemoveNode, Node: "ace"}) || r.added[len(r.added)-1].Warrant.Ask != "ask-ace" { + !reflect.DeepEqual(r.added[len(r.added)-1].Change, meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpRemoveNode, Node: "ace"}) || r.added[len(r.added)-1].Warrant.Ask != "ask-ace" { t.Errorf("revoked and still placed: %+v", r.added) } if len(r.keys()) != 0 || !slices.Equal(r.events(), []string{EventRevoked}) || len(r.acts) != 1 || !slices.Contains(r.vault.calls, "retire ace") { @@ -55,7 +59,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { t.Error("expired and still placed") } last := r.said[len(r.said)-1] - x, err := delivery.DecodeEnrolmentExpiry(last.Body) + x, err := decodeExpired(last.Body) if err != nil || last.Event != EventExpired || x.Node != "novox" || !strings.HasPrefix(x.IssuedID, "enrolment-issued.novox.") || !slices.Contains(r.vault.calls, "drop novox") || r.has(AddressKey("10.9.0.4")) { t.Errorf("said %s %s: %+v, %v; vault %v, keys %v", last.Event, last.Body, x, err, r.vault.calls, r.keys()) @@ -87,10 +91,10 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { t.Fatalf("%s: %v", c.name, err) } last := r.said[len(r.said)-1] - x, err := delivery.DecodeEnrolmentExpiry(last.Body) + x, err := decodeExpired(last.Body) _, placed := r.rd["mesh-delivery-assigner.placement"][c.node+".node"] removed := r.added[len(r.added)-1] - if err != nil || x.IssuedID != delivery.EnrolmentIssuedID(c.node, issued) || placed || removed.Change.Op != delivery.OpRemoveNode || + if err != nil || x.IssuedID != meshnodeenrollerclient.IssuedID(c.node, issued) || placed || removed.Change.Op != meshmoduleassignerclient.OpRemoveNode || removed.Change.Node != c.node || removed.Warrant.Ask != "ask-"+c.node || !slices.Contains(r.vault.calls, "drop "+c.node) || r.has(AddressKey("10.9.0.9")) { t.Errorf("%s: said %s (%v), placed %v, last placement call %+v, vault %v, keys %v", c.name, last.Body, err, placed, removed, r.vault.calls, r.keys()) } @@ -105,7 +109,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { }{ {"after a confirm stopped before the enrolment key went", func(r *rig, node string) { e, _ := r.enrolment(node) - e.Stage, e.KeysDigest = StageRedeeming, delivery.KeysDigest("pk", "ov", "se", "sv") + e.Stage, e.KeysDigest = StageRedeeming, meshnodeenrollerclient.KeysDigest("pk", "ov", "se", "sv") _, _ = r.st.Put(NodeKey(node), e, 0) r.asker.answer("ask-"+node, "yes") r.st.failDelete[NodeKey(node)] = true // the confirm stops after the address key, before the enrolment key @@ -142,7 +146,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { c.end(r, "joiner") removes := 0 for _, a := range r.added { - if a.Change.Op == delivery.OpRemoveNode { + if a.Change.Op == meshmoduleassignerclient.OpRemoveNode { removes++ } } @@ -151,7 +155,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { } after := 0 for _, a := range r.added { - if a.Change.Op == delivery.OpRemoveNode { + if a.Change.Op == meshmoduleassignerclient.OpRemoveNode { after++ } } @@ -207,3 +211,15 @@ func theDropFollowsOnlyAnEndAndNothingHoldsTheToken(t *testing.T) { t.Errorf("history %s", raw) } } + +// expired is what enrolment-expired says, read as the seat's contract has it: the enrolment, and the enrolment-issued event +// its issue time names. +type expired struct{ Node, IssuedID string } + +func decodeExpired(raw []byte) (expired, error) { + c, err := meshnodeenroller.DecodeEnrolmentsByNodeNodeValue(raw) + if err != nil || c.Issued == nil { + return expired{}, fmt.Errorf("enrolment-expired is not the seat's enrolment with its issue time: %v", err) + } + return expired{Node: c.Node, IssuedID: meshnodeenrollerclient.IssuedID(c.Node, *c.Issued)}, nil +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go index a6f76d5a..06d342b8 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go @@ -8,8 +8,8 @@ import ( "regexp" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // mesh-enrolment: the holder of the seat mesh-enrolment (novox/hq ADR 0587 §1), held once for the mesh. A Go bundle served @@ -22,7 +22,7 @@ const ( Module = "mesh-enrolment" Seat = meshnodeenroller.Seat // State is the holder's bucket of open enrolments; its keys are node. and address.
(ADR 0587 §2). - State = delivery.EnrolmentsState + State = enrolmentsState ) // The seat's six verbs and five events (ADR 0587 §1). @@ -41,20 +41,14 @@ const ( // to drop the token. A seat's event is heard as .. var EndEvents = []string{EventEnrolled, EventRefused, EventRevoked, EventExpired} -// The stages of an open enrolment (ADR 0587 §2), as the seat's contract names them (delivery.Enrolment). +// The stages of an open enrolment (ADR 0587 §2), as the seat's contract names them (Enrolment). const ( // StageAsked: the operator's Approve is asked, no token exists. - StageAsked = delivery.EnrolmentAsked + StageAsked = stageAsked // StageLive: the Approve is given, the token exists and expires on the bus with the key. - StageLive = delivery.EnrolmentLive + StageLive = stageLive // StageRedeeming: the token is spent; the key has no expiry and ends only by confirm or revoke. - StageRedeeming = delivery.EnrolmentRedeeming -) - -// Enrolment and Reservation are the seat's state contract (delivery.Enrolment, delivery.EnrolmentAddress). -type ( - Enrolment = delivery.Enrolment - Reservation = delivery.EnrolmentAddress + StageRedeeming = stageRedeeming ) const ( @@ -62,11 +56,11 @@ const ( ) // NodeKey and AddressKey are the two keys of an enrolment. -func NodeKey(node string) string { return delivery.EnrolmentNodeKey(node) } -func AddressKey(address string) string { return delivery.EnrolmentAddressKey(address) } +func NodeKey(node string) string { return nodeKey(node) } +func AddressKey(address string) string { return addressKey(address) } // EnrolUser and NodeUser are the two bus users of a node: the one it joins with, and its own. -func EnrolUser(node string) string { return delivery.EnrolmentUser(node) } +func EnrolUser(node string) string { return meshnodeenrollerclient.User(node) } func NodeUser(node string) string { return nodePrefix + node } var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`) @@ -79,15 +73,13 @@ func CheckNode(node string) error { return nil } -func decodeEnrolment(raw []byte) (Enrolment, error) { return delivery.DecodeEnrolment(raw) } - // Encode is a value of the bucket as the seat's contract writes it: an Enrolment or a Reservation checked and versioned. func Encode(v any) (json.RawMessage, error) { switch x := v.(type) { case Enrolment: - return delivery.EncodeEnrolment(x) + return encodeEnrolment(x) case Reservation: - return delivery.EncodeEnrolmentAddress(x.Node, x.Ask, x.Issued) + return encodeReservation(x.Node, x.Ask, x.Issued) } return json.Marshal(v) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go index 73b96e06..a74dcafe 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go @@ -11,9 +11,12 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" ) // Vault is the vault's side of enrolment (ADR 0587 §3, §5): the token and the node's account, and the signatures. The @@ -22,7 +25,7 @@ type Vault interface { // Token asks the vault to make the token and its hash for node, expiring at expires, and answers what the joining // machine must trust (the bus's address and fingerprint, the vault's signing key). No token is in the answer. // The token carries tunnel, the node's first tunnel built at issue (ADR 0587 §3, insight of 2026-11-02). - Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) + Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) // Sealed asks the vault for node's token sealed to the operator key, as the vault sealed it. Sealed(node string) (string, error) // Drop asks the vault to drop node's token and its hash; it answers only once both are gone. @@ -33,7 +36,7 @@ type Vault interface { Retire(node string) error // SignCertificate and SignReply sign through the vault's `sign`; the certificate's call carries every public key the node // presented, which the vault checks against the digest node-account was minted for. - SignCertificate(node string, keys delivery.JoiningKeys) (mv.SignAnswer, error) + SignCertificate(node string, keys meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) SignReply(node string, reply []byte) (mv.SignAnswer, error) // Signer is the vault's public signing key in base64, which the redeem answer names for the node to check it against its token. Signer() (string, error) @@ -98,16 +101,16 @@ func (h *Holder) say(event string, body any, id string) error { // AddNode is what the holder asks mesh-placement to add (ADR 0587 §7): the proposal's one change, a joining node with the // address the operator approved and the public halves of its keys, and the issue's warrant, which names this node and this -// address (delivery.AddNodeWarrant). +// address (meshmoduleassigner.Warrant). type AddNode struct { Why string - Change delivery.PlacementProposalChange - Warrant delivery.AddNodeWarrant + Change meshmoduleassigner.ProposalChange + Warrant meshmoduleassigner.Warrant } // place adds the node into placement, with the address the operator approved and its tunnel key, on the issue's warrant // (ADR 0587 §7; the operator's decision of 2026-11-02 01:05), unless placement holds it at that address already. -func (h *Holder) place(e Enrolment, placements map[string]delivery.Placement) error { +func (h *Holder) place(e Enrolment, placements map[string]meshmoduleassigner.Placement) error { if p, ok := placements[e.Node]; ok && p.Decided.Address == e.Address { return nil } @@ -116,8 +119,8 @@ func (h *Holder) place(e Enrolment, placements map[string]delivery.Placement) er return err } return h.AddNode(AddNode{Why: "enrolment of " + e.Node + ": " + e.Why, Warrant: w, - Change: delivery.PlacementProposalChange{Op: delivery.OpAddNode, Node: e.Node, Decided: &delivery.NodeDecided{Address: e.Address}, - Keys: &delivery.JoiningKeys{Overlay: e.OverlayKey}}}) + Change: meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpAddNode, Node: e.Node, Decided: &meshmoduleassigner.NodeDecided{Address: e.Address}, + Keys: &meshmoduleassigner.ProposalChangeKeys{Overlay: e.OverlayKey}}}) } // unplace takes a node that never confirmed off placement again, on the same warrant, when its enrolment ends revoked, refused @@ -135,22 +138,22 @@ func (h *Holder) unplace(e Enrolment, why string) error { return err } return h.AddNode(AddNode{Why: "enrolment of " + e.Node + " ended unconfirmed: " + why, Warrant: w, - Change: delivery.PlacementProposalChange{Op: delivery.OpRemoveNode, Node: e.Node}}) + Change: meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpRemoveNode, Node: e.Node}}) } -func (h *Holder) warrantOf(e Enrolment) (delivery.AddNodeWarrant, error) { +func (h *Holder) warrantOf(e Enrolment) (meshmoduleassigner.Warrant, error) { if h.AddNode == nil || h.WarrantOf == nil { - return delivery.AddNodeWarrant{}, errors.New("no route to mesh-placement") + return meshmoduleassigner.Warrant{}, errors.New("no route to mesh-placement") } warrant, err := h.WarrantOf(e.Ask) if err != nil { - return delivery.AddNodeWarrant{}, fmt.Errorf("the operator's warrant for %s cannot be read from the router: %w", e.Ask, err) + return meshmoduleassigner.Warrant{}, fmt.Errorf("the operator's warrant for %s cannot be read from the router: %w", e.Ask, err) } - return delivery.AddNodeWarrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}, nil + return meshmoduleassigner.Warrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}, nil } // checkTunnelKey says a tunnel key is a WireGuard public key, as `nox-mesh-host key` prints it: 32 bytes in standard base64 -// (as delivery.Enrolment requires it too); said here so issue names it among its findings. +// (as Enrolment requires it too); said here so issue names it among its findings. func checkTunnelKey(k string) error { raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(k)) if err != nil || len(raw) != 32 { @@ -189,7 +192,7 @@ func (h *Holder) openEnrolments() (map[string]Row, map[string]Enrolment, error) } // placements reads every node's placement, assembled from its keys. -func (h *Holder) placements() (map[string]delivery.Placement, error) { +func (h *Holder) placements() (map[string]meshmoduleassigner.Placement, error) { keys, err := h.Read.Keys(PlacementRead) if err != nil { return nil, fmt.Errorf("placement cannot be listed: %w", err) @@ -202,7 +205,7 @@ func (h *Holder) placements() (map[string]delivery.Placement, error) { held[k] = raw } } - p, err := delivery.PlacementsOf(held) + p, err := meshmoduleassignerclient.PlacementsOf(held) if len(p) == 0 && err != nil { return nil, err } @@ -215,8 +218,8 @@ func (h *Holder) placements() (map[string]delivery.Placement, error) { // that has not written one, the found tunnel's key kept in its placement), the hub's endpoint from its placement, and the // node's address on the private network. A mesh with no hub, or a hub with no key or no endpoint, has no tunnel to join // through, and that is said. -func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address, overlayKey string) (delivery.TokenTunnel, error) { - var hub *delivery.Placement +func (h *Holder) firstTunnel(placements map[string]meshmoduleassigner.Placement, address, overlayKey string) (meshnodeenrollerclient.TokenTunnel, error) { + var hub *meshmoduleassigner.Placement for _, p := range placements { if p.Decided.Hub { p := p @@ -224,12 +227,12 @@ func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address, } } if hub == nil { - return delivery.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through") + return meshnodeenrollerclient.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through") } key := "" - if k, err := delivery.HaveNodeKey(hub.Node); err == nil { + if k, err := nodeengineclient.HaveNodeKey(hub.Node); err == nil { if raw, found, err := h.Read.Get("node-engine.have", k); err == nil && found { - if hn, err := delivery.DecodeHaveNode(raw); err == nil && hn.Record != nil { + if hn, err := nodeengineclient.DecodeHaveNode(raw); err == nil && hn.Record != nil { key = hn.Record.TunnelKey } } @@ -246,12 +249,12 @@ func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address, } prefix, rerr := privateRange(placements) if len(missing) > 0 { - return delivery.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and ")) + return meshnodeenrollerclient.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and ")) } if rerr != nil { - return delivery.TokenTunnel{}, rerr + return meshnodeenrollerclient.TokenTunnel{}, rerr } - return delivery.TokenTunnel{Key: overlayKey, Address: address + "/32", Range: prefix.String(), HubKey: key, HubEndpoint: hub.Decided.Endpoint}, nil + return meshnodeenrollerclient.TokenTunnel{Key: overlayKey, Address: address + "/32", Range: prefix.String(), HubKey: key, HubEndpoint: hub.Decided.Endpoint}, nil } // hasRecord says the node already writes a record (a node of the mesh, ADR 0587 §4: "a node record ... holds it"). @@ -261,7 +264,7 @@ func (h *Holder) hasRecord(node string) bool { return false } for _, k := range keys { - if n, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode && n == node { + if n, _, isNode, err := nodeengineclient.ParseHaveKey(k); err == nil && isNode && n == node { return true } } @@ -271,7 +274,7 @@ func (h *Holder) hasRecord(node string) bool { // privateRange is the private network's range: the hub's tunnel range in its placement, as the controller's `token issue` // writes it into the token today (the adopted tunnel's range). A mesh whose hub's placement names none has no range a token // can carry, and that is said rather than guessed. -func privateRange(placements map[string]delivery.Placement) (netip.Prefix, error) { +func privateRange(placements map[string]meshmoduleassigner.Placement) (netip.Prefix, error) { for _, p := range placements { if t := p.Decided.Tunnel; p.Decided.Hub && t != nil && t.Range != "" { r, err := netip.ParsePrefix(t.Range) @@ -286,7 +289,7 @@ func privateRange(placements map[string]delivery.Placement) (netip.Prefix, error // nextAddress is the lowest address of the private network that no placement key and no reservation holds (ADR 0587 §4), // after skip. The network is the hub's range; the hub's own address and the tunnel's are never given. -func nextAddress(placements map[string]delivery.Placement, reserved map[string]bool, skip map[string]bool) (string, error) { +func nextAddress(placements map[string]meshmoduleassigner.Placement, reserved map[string]bool, skip map[string]bool) (string, error) { taken := map[string]bool{} for _, p := range placements { if a := p.Decided.Address; a != "" { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go index f2fefcf2..c5adee20 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go @@ -23,10 +23,13 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/testbus" ) @@ -145,7 +148,7 @@ type standInVault struct { ctx context.Context logins jetstream.KeyValue mu sync.Mutex - tokens map[string]delivery.Token + tokens map[string]meshnodeenrollerclient.Token calls []string } @@ -156,9 +159,9 @@ func (v *standInVault) note(c string) { } // Token makes the token as the vault does: the bus it states, its signing key, a secret, and the first tunnel it is handed. -func (v *standInVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { +func (v *standInVault) Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { v.mu.Lock() - v.tokens[node] = delivery.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel} + v.tokens[node] = meshnodeenrollerclient.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel} v.mu.Unlock() v.note("token " + node) return mv.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil @@ -172,21 +175,21 @@ func (v *standInVault) Drop(node string) error { return nil } func (v *standInVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer, error) { - raw, err := delivery.EncodeNodeLogin(delivery.NodeLogin{Node: node, Login: delivery.FirstLogin(node), Since: time.Now().UTC()}) + raw, err := meshvaultclient.EncodeNodeLogin(mv.NodeLoginsByNodeValue{Node: node, Login: meshvaultclient.FirstLogin(node), Since: time.Now().UTC()}) if err != nil { return mv.NodeAccountAnswer{}, err } if _, err := v.logins.Put(v.ctx, node, raw); err != nil { return mv.NodeAccountAnswer{}, err } - return mv.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil + return mv.NodeAccountAnswer{Node: node, Login: meshvaultclient.FirstLogin(node), Account: "account-" + node}, nil } func (v *standInVault) Retire(node string) error { return nil } -func (v *standInVault) SignCertificate(node string, _ delivery.JoiningKeys) (mv.SignAnswer, error) { - return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil +func (v *standInVault) SignCertificate(node string, _ meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) { + return mv.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil } func (v *standInVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) { - return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil + return mv.SignAnswer{Kind: meshvaultclient.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil } func (v *standInVault) Signer() (string, error) { return "vault-key", nil } @@ -220,9 +223,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { } return kv } - enrolments := mk(delivery.EnrolmentsBucket, true) + enrolments := mk(enrolmentsBucket, true) // No controller lease bucket at all: the controller ended by its unassignment (issue 1126). - logins, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false) + logins, placement := mk(nodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false) have := mk("node-engine_have", false) if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil { t.Fatal(err) @@ -249,7 +252,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { events := func() []string { heardMu.Lock(); defer heardMu.Unlock(); return slices.Clone(heard) } store := &busStore{ctx: ctx, js: js, kv: enrolments} - vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]delivery.Token{}} + vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]meshnodeenrollerclient.Token{}} asker := &fakeAsker{} h := &Holder{Store: store, Read: busReader{ctx, js}, Vault: vault, Asker: asker, AskLasts: time.Hour, Say: func(event string, body any, id string) error { @@ -259,7 +262,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { }, // mesh-placement, stood in: an add-node writes the node's key, a remove-node deletes it, each saying placement-changed. AddNode: func(a AddNode) error { - if a.Change.Op == delivery.OpRemoveNode { + if a.Change.Op == meshmoduleassignerclient.OpRemoveNode { if err := placement.Delete(ctx, a.Change.Node+".node"); err != nil { return err } @@ -371,7 +374,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { if err != nil { t.Fatal(err) } - nl, err := delivery.DecodeNodeLogin(e.Value()) + nl, err := meshvaultclient.DecodeNodeLogin(e.Value()) if err != nil || nl.Login != ans.Login { t.Fatalf("node-logins says %+v, %v, and the answer %s", nl, err, ans.Login) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go index 8f24f725..7f0a5481 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go @@ -8,10 +8,10 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Event is what each of the seat's events carries (ADR 0587 §2): the key's whole content, which holds no secret and nothing @@ -63,10 +63,10 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration findings = append(findings, err.Error()) } if lasts == 0 { - lasts = delivery.DefaultTokenLife + lasts = meshnodeenrollerclient.DefaultTokenLife } - if lasts < 0 || lasts > delivery.MostTokenLife { - findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, delivery.MostTokenLife)) + if lasts < 0 || lasts > meshnodeenrollerclient.MostTokenLife { + findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, meshnodeenrollerclient.MostTokenLife)) } placements, perr := h.placements() if perr != nil { @@ -289,7 +289,7 @@ func (h *Holder) dropAddress(e Enrolment) error { if !found { return nil } - if r, err := delivery.DecodeEnrolmentAddress(row.Value); err == nil && r.Node != e.Node { + if r, err := decodeReservation(row.Value); err == nil && r.Node != e.Node { return nil // another enrolment holds the address now } if err := h.Store.Delete(AddressKey(e.Address)); err != nil { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go index 42a41569..1d567c32 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" ) var yes = oc.Option{ID: "yes"} @@ -103,11 +104,11 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) { } // The address key names the ask and the issue, with no expiry of its own, so it outlives the enrolment key (ADR 0589 §6). if row, _, _ := r.st.Get(AddressKey(live.Address)); true { - if res, err := delivery.DecodeEnrolmentAddress(row.Value); err != nil || res.Ask != e.Ask || !res.Issued.Equal(live.Issued) { + if res, err := decodeReservation(row.Value); err != nil || res.Ask != e.Ask || !res.Issued.Equal(live.Issued) { t.Errorf("the address key %s: %+v, %v", row.Value, res, err) } } - if got, want := r.vault.tunnels["ace"], (delivery.TokenTunnel{Key: tunnelKey, Address: live.Address + "/32", Range: "10.9.0.0/24", + if got, want := r.vault.tunnels["ace"], (meshnodeenrollerclient.TokenTunnel{Key: tunnelKey, Address: live.Address + "/32", Range: "10.9.0.0/24", HubKey: "hub-tunnel-key", HubEndpoint: "192.0.2.1:51820"}); got != want { t.Errorf("the token's tunnel %+v, want %+v", got, want) } @@ -116,7 +117,7 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) { t.Errorf("the token was made before placement held the node: %v", r.vault.calls) } for _, a := range r.added { - if a.Change.Node == "ace" && (*a.Change.Keys != delivery.JoiningKeys{Overlay: tunnelKey} || a.Warrant.Ask != e.Ask || a.Change.Decided.Address != live.Address) { + if a.Change.Node == "ace" && (*a.Change.Keys != meshmoduleassigner.ProposalChangeKeys{Overlay: tunnelKey} || a.Warrant.Ask != e.Ask || a.Change.Decided.Address != live.Address) { t.Errorf("the add-node at the yes %+v", a) } } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go index deea20f6..d82cb107 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go @@ -11,9 +11,12 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" ) // memStore is the holder's bucket in memory: revisions, and the expiry each key was written with. @@ -117,8 +120,8 @@ func (r memRead) Get(state, key string) ([]byte, bool, error) { type fakeVault struct { calls []string fail map[string]bool - tunnels map[string]delivery.TokenTunnel - cert delivery.JoiningKeys // the keys the certificate's call carried + tunnels map[string]meshnodeenrollerclient.TokenTunnel + cert meshmoduleassigner.ProposalChangeKeys // the keys the certificate's call carried } func (v *fakeVault) do(what string) error { @@ -130,9 +133,9 @@ func (v *fakeVault) do(what string) error { return nil } -func (v *fakeVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { +func (v *fakeVault) Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { if v.tunnels == nil { - v.tunnels = map[string]delivery.TokenTunnel{} + v.tunnels = map[string]meshnodeenrollerclient.TokenTunnel{} } v.tunnels[node] = tunnel return mv.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node) @@ -145,13 +148,13 @@ func (v *fakeVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer, return mv.NodeAccountAnswer{Node: node, Login: "node." + node, Account: "account-" + node}, v.do("account " + node) } func (v *fakeVault) Retire(node string) error { return v.do("retire " + node) } -func (v *fakeVault) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) { +func (v *fakeVault) SignCertificate(node string, k meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) { serving := k.Serving v.cert = k - return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node) + return mv.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node) } func (v *fakeVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) { - return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig-" + delivery.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node) + return mv.SignAnswer{Kind: meshvaultclient.KindEnrolment, Node: node, Signature: "sig-" + meshvaultclient.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node) } func (v *fakeVault) Signer() (string, error) { return "vault-signer", nil } @@ -247,7 +250,7 @@ func newRig(t *testing.T) *rig { r.vault.calls = append(r.vault.calls, a.Change.Op+" "+a.Change.Node) if r.placeErr == nil { // placement writes or deletes the node as the assigner does on the warrant n := a.Change.Node - if a.Change.Op == delivery.OpRemoveNode { + if a.Change.Op == meshmoduleassignerclient.OpRemoveNode { delete(r.rd["mesh-delivery-assigner.placement"], n+".node") } else { r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}` diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go index ed8ec3b9..e7a62901 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go @@ -15,7 +15,6 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -117,8 +116,8 @@ func (r followedReader) Get(state, key string) ([]byte, bool, error) { // PlacementRead is the assigner's placement as the enrolment reads it, and PlacementTool the assigner's read tool the // manifest invokes for its first view (ADR 0615, DLV-M6). const ( - PlacementRead = delivery.AssignerModule + "." + delivery.PlacementState - PlacementTool = delivery.AssignerModule + ".assigner_read_placement" + PlacementRead = assignerModule + "." + placementState + PlacementTool = assignerModule + ".assigner_read_placement" ) // followed is the enrolment's pictures of the assigner's placement, started at once: its first view asked through @@ -126,7 +125,7 @@ const ( func followed() *views.Reader { v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { logf("["+Module+"] "+f, a...) }, Findings: views.OwnFindings{}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} - v.Follow(delivery.AssignerModule, delivery.PlacementState, PlacementTool) + v.Follow(assignerModule, placementState, PlacementTool) v.Start(context.Background()) return v } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/names.go b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go new file mode 100644 index 00000000..d5f5171b --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go @@ -0,0 +1,22 @@ +package main + +import "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" + +// The holders, states and buckets the enrolment names beside its own, taken from its manifest (module.json: its module, its +// `reads`, its state, the seats it invokes); a unit row checks them (TestTheNamesAreTheManifests). No contract names a +// holder (novox/hq ADR 0625 §4, ADR 0634 §1). +const ( + vaultModule = "mesh-vault" + assignerModule = "mesh-delivery-assigner" + + // enrolmentsState is the holder's state of open enrolments, enrolmentsBucket its bucket. + enrolmentsState = meshnodeenroller.StateEnrolments + enrolmentsBucket = Module + "_" + enrolmentsState + // signersRead is the vault's state signers as the enrolment's manifest reads it, signersKey the key holding its signing key. + signersRead = vaultModule + ".signers" + signersKey = "key" + // nodeLoginsBucket is the vault's state node-logins (the holder reads it through the grants part, not directly). + nodeLoginsBucket = vaultModule + "_node-logins" + // placementState is the assigner's state placement, which the enrolment reads (reads: through its first view). + placementState = "placement" +) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go index 7ee0b091..94376b25 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go @@ -7,7 +7,8 @@ import ( "fmt" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" ) // Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be @@ -18,55 +19,55 @@ import ( // of the keys presented, // waits for the bus node to load node., and answers the whole, signed by the vault. Called again with the same keys it // finishes what is not done and answers the same. -func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer, error) { +func (h *Holder) Redeem(caller string, a meshnodeenrollerclient.Redeem) (meshnodeenrollerclient.RedeemAnswer, error) { if err := CheckNode(a.Node); err != nil { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } if caller != EnrolUser(a.Node) { - return delivery.RedeemAnswer{}, fmt.Errorf("only %s may redeem the enrolment of %s, and the runtime proved the caller to be %q", EnrolUser(a.Node), a.Node, caller) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("only %s may redeem the enrolment of %s, and the runtime proved the caller to be %q", EnrolUser(a.Node), a.Node, caller) } pub, err := base64.StdEncoding.DecodeString(a.PublicKey) proof, perr := a.ProofBytes() if err != nil || perr != nil || len(pub) != ed25519.PublicKeySize || - !ed25519.Verify(pub, delivery.RedeemProof(a.Node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey), proof) { - return delivery.RedeemAnswer{}, errors.New("the proof does not verify against the identity key presented: the enrolment is not spent") + !ed25519.Verify(pub, meshnodeenrollerclient.RedeemProof(a.Node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey), proof) { + return meshnodeenrollerclient.RedeemAnswer{}, errors.New("the proof does not verify against the identity key presented: the enrolment is not spent") } if a.SealingKey == "" { - return delivery.RedeemAnswer{}, errors.New("no sealing key is presented, so the account cannot be sealed to the node: the enrolment is not spent") + return meshnodeenrollerclient.RedeemAnswer{}, errors.New("no sealing key is presented, so the account cannot be sealed to the node: the enrolment is not spent") } digest := a.KeysDigest() e, err := h.spend(a.Node, digest, a.OverlayKey) if err != nil { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } account, err := h.Vault.Account(e.Node, a.SealingKey, digest) if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err) } - cert, err := h.Vault.SignCertificate(e.Node, delivery.JoiningKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey}) + cert, err := h.Vault.SignCertificate(e.Node, meshmoduleassigner.ProposalChangeKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey}) if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err) } if h.Loaded != nil { loaded, err := h.Loaded(NodeUser(e.Node)) if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("whether the bus node loaded %s cannot be read (call redeem again with the same keys): %w", NodeUser(e.Node), err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("whether the bus node loaded %s cannot be read (call redeem again with the same keys): %w", NodeUser(e.Node), err) } if !loaded { - return delivery.RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node)) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node)) } } signer, err := h.Vault.Signer() if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the vault's signing key cannot be named (call redeem again with the same keys): %w", err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault's signing key cannot be named (call redeem again with the same keys): %w", err) } - out := delivery.RedeemAnswer{Node: e.Node, Login: account.Login, Account: account.Account, Certificate: cert.Certificate, Signer: signer} + out := meshnodeenrollerclient.RedeemAnswer{Node: e.Node, Login: account.Login, Account: account.Account, Certificate: cert.Certificate, Signer: signer} if out.Login == "" { out.Login = NodeUser(e.Node) } signed, err := h.Vault.SignReply(e.Node, out.SignedBytes()) if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err) } out.Signature = signed.Signature return out, nil diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go index 6edebd44..e4858f20 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go @@ -7,20 +7,20 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" ) // redeemArgs are a node's redeem call with a proof signed by the key seeded, over the keys given. -func redeemArgs(node string, seed byte, serving string) delivery.Redeem { +func redeemArgs(node string, seed byte, serving string) meshnodeenrollerclient.Redeem { return redeemArgsWith(node, seed, serving, tunnelKey) } // redeemArgsWith is redeemArgs presenting the tunnel key given. -func redeemArgsWith(node string, seed byte, serving, overlay string) delivery.Redeem { +func redeemArgsWith(node string, seed byte, serving, overlay string) meshnodeenrollerclient.Redeem { priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize)) - a := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), + a := meshnodeenrollerclient.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), OverlayKey: overlay, SealingKey: "sealing-" + node, ServingKey: serving} - a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, delivery.RedeemProof(node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey))) + a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, meshnodeenrollerclient.RedeemProof(node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey))) return a } @@ -36,7 +36,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) { cases := []struct { name string caller string - args delivery.Redeem + args meshnodeenrollerclient.Redeem setup func(r *rig) want string }{ @@ -54,11 +54,11 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) { _, _ = r.st.Put(NodeKey("ace"), e, 0) }, "waits for the operator's yes"}, {"no enrolment", "enrol.novox", redeemArgs("novox", 3, "s"), nil, "no enrolment of novox is open"}, - {"another tunnel key than issued", "enrol.ace", func() delivery.Redeem { + {"another tunnel key than issued", "enrol.ace", func() meshnodeenrollerclient.Redeem { priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) a := good a.OverlayKey = "another-tunnel-key" - a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, delivery.RedeemProof("ace", a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey))) + a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, meshnodeenrollerclient.RedeemProof("ace", a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey))) return a }(), nil, "not the one ace's enrolment was issued for"}, {"not in placement yet", "enrol.ace", good, func(r *rig) { delete(r.rd["mesh-delivery-assigner.placement"], "ace.node") }, "placement does not hold ace"}, @@ -95,7 +95,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) { t.Errorf("placement %+v, answer %+v", r.added, ans) } // The certificate's call carries every key presented, which the vault checks against the account's digest. - if r.vault.cert.Digest() != good.KeysDigest() { + if meshnodeenrollerclient.KeysDigest(r.vault.cert.Identity, r.vault.cert.Overlay, r.vault.cert.Sealing, r.vault.cert.Serving) != good.KeysDigest() { t.Errorf("the certificate was asked with %+v", r.vault.cert) } r.loaded = false diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/state.go b/modules/mesh-enrolment/cmd/mesh-enrolment/state.go new file mode 100644 index 00000000..70a3c83a --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/state.go @@ -0,0 +1,166 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/netip" + "regexp" + "strings" + "time" + + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The holder's state: the two kinds of key of `enrolments`, whose forms are the seat's contract (meshnodeenroller: +// EnrolmentsByNodeNodeValue, EnrolmentsByAddressAddressValue). The contract's decoding is strict by schema only; the rules +// of a value are the holder's, who writes it, and are kept here once (novox/hq ADR 0587 §2, ADR 0634 §2). + +// The stages of an open enrolment, the contract's `asked, live or redeeming`. +const ( + stageAsked = "asked" + stageLive = "live" + stageRedeeming = "redeeming" +) + +// Enrolment is one key `node.` of `enrolments`: the node's name, the private-network address chosen at issue, the +// why, the caller as the runtime proved it, the ask's id, the stage, when it was issued and when the token expires, and +// from redeem the digest of the public keys presented. No secret and nothing derived from the token. +type Enrolment struct { + V int `json:"v"` + Node string `json:"node"` + Address string `json:"address"` + Why string `json:"why"` + Caller string `json:"caller"` + Ask string `json:"ask,omitempty"` + Stage string `json:"stage"` + Issued time.Time `json:"issued"` + Expires time.Time `json:"expires"` + KeysDigest string `json:"keys-digest,omitempty"` + // OverlayKey is the machine's tunnel public key, base64 of 32 bytes, given at issue. + OverlayKey string `json:"overlay-key"` +} + +// Reservation is one key `address.
`: the node the address is reserved for and, from when the issue's ask is +// raised, that ask's id and the issue's time. Ask and Issued are both empty before the ask and both set from it. +type Reservation struct { + V int `json:"v"` + Node string `json:"node"` + Ask string `json:"ask,omitempty"` + Issued time.Time `json:"issued,omitzero"` +} + +var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) + +func nodeKey(node string) string { return meshnodeenroller.EnrolmentsByNodeNodeKey(node) } +func addressKey(addr string) string { return meshnodeenroller.EnrolmentsByAddressAddressKey(addr) } +func timeOf(t *time.Time) time.Time { + if t == nil { + return time.Time{} + } + return *t +} + +func (e Enrolment) check() error { + if err := CheckNode(e.Node); err != nil { + return err + } + if _, err := netip.ParseAddr(e.Address); err != nil { + return fmt.Errorf("the enrolment of %s names the address %q, which is not an address", e.Node, e.Address) + } + if strings.TrimSpace(e.Why) == "" || strings.TrimSpace(e.Caller) == "" { + return fmt.Errorf("the enrolment of %s names no why or no caller", e.Node) + } + if k, err := base64.StdEncoding.DecodeString(e.OverlayKey); err != nil || len(k) != 32 { + return fmt.Errorf("the enrolment of %s names no tunnel key of 32 bytes in base64", e.Node) + } + switch e.Stage { + case stageAsked, stageLive: + if e.KeysDigest != "" { + return fmt.Errorf("the enrolment of %s at %s names presented keys, which only a redeem gives", e.Node, e.Stage) + } + case stageRedeeming: + if !digestForm.MatchString(e.KeysDigest) { + return fmt.Errorf("the enrolment's keys digest %q is not sha256:<64 lowercase hex digits>", e.KeysDigest) + } + default: + return fmt.Errorf("the enrolment of %s is at stage %q, not asked, live or redeeming", e.Node, e.Stage) + } + if e.Issued.IsZero() || !e.Expires.After(e.Issued) { + return fmt.Errorf("the enrolment of %s names no issue or an expiry not after it", e.Node) + } + return nil +} + +// encodeEnrolment is e as JSON at the layout version; it refuses what decodeEnrolment refuses. +func encodeEnrolment(e Enrolment) ([]byte, error) { + if err := e.check(); err != nil { + return nil, err + } + e.V = keys.Version + return json.Marshal(e) +} + +// decodeEnrolment reads a key `node.` through the contract's decoding; it refuses an unknown version, a node that is +// not a name, an address that is not one, an unknown stage, presented keys before redeem or none at it, and an expiry +// not after the issue. +func decodeEnrolment(raw []byte) (Enrolment, error) { + if err := keys.CheckVersion(raw); err != nil { + return Enrolment{}, err + } + c, err := meshnodeenroller.DecodeEnrolmentsByNodeNodeValue(raw) + if err != nil { + return Enrolment{}, fmt.Errorf("the enrolment does not fit its shape: %w", err) + } + e := Enrolment{V: int(c.V), Node: c.Node, Address: c.Address, Why: c.Why, Caller: c.Caller, Ask: c.Ask, Stage: c.Stage, + Issued: timeOf(c.Issued), Expires: timeOf(c.Expires), KeysDigest: c.KeysDigest, OverlayKey: c.OverlayKey} + if err := e.check(); err != nil { + return Enrolment{}, err + } + return e, nil +} + +func (r Reservation) check() error { + if err := CheckNode(r.Node); err != nil { + return err + } + if (strings.TrimSpace(r.Ask) == "") != r.Issued.IsZero() { + return fmt.Errorf("the address reserved for %s names its ask without the issue's time, or the reverse", r.Node) + } + return nil +} + +// encodeReservation is a key `address.
` reserving it for node: with no ask and a zero issued when it is reserved, +// with the issue's ask and time once the ask is raised. +func encodeReservation(node, ask string, issued time.Time) ([]byte, error) { + r := Reservation{V: keys.Version, Node: node, Ask: ask, Issued: issued} + if err := r.check(); err != nil { + return nil, err + } + return json.Marshal(r) +} + +// decodeReservation reads a key `address.
` through the contract's decoding; it refuses no node, and an ask without +// the issue's time or the reverse. +func decodeReservation(raw []byte) (Reservation, error) { + if err := keys.CheckVersion(raw); err != nil { + return Reservation{}, err + } + c, err := meshnodeenroller.DecodeEnrolmentsByAddressAddressValue(raw) + if err != nil { + return Reservation{}, fmt.Errorf("the reservation does not fit its shape: %w", err) + } + r := Reservation{V: int(c.V), Node: c.Node, Ask: c.Ask, Issued: timeOf(c.Issued)} + if err := r.check(); err != nil { + return Reservation{}, err + } + return r, nil +} + +// expiredEvent is the body of enrolment-expired, the contract's: the enrolment as it stood. The enrolment-issued event it +// ends is named by the issue time (the client's IssuedID). +func expiredEvent(e Enrolment) meshnodeenroller.EnrolmentExpiredEvent { + issued := e.Issued + return meshnodeenroller.EnrolmentExpiredEvent{Node: e.Node, Address: e.Address, Ask: e.Ask, Stage: stageLive, Issued: &issued, V: keys.Version} +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go index 5141f2af..7300b786 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" ) // seed writes an open enrolment of node at stage, with its address key, as the holder would have. @@ -17,7 +17,7 @@ func (r *rig) seed(node, address string, stage string) { e := Enrolment{Node: node, Address: address, Why: "a new machine", Caller: "person.operator", Ask: "ask-" + node, Stage: stage, Issued: r.now.Add(-time.Minute), Expires: r.now.Add(time.Hour), OverlayKey: tunnelKey} if stage == StageRedeeming { - e.KeysDigest = delivery.KeysDigest("pk", "ov", "se", "sv") + e.KeysDigest = meshnodeenrollerclient.KeysDigest("pk", "ov", "se", "sv") } _, _ = r.st.Put(NodeKey(node), e, 0) _, _ = r.st.Put(AddressKey(address), Reservation{Node: node, Ask: e.Ask, Issued: e.Issued}, 0) @@ -107,11 +107,19 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) { if !slices.Contains(m.Invokes, PlacementTool) || !slices.Equal(m.Tools, []string{"enrolment_first_views", "enrolment_first_view_again"}) { t.Errorf("invokes %v, tools %v: want %s and the first-view tools", m.Invokes, m.Tools, PlacementTool) } + // The holders the holder names beside its own are the manifest's (D-11): the vault's signers it reads, the seats it invokes. + if !slices.Contains(m.Reads, signersRead) || !slices.Contains(m.Invokes, placementAdd) || !slices.Contains(m.Invokes, vaultSign) || + !slices.Contains(m.Invokes, vaultToken) || !slices.Contains(m.Invokes, vaultAccount) { + t.Errorf("reads %v, invokes %v: want %s, %s, %s, %s and %s", m.Reads, m.Invokes, signersRead, placementAdd, vaultSign, vaultToken, vaultAccount) + } + if enrolmentsBucket != m.Module+"_"+State || !strings.HasPrefix(PlacementTool, assignerModule+".") { + t.Errorf("the names %s, %s are not the manifest's module %s and the assigner %s", enrolmentsBucket, PlacementTool, m.Module, assignerModule) + } var heard []string for _, e := range EndEvents { heard = append(heard, Seat+"."+e) } - heard = append(heard, "mesh-delivery-assigner.views.placement") + heard = append(heard, assignerModule+".views."+placementState) if !slices.Equal(m.Consumes, heard) { t.Errorf("consumes %v, the end events are %v", m.Consumes, heard) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go index 68a57e95..3d54ec78 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go @@ -6,8 +6,8 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Tool names: the seat's verbs, as the manifest claims them (ADR 0587 §1, §8). @@ -39,8 +39,8 @@ func flag(a map[string]any, k string) bool { return a[k] == true || strings.EqualFold(arg(a, k), "true") } -// lastsOf reads an expiry as a duration (delivery.TokenLife); none is the default. -func lastsOf(s string) (time.Duration, error) { return delivery.TokenLife(s) } +// lastsOf reads an expiry as a duration (meshnodeenrollerclient.TokenLife); none is the default. +func lastsOf(s string) (time.Duration, error) { return meshnodeenrollerclient.TokenLife(s) } // Tools are what the runtime serves. func Tools(h *Holder) []stdio.Tool { @@ -72,7 +72,7 @@ func Tools(h *Holder) []stdio.Tool { "overlay_key": str("its private-network key"), "sealing_key": str("its sealing key, base64"), "serving_key": str("its serving key"), "proof": str("the proof, base64")}, "node", "public_key", "proof"), Run: func(a map[string]any) (any, error) { - var args delivery.Redeem + var args meshnodeenrollerclient.Redeem raw, err := json.Marshal(a) if err != nil { return nil, err diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go index 9d2dbe65..b989ed14 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go @@ -7,8 +7,10 @@ import ( "fmt" "time" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" ) // Seats the holder calls (ADR 0587 §3, §5, §7). The vault's two verbs are the ones its manifest names this seat the only @@ -17,7 +19,7 @@ const ( vaultToken = "seat:mesh-vault." + mv.VerbEnrolmentToken vaultAccount = "seat:mesh-vault." + mv.VerbNodeAccount vaultSign = "seat:mesh-vault." + mv.VerbSign - placementAdd = "seat:mesh-placement.propose" + placementAdd = "seat:" + meshmoduleassigner.Seat + "." + meshmoduleassigner.VerbPropose ) // Call asks a seat verb by its address (stdio.Ask through the runtime, or a test's). @@ -40,8 +42,8 @@ func answer[T any](raw json.RawMessage, err error) (T, error) { return out, nil } -func (v vaultOver) Token(node string, expires time.Time, t delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { - // The whole first tunnel the token carries (delivery.TokenTunnel, catalogue's enrolment-token). +func (v vaultOver) Token(node string, expires time.Time, t meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { + // The whole first tunnel the token carries (meshnodeenrollerclient.TokenTunnel, catalogue's enrolment-token). tunnel := map[string]any{"hub_key": t.HubKey, "hub_endpoint": t.HubEndpoint, "address": t.Address, "key": t.Key, "range": t.Range} return answer[mv.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "expires": expires.UTC().Format(time.RFC3339), "tunnel": tunnel})) @@ -75,22 +77,22 @@ func (v vaultOver) Retire(node string) error { return err } -func (v vaultOver) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) { - return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node, +func (v vaultOver) SignCertificate(node string, k meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) { + return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node, "public_key": k.Identity, "overlay_key": k.Overlay, "sealing_key": k.Sealing, "serving_key": k.Serving})) } func (v vaultOver) SignReply(node string, reply []byte) (mv.SignAnswer, error) { - return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node, + return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": meshvaultclient.KindEnrolment, "node": node, "reply": base64.StdEncoding.EncodeToString(reply)})) } func (v vaultOver) Signer() (string, error) { - raw, found, err := v.read.Get(delivery.SignersRead, delivery.SignersKeyKey) + raw, found, err := v.read.Get(signersRead, signersKey) if err != nil || !found { - return "", fmt.Errorf("the vault's signing key is not readable in %s: %v", delivery.SignersRead, err) + return "", fmt.Errorf("the vault's signing key is not readable in %s: %v", signersRead, err) } - k, err := delivery.DecodeSignerKey(raw) + k, err := meshvaultclient.DecodeSignerKey(raw) if err != nil { return "", err } @@ -102,7 +104,7 @@ func addNodeOver(call Call) func(AddNode) error { return func(a AddNode) error { _, err := call(placementAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant}) if err != nil { - return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Change.Node, err) + return fmt.Errorf("mesh-module-assigner did not take the node %s: %w", a.Change.Node, err) } return nil } diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 419c1d3d..11e046a7 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,6 +3,11 @@ module mesh-enrolment go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-enrolment/module.json b/modules/mesh-enrolment/module.json index b399b363..b3e92e18 100644 --- a/modules/mesh-enrolment/module.json +++ b/modules/mesh-enrolment/module.json @@ -29,7 +29,7 @@ "invokes": [ "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.warranted", - "seat:mesh-placement.propose", + "seat:mesh-module-assigner.propose", "seat:mesh-vault.enrolment-token", "seat:mesh-vault.node-account", "seat:mesh-vault.sign",