533 lines
27 KiB
Go
533 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
|
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
|
|
|
"mesh-delivery-builder/internal/build"
|
|
"mesh-delivery-builder/internal/versions"
|
|
)
|
|
|
|
// 1945 (a part exits at start for a bucket it works without): a bus that refuses every toolchains bucket leaves the
|
|
// watch unstarted and the part running, said; a refused bucket it cannot work without is an error naming the bucket and
|
|
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
|
// issue 774).
|
|
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
|
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
|
|
t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding)
|
|
t.Run("a branch's end is acknowledged once its tagged versions are removed (design 51 Tagged versions)", aBranchsEndIsHeard)
|
|
set := &buildrun.ToolchainSet{}
|
|
var logs bytes.Buffer
|
|
logger := log.New(&logs, "", 0)
|
|
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-build-node_toolchains")}
|
|
if err := newToolchainWatch(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, set, func(context.Context) {}, logger).start(); err != nil {
|
|
t.Errorf("a refused toolchains bucket stopped the start: %v", err)
|
|
}
|
|
if !strings.Contains(logs.String(), "cannot be read") {
|
|
t.Errorf("the refusals were not said: %q", logs.String())
|
|
}
|
|
if _, err := process.EssentialBucket(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, "builder", "mesh-delivery-indexer_modules", "work"); err == nil ||
|
|
!strings.Contains(err.Error(), "grant of mesh-delivery-indexer_modules") {
|
|
t.Errorf("a refused bucket the part needs: %v, want an error naming its grant", err)
|
|
}
|
|
// The gate's source (the previews' trunk heads): refused at start, the builder goes on, and a candidate asking a trunk
|
|
// head is told why and opens it again then.
|
|
src := &trunkSource{ctx: context.Background(), js: refusingBus{refuseOpen: true, err: refused.err}, heads: &sourceHeads{}, errs: make(chan error, 1), logf: logger.Printf}
|
|
src.start()
|
|
if _, err := src.head(context.Background(), 7, "novox/mesh-catalog"); err == nil || !strings.Contains(err.Error(), sourceBucket) {
|
|
t.Errorf("a trunk head with the source refused: %v, want why", err)
|
|
}
|
|
if src.opened {
|
|
t.Error("a refused source counts as opened, so it is never opened again")
|
|
}
|
|
t.Run("ADR 0624: the counted nodes' engine reads", theCountedNodesEngineReads)
|
|
}
|
|
|
|
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the assignments names; a node away
|
|
// and never asked has no statement and reads as the baseline's first forms, with no node record read in its place; an
|
|
// away node keeps its last statement; a node the assignments no longer names leaves the count, its key removed; and
|
|
// builder_engine_reads drops one node's key.
|
|
func theCountedNodesEngineReads(t *testing.T) {
|
|
gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf)
|
|
place := func(nodes ...string) {
|
|
for _, node := range []string{"n1", "n2"} {
|
|
pl := meshmoduleassigner.Assignments{Node: node}
|
|
if slices.Contains(nodes, node) {
|
|
pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}}
|
|
}
|
|
values, err := meshmoduleassignerclient.AssignmentsValues(pl)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for key, raw := range values {
|
|
gate.Assignments(key, raw, false, time.Now())
|
|
}
|
|
if !slices.Contains(nodes, node) {
|
|
key, _ := meshmoduleassignerclient.ModuleKey(node, "web")
|
|
gate.Assignments(key, nil, true, time.Now())
|
|
}
|
|
}
|
|
}
|
|
away := map[string]bool{"n2": true}
|
|
asked := map[string]int{}
|
|
r := newEngineReads(func(node, key string, _ any) (json.RawMessage, error) {
|
|
asked[node]++
|
|
if away[node] {
|
|
return nil, nats.ErrNoResponders
|
|
}
|
|
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
|
|
}, gate, t.Logf)
|
|
r.keeper.Sync = true
|
|
kept := map[string][]byte{}
|
|
r.keeper.Put = func(node string, v []byte) error { kept[node] = v; return nil }
|
|
r.keeper.Delete = func(node string) error { delete(kept, node); return nil }
|
|
place("n1", "n2")
|
|
r.placed()
|
|
present, counted := r.forms()
|
|
if !slices.Equal(present["n1"], []string{"form:new"}) || len(present) != 1 || len(counted) != 2 || counted["n2"] != nil {
|
|
t.Errorf("away and never asked: present %v, counted %v", present, counted)
|
|
}
|
|
if s := r.keeper.Statements()["n2"]; s.From != enginereads.FromNone || s.Answered || asked["n2"] != 1 {
|
|
t.Errorf("n2, away and never asked, is %+v (asked %d)", s, asked["n2"])
|
|
}
|
|
// n2 back, stating {word:module}, then away again: it keeps that statement.
|
|
away["n2"] = false
|
|
r.keeper.Heard("n2", mustStatement(t, enginereads.Statement{Node: "n2", From: enginereads.FromVerb, Answered: true,
|
|
AskAgain: &enginereads.Again{By: "n1.ada", Why: "back"}}), false)
|
|
away["n2"] = true
|
|
r.keeper.AskNode("n2", "its event")
|
|
if _, counted = r.forms(); !slices.Equal(counted["n2"], []string{"form:new"}) || r.keeper.Statements()["n2"].Answered {
|
|
t.Errorf("away again, n2 reads %v (%+v)", counted["n2"], r.keeper.Statements()["n2"])
|
|
}
|
|
// n2 retired: it leaves the assignments, and its key goes.
|
|
place("n1")
|
|
r.placed()
|
|
if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil {
|
|
t.Errorf("retired n2: counted %v, its key %s", counted, kept["n2"])
|
|
}
|
|
// builder_engine_reads drops one key.
|
|
st := &memReads{v: map[string][]byte{"n1": kept["n1"]}}
|
|
tool := enginereads.Tool("builder_engine_reads", "the builder", st, true, time.Now)
|
|
if _, err := tool.Run(map[string]any{"node": "n1", "drop": "true", "why": "a test"}); err != nil || st.v["n1"] != nil {
|
|
t.Errorf("drop: %v, the key %s", err, st.v["n1"])
|
|
}
|
|
}
|
|
|
|
func mustStatement(t *testing.T, s enginereads.Statement) []byte {
|
|
t.Helper()
|
|
raw, err := enginereads.Encode(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// memReads is the builder's state of engine reads in memory, as the tool reads it.
|
|
type memReads struct{ v map[string][]byte }
|
|
|
|
func (m *memReads) Keys() ([]string, error) { return slices.Collect(maps.Keys(m.v)), nil }
|
|
func (m *memReads) Get(key string) ([]byte, uint64, bool, error) {
|
|
v, ok := m.v[key]
|
|
return v, 1, ok, nil
|
|
}
|
|
func (m *memReads) Put(key string, value []byte) (uint64, error) { m.v[key] = value; return 2, nil }
|
|
func (m *memReads) Delete(key string) error { delete(m.v, key); return nil }
|
|
func (m *memReads) Await(string, uint64, func([]byte) bool, time.Duration) ([]byte, error) {
|
|
return nil, errors.New("no part answers here")
|
|
}
|
|
|
|
// refusingBus is a bus that refuses: every open (refuseOpen), or every watch of a bucket it opens, with err.
|
|
type refusingBus struct {
|
|
refuseOpen bool
|
|
err error
|
|
}
|
|
|
|
func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) {
|
|
if r.refuseOpen {
|
|
return nil, r.err
|
|
}
|
|
return refusedWatch{err: r.err}, nil
|
|
}
|
|
|
|
type refusedWatch struct {
|
|
jetstream.KeyValue
|
|
err error
|
|
}
|
|
|
|
func (r refusedWatch) WatchAll(context.Context, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
|
|
return nil, r.err
|
|
}
|
|
func (r refusedWatch) Watch(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
|
|
return nil, r.err
|
|
}
|
|
|
|
// wakeMsg is a message of the builder's events consumer: its subject and body, acknowledged.
|
|
type wakeMsg struct {
|
|
jetstream.Msg
|
|
subject string
|
|
data []byte
|
|
acked bool
|
|
naked bool
|
|
delayed time.Duration
|
|
}
|
|
|
|
func (m *wakeMsg) Subject() string { return m.subject }
|
|
func (m *wakeMsg) Data() []byte { return m.data }
|
|
func (m *wakeMsg) Ack() error { m.acked = true; return nil }
|
|
func (m *wakeMsg) Nak() error { m.naked = true; return nil }
|
|
func (m *wakeMsg) NakWithDelay(d time.Duration) error {
|
|
m.delayed = d
|
|
return nil
|
|
}
|
|
func (m *wakeMsg) Metadata() (*jetstream.MsgMetadata, error) {
|
|
return nil, errors.New("no metadata")
|
|
}
|
|
func (m *wakeMsg) Headers() nats.Header {
|
|
return nil
|
|
}
|
|
|
|
func aFloorsWakeIsHeard(t *testing.T) {
|
|
var logs bytes.Buffer
|
|
waits := &versions.FloorWaits{}
|
|
waits.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceRequirements}}, nil, time.Unix(0, 0))
|
|
waits.Wait("m/img", versions.SeatImages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
|
|
f := &floorWake{b: &build.Builder{}, waits: waits, logf: log.New(&logs, "", 0).Printf}
|
|
accepted := &wakeMsg{subject: manifestAcceptedSubject}
|
|
if !f.heard(context.Background(), accepted) || !accepted.acked || !strings.Contains(logs.String(), "the floor of m/lib is asked again: manifest-accepted") {
|
|
t.Fatalf("manifest-accepted: acked %v, log %q", accepted.acked, logs.String())
|
|
}
|
|
logs.Reset()
|
|
cleared := &wakeMsg{subject: conditionClearedSubject, data: []byte(`{"subject":{"scope":"probe","id":"ace/mesh-delivery-forge-bridge"}}`)}
|
|
if !f.heard(context.Background(), cleared) || !strings.Contains(logs.String(), "the floor of m/img is asked again: condition-cleared") {
|
|
t.Fatalf("condition-cleared: log %q", logs.String())
|
|
}
|
|
if f.heard(context.Background(), &wakeMsg{subject: sweep.SubjectArtifactsGone}) {
|
|
t.Fatal("artifacts-gone was taken as a floor's wake")
|
|
}
|
|
// DLV-B4: a registry seat's outcome on the same consumer moves a version; a published wakes a name whose seat was
|
|
// silent; one the state cannot take is left for the bus to give again.
|
|
logs.Reset()
|
|
waits.Wait("m/pkg", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceSeat}}, nil, time.Unix(0, 0))
|
|
h := &seatHearer{n: &versions.Numberer{Versions: versions.Versions{Store: noVersions{}}, Waits: waits}, b: &build.Builder{}, logf: log.New(&logs, "", 0).Printf}
|
|
byDigest := &wakeMsg{subject: "mesh.seat.go-package-registry.event.published", data: []byte(`{"name":"m/other","digest":"sha256:aa","kind":"package","holder":"gitea"}`)}
|
|
if !h.heard(context.Background(), byDigest) || !byDigest.acked || !strings.Contains(logs.String(), "the floor of m/pkg is asked again: go-package-registry said published") {
|
|
t.Fatalf("published: acked %v, log %q", byDigest.acked, logs.String())
|
|
}
|
|
refused := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.refused", data: []byte(`{"name":"m/program","version":"0.1.3","digest":"sha256:bb","reason":"bytes differ"}`)}
|
|
if !h.heard(context.Background(), refused) || refused.acked || !refused.naked {
|
|
t.Fatalf("a refusal the state cannot take: acked %v, naked %v, log %q", refused.acked, refused.naked, logs.String())
|
|
}
|
|
if h.heard(context.Background(), &wakeMsg{subject: manifestAcceptedSubject}) {
|
|
t.Fatal("manifest-accepted was taken as a seat's outcome")
|
|
}
|
|
}
|
|
|
|
// noVersions is a state versions that holds nothing.
|
|
type noVersions struct{}
|
|
|
|
func (noVersions) Get(context.Context, string) ([]byte, uint64, bool, error) {
|
|
return nil, 0, false, nil
|
|
}
|
|
func (noVersions) Create(context.Context, string, []byte) (uint64, error) {
|
|
return 0, errors.New("read only")
|
|
}
|
|
func (noVersions) Update(context.Context, string, []byte, uint64) (uint64, error) {
|
|
return 0, errors.New("read only")
|
|
}
|
|
func (noVersions) KeysOf(context.Context, string) ([]string, error) { return nil, nil }
|
|
|
|
func aBranchsEndIsHeard(t *testing.T) {
|
|
for _, c := range []struct {
|
|
f forgeEnd
|
|
want bool
|
|
}{
|
|
{forgeEnd{Kind: forgemsg.ForgeMerged, Repository: "o/r", Branch: "feat/x"}, true},
|
|
{forgeEnd{Kind: forgeBranchDeleted, Repository: "o/r", Branch: "feat/x"}, true},
|
|
{forgeEnd{Kind: forgemsg.ForgePull, Repository: "o/r", Branch: "feat/x", Action: forgemsg.PullClosed}, true},
|
|
{forgeEnd{Kind: forgemsg.ForgePull, Repository: "o/r", Branch: "feat/x", Action: forgemsg.PullOpened}, false},
|
|
{forgeEnd{Kind: forgemsg.ForgeMerged, Repository: "o/r"}, false},
|
|
} {
|
|
if got := c.f.ended(); got != c.want {
|
|
t.Errorf("%+v ended = %v, want %v", c.f, got, c.want)
|
|
}
|
|
}
|
|
var logs bytes.Buffer
|
|
woken := 0
|
|
h := &branchEnds{acts: versions.Acts{Numberer: &versions.Numberer{Versions: versions.Versions{Store: noVersions{}}}}, findings: &findings{logf: t.Logf},
|
|
tagAsks: func() jetstream.KeyValue { return nil }, wake: func(context.Context) { woken++ }, logf: log.New(&logs, "", 0).Printf, now: time.Now}
|
|
open := &wakeMsg{subject: "mesh.mod.mesh-delivery-forge-bridge.event.forge.o.r.pull", data: []byte(`{"kind":"pull","repository":"o/r","branch":"feat/x","action":"opened"}`)}
|
|
h.heard(context.Background(), open)
|
|
merged := &wakeMsg{subject: "mesh.mod.mesh-delivery-forge-bridge.event.forge.o.r.merged", data: []byte(`{"kind":"merged","repository":"o/r","branch":"feat/x"}`)}
|
|
h.heard(context.Background(), merged)
|
|
if !open.acked || !merged.acked || woken != 2 {
|
|
t.Errorf("an open pull acked %v, a merge with nothing tagged acked %v, floors woken %d times", open.acked, merged.acked, woken)
|
|
}
|
|
// The checker's family holds its verdicts beside tag-wanted: a verdict is acknowledged and not kept; a tag-wanted with
|
|
// tag-asks not bound is heard again.
|
|
verdict := &wakeMsg{subject: "mesh.stream.mesh-delivery-checker.event.verdict", data: []byte(`{"repository":"o/r"}`)}
|
|
wanted := &wakeMsg{subject: tagWantedSubject, data: []byte(`{"repository":"o/r","branch":"feat/x","head":"abc","tag":"feat-x"}`)}
|
|
for _, m := range []*wakeMsg{verdict, wanted} {
|
|
tagWanted(context.Background(), nil, m, checkerModule, time.Now(), t.Logf)
|
|
}
|
|
if !verdict.acked || verdict.delayed != 0 || wanted.acked || wanted.delayed != versions.RemoveAgainAfter || !strings.HasPrefix(tagWantedSubject, "mesh.stream.") {
|
|
t.Errorf("a verdict acked %v (delayed %v), a tag-wanted acked %v (delayed %v)", verdict.acked, verdict.delayed, wanted.acked, wanted.delayed)
|
|
}
|
|
unreadable := &wakeMsg{subject: merged.subject, data: merged.data}
|
|
h.acts.Numberer.Versions.Store = failingVersions{}
|
|
h.heard(context.Background(), unreadable)
|
|
if unreadable.acked || unreadable.delayed != versions.RemoveAgainAfter {
|
|
t.Errorf("a branch's end the versions cannot answer: acked %v, given again after %v", unreadable.acked, unreadable.delayed)
|
|
}
|
|
}
|
|
|
|
// failingVersions is a state versions that cannot be read.
|
|
type failingVersions struct{ noVersions }
|
|
|
|
func (failingVersions) KeysOf(context.Context, string) ([]string, error) {
|
|
return nil, errors.New("down")
|
|
}
|
|
|
|
func aFailedTrunkBuildIsAFinding(t *testing.T) {
|
|
st := &memFindings{vals: map[string][]byte{}}
|
|
f := &findings{store: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
|
|
ended := func(outcome, ref string) builds.BuildLogEntry {
|
|
return builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome}
|
|
}
|
|
key := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m", BuildFailed)
|
|
f.buildEnded(context.Background(), ended(builds.BuildFailed, "refs/pull/3/head"))
|
|
if len(st.vals) != 0 {
|
|
t.Fatalf("a candidate's failed build is a finding: %v", st.vals)
|
|
}
|
|
f.buildEnded(context.Background(), ended(builds.BuildFailed, ""))
|
|
got, err := meshconditionsclient.DecodeFinding(st.vals[key])
|
|
if err != nil || got.Observer != builds.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") {
|
|
t.Fatalf("the finding = %+v, %v", got, err)
|
|
}
|
|
f.buildEnded(context.Background(), ended(builds.BuildDone, ""))
|
|
if _, ok := st.vals[key]; ok {
|
|
t.Error("a later done trunk build leaves build-failed")
|
|
}
|
|
// The floor finding: raised while a name waits on its floor, cleared once it is known.
|
|
fw := &versions.FloorWaits{Said: f.floorSaid(context.Background())}
|
|
fw.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
|
|
floorKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/lib", versions.FloorSilent)
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[floorKey]); err != nil || !strings.Contains(got.Summary, "bridge_tags did not answer") {
|
|
t.Fatalf("the floor finding = %+v, %v", got, err)
|
|
}
|
|
fw.Answered("m/lib")
|
|
if _, ok := st.vals[floorKey]; ok {
|
|
t.Error("a known floor leaves floor-silent")
|
|
}
|
|
// Design 51 *Health and conditions* (review round 1): a holder's refused is publish-refused with its reason, a version
|
|
// no holder answered within its bound publish-unanswered, a seat's outcome that does not read seat-outcome-unread;
|
|
// a later published of the name clears the first two, the seat's next outcome that reads the third.
|
|
vs := versions.Versions{Store: memVersions{}}
|
|
for _, v := range []string{"0.1.3", "0.1.4"} {
|
|
raw, err := versions.Encode(versions.Entry{Name: "m/program", Kind: versions.KindBundle, Series: "0.1", Version: v, Seat: "oci-artifact-registry", State: versions.Numbered})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vs.Store.(memVersions)[versions.Key("m/program", v)] = raw
|
|
}
|
|
pubs := &publishes{bounds: func() jetstream.KeyValue { return nil }, versions: vs, findings: f, bound: versions.PublishBound, logf: t.Logf}
|
|
h := &seatHearer{n: &versions.Numberer{Versions: vs}, b: &build.Builder{}, logf: t.Logf, pub: pubs}
|
|
refusedKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/program", versions.PublishRefused)
|
|
refused := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.refused", data: []byte(`{"name":"m/program","version":"0.1.3","digest":"sha256:bb","reason":"bytes differ"}`)}
|
|
if !h.heard(context.Background(), refused) || !refused.acked {
|
|
t.Fatalf("a refusal: acked %v", refused.acked)
|
|
}
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[refusedKey]); err != nil || !strings.Contains(got.Summary, "refused to publish m/program@0.1.3") || !strings.Contains(got.Summary, "bytes differ") {
|
|
t.Fatalf("a holder's refused: the finding = %+v, %v", got, err)
|
|
}
|
|
pubs.ended(context.Background(), purged{key: versions.PublishBoundKey("m/program", "0.1.4")})
|
|
unansweredKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/program", versions.PublishUnanswered)
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[unansweredKey]); err != nil || !strings.Contains(got.Summary, "neither published nor refused of m/program@0.1.4") {
|
|
t.Fatalf("a bound ended unanswered: the finding = %+v, %v", got, err)
|
|
}
|
|
// Review round 2: a bound that cannot be written is publish-unanswered at once; a watch of bounds that cannot start
|
|
// is family-not-heard naming the state.
|
|
delete(st.vals, unansweredKey)
|
|
pubs.arm(context.Background(), versions.Entry{Name: "m/program", Version: "0.1.4", Seat: "oci-artifact-registry"})
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[unansweredKey]); err != nil || !strings.Contains(got.Summary, "its bound could not be written") {
|
|
t.Fatalf("a bound not written: the finding = %+v, %v", got, err)
|
|
}
|
|
watchBounds(context.Background(), refusedWatch{err: errors.New("permissions violation")}, func(jetstream.KeyValueEntry) {}, f.notHeard(context.Background()), nil, t.Logf)
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[heard.Key(builds.BuilderModule, versions.BoundsState)]); err != nil || !strings.Contains(got.Summary, "permissions violation") {
|
|
t.Fatalf("bounds not watched: the finding = %+v, %v", got, err)
|
|
}
|
|
delete(st.vals, heard.Key(builds.BuilderModule, versions.BoundsState))
|
|
malformed := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.published", data: []byte(`{"version":"0.1.4"}`)}
|
|
unreadKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "oci-artifact-registry", versions.SeatOutcomeUnread)
|
|
if !h.heard(context.Background(), malformed) || !malformed.acked || st.vals[unreadKey] == nil {
|
|
t.Fatalf("an outcome that does not read: acked %v, findings %v", malformed.acked, st.vals)
|
|
}
|
|
published := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.published", data: []byte(`{"name":"m/program","version":"0.1.4","digest":"sha256:cc"}`)}
|
|
if !h.heard(context.Background(), published) || st.vals[refusedKey] != nil || st.vals[unansweredKey] != nil || st.vals[unreadKey] != nil {
|
|
t.Fatalf("a later published leaves the findings: %v", st.vals)
|
|
}
|
|
// ADR 0615 §5 (review round 1): a consumer not there yet is family-not-heard naming its stream, cleared when it binds.
|
|
notHeard := f.notHeard(context.Background())
|
|
unheardKey := heard.Key(builds.BuilderModule, "FORGE")
|
|
_ = notHeard.Bind("FORGE", "a branch's end is not heard", func() error { return errors.New("consumer not found") })
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[unheardKey]); err != nil || got.Kind != heard.Kind || !strings.Contains(got.Summary, "does not hear FORGE") {
|
|
t.Fatalf("a consumer not there: the finding = %+v, %v", got, err)
|
|
}
|
|
_ = notHeard.Bind("FORGE", "a branch's end is not heard", func() error { return nil })
|
|
if _, ok := st.vals[unheardKey]; ok {
|
|
t.Error("a consumer bound leaves family-not-heard")
|
|
}
|
|
// DLV-B9, the old form's window: one counted node reading only the first form holds it; seven days on it is
|
|
// old-form-held about that node; the node stating the newest form closes it, said once, the finding cleared; a
|
|
// retired node dropped by hand leaves the count.
|
|
now := time.Unix(0, 0)
|
|
fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}}
|
|
var closings []versions.OldFormClosedBody
|
|
var logged []builds.BuildLogEntry
|
|
marks := &memBounds{keys: map[string]bool{}}
|
|
window := &oldForm{forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now }, bounds: func() jetstream.KeyValue { return marks },
|
|
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) },
|
|
log: func(e builds.BuildLogEntry) { logged = append(logged, e) }}
|
|
reads := &engineReads{keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
|
|
newest := []string{forms.FormNeed("oci")}
|
|
counted := map[string][]string{"a": newest, "b": nil}
|
|
window.step(context.Background(), []string{"a", "b"}, counted)
|
|
heldKey := meshconditionsclient.Key(meshconditionsclient.ScopeMachine, "b", versions.OldFormHeld)
|
|
if !window.w.Open || !slices.Equal(window.w.Holding, []string{"b"}) || st.vals[heldKey] != nil {
|
|
t.Fatalf("one node lacking the newest form: %+v", window.w)
|
|
}
|
|
// Review round 1: the node holding it has a bound in the state bounds ending at its seven days, so old-form-held is
|
|
// raised then, not at the next change.
|
|
if !marks.keys[versions.OldFormBoundKey("b")] || marks.keys[versions.OldFormBoundKey("a")] {
|
|
t.Fatalf("the old form's bounds = %v, want b's alone", marks.keys)
|
|
}
|
|
// Security-upgrade F3 (review round 1): an empty node_haves read, naming no node, leaves the window open.
|
|
window.step(context.Background(), nil, map[string][]string{})
|
|
if !window.w.Open || !slices.Equal(window.w.Holding, []string{"b"}) || len(closings) != 0 {
|
|
t.Fatalf("an empty read closed the window: %+v, closings %+v", window.w, closings)
|
|
}
|
|
now = now.Add(versions.FindingAfter)
|
|
window.step(context.Background(), []string{"a", "b"}, counted)
|
|
if got, err := meshconditionsclient.DecodeFinding(st.vals[heldKey]); err != nil || !strings.Contains(got.Summary, "the node-engine of b") {
|
|
t.Fatalf("seven days held: the finding = %+v, %v", got, err)
|
|
}
|
|
counted["b"] = newest
|
|
window.step(context.Background(), []string{"a", "b"}, counted)
|
|
window.step(context.Background(), []string{"a", "b"}, counted)
|
|
if marks.keys[versions.OldFormBoundKey("b")] {
|
|
t.Error("a node no longer holding the window keeps its bound")
|
|
}
|
|
if window.w.Open || len(closings) != 1 || !slices.Equal(closings[0].LastHolding, []string{"b"}) || st.vals[heldKey] != nil {
|
|
t.Errorf("the node back with the newest form: %+v, closings %+v, finding left %v", window.w, closings, st.vals[heldKey] != nil)
|
|
}
|
|
if len(logged) != 1 || logged[0].Event != builds.BuildSaidEvent || logged[0].Why != versions.SaidOldFormClosed ||
|
|
!strings.Contains(logged[0].Line, "last held by b") {
|
|
t.Errorf("the build log = %+v, want the close said once (build.said)", logged)
|
|
} else if _, err := builds.EncodeBuildLogEntry(logged[0]); err != nil {
|
|
t.Errorf("the close is no entry the build log takes: %v", err)
|
|
}
|
|
if _, err := window.drop(context.Background(), "b", "op@shanks", "retired", now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, c := reads.forms(); len(c) != 1 || c["b"] != nil {
|
|
t.Errorf("a node dropped by hand is still counted: %v", c)
|
|
}
|
|
}
|
|
|
|
// memVersions is a state versions in memory.
|
|
type memVersions map[string][]byte
|
|
|
|
func (m memVersions) Get(_ context.Context, key string) ([]byte, uint64, bool, error) {
|
|
v, ok := m[key]
|
|
return v, 1, ok, nil
|
|
}
|
|
func (m memVersions) Create(_ context.Context, key string, v []byte) (uint64, error) {
|
|
m[key] = v
|
|
return 1, nil
|
|
}
|
|
func (m memVersions) Update(_ context.Context, key string, v []byte, _ uint64) (uint64, error) {
|
|
m[key] = v
|
|
return 2, nil
|
|
}
|
|
func (m memVersions) KeysOf(context.Context, string) ([]string, error) {
|
|
return slices.Collect(maps.Keys(m)), nil
|
|
}
|
|
|
|
// memBounds is the state bounds in memory: which keys stand.
|
|
type memBounds struct {
|
|
jetstream.KeyValue
|
|
keys map[string]bool
|
|
}
|
|
|
|
func (m *memBounds) Create(_ context.Context, key string, _ []byte, _ ...jetstream.KVCreateOpt) (uint64, error) {
|
|
if m.keys[key] {
|
|
return 0, jetstream.ErrKeyExists
|
|
}
|
|
m.keys[key] = true
|
|
return 1, nil
|
|
}
|
|
func (m *memBounds) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
|
|
delete(m.keys, key)
|
|
return nil
|
|
}
|
|
|
|
// purged is the bus's marker of a key whose time to live ended.
|
|
type purged struct {
|
|
jetstream.KeyValueEntry
|
|
key string
|
|
}
|
|
|
|
func (p purged) Key() string { return p.key }
|
|
func (p purged) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePurge }
|
|
|
|
// memFindings is a findings bucket in memory.
|
|
type memFindings struct{ vals map[string][]byte }
|
|
|
|
type memEntry struct {
|
|
jetstream.KeyValueEntry
|
|
v []byte
|
|
}
|
|
|
|
func (e memEntry) Value() []byte { return e.v }
|
|
|
|
func (m *memFindings) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
|
|
v, ok := m.vals[key]
|
|
if !ok {
|
|
return nil, jetstream.ErrKeyNotFound
|
|
}
|
|
return memEntry{v: v}, nil
|
|
}
|
|
func (m *memFindings) Put(_ context.Context, key string, v []byte) (uint64, error) {
|
|
m.vals[key] = v
|
|
return 1, nil
|
|
}
|
|
func (m *memFindings) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
|
|
delete(m.vals, key)
|
|
return nil
|
|
}
|