Merge branch 'b7m/registry' into b7/registry
This commit is contained in:
@@ -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) {
|
||||
|
||||
+17
-16
@@ -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"},
|
||||
|
||||
@@ -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, `<module>_<name>` 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.<owed key>` and `merges.<merges key>`; 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 }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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, `<module>_<name>` 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 }
|
||||
|
||||
+17
-15
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.<module>.event.<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() {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, `<owner>/<repo>@<full sha>` 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.<merges key>`, 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `<node>.<ask id>` 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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 `<node>.<ask id>` running in the build module's steps (delivery.StepsBucket) when it takes the ask, and writes it passed,
|
||||
// key `<node>.<ask id>` 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.<node>` 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]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.<key>.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
|
||||
}
|
||||
|
||||
@@ -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 + ": " +
|
||||
|
||||
@@ -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 `<consumer>.<node>`, 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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 "<module>_<state>".
|
||||
// bucket is "<module>_<state>" of a manifest entry "<module>.<state>" or "<module>.views.<state>".
|
||||
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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.<node> and address.<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 <seat>.<event>.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":[]}`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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.<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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.<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.<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.<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.<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.<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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user