Compare commits
19
Commits
feat/l5-l7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc38ca3f8d | ||
|
|
d739516056 | ||
|
|
a6c225eb45 | ||
|
|
5085b681e9 | ||
|
|
9e407f8f76 | ||
|
|
8545e7fc9f | ||
|
|
55a506975b | ||
|
|
b7dfe4c637 | ||
|
|
c4b05a3e93 | ||
|
|
38f2401f17 | ||
|
|
c8b041ab37 | ||
|
|
f52f47e1ff | ||
|
|
213ebe7cfd | ||
|
|
90e278cb97 | ||
|
|
58748c2be1 | ||
|
|
8e9cc58301 | ||
|
|
fb96216e46 | ||
|
|
5059fef3e6 | ||
|
|
564a04b45c |
@@ -407,7 +407,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
|||||||
for _, fp := range []string{fNew, fOld} {
|
for _, fp := range []string{fNew, fOld} {
|
||||||
gkey, _ := delivery.ControllerGrantKey("web", fp)
|
gkey, _ := delivery.ControllerGrantKey("web", fp)
|
||||||
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
||||||
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant)
|
b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
|
||||||
}
|
}
|
||||||
runs := map[string]string{"ace": fOld, "g14": "build-9", "shanks": fOld}
|
runs := map[string]string{"ace": fOld, "g14": "build-9", "shanks": fOld}
|
||||||
for _, node := range []string{"ace", "g14", "shanks"} {
|
for _, node := range []string{"ace", "g14", "shanks"} {
|
||||||
@@ -617,7 +617,7 @@ func TestTheAssignerReplacedResumesWithNothingLostOrRepeated(t *testing.T) {
|
|||||||
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
||||||
gkey, _ := delivery.ControllerGrantKey(name, fp)
|
gkey, _ := delivery.ControllerGrantKey(name, fp)
|
||||||
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
||||||
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant)
|
b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
|
||||||
b.compose(t, "ace", name, fp, 'c', delivery.JudgedNeither)
|
b.compose(t, "ace", name, fp, 'c', delivery.JudgedNeither)
|
||||||
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(fmt.Sprintf(`{"node":"ace","module":%q,"build":"b-%d"}`, name, i)))
|
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(fmt.Sprintf(`{"node":"ace","module":%q,"build":"b-%d"}`, name, i)))
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ func TestAWantWaitsForItsComposition(t *testing.T) {
|
|||||||
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
||||||
gkey, _ := delivery.ControllerGrantKey("web", fNew)
|
gkey, _ := delivery.ControllerGrantKey("web", fNew)
|
||||||
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
||||||
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant)
|
b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
|
||||||
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
|
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
|
||||||
b.putJSON(t, delivery.PresenceBucket, "ace", on)
|
b.putJSON(t, delivery.PresenceBucket, "ace", on)
|
||||||
b.putJSON(t, provider.AssignmentsBucket, "ace.web", []byte(`{"node":"ace","module":"web","build":"b-1"}`))
|
b.putJSON(t, provider.AssignmentsBucket, "ace.web", []byte(`{"node":"ace","module":"web","build":"b-1"}`))
|
||||||
@@ -712,7 +712,7 @@ func (b *bed) fixture(t *testing.T, name, fp, manifest string) {
|
|||||||
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
||||||
gkey, _ := delivery.ControllerGrantKey(name, fp)
|
gkey, _ := delivery.ControllerGrantKey(name, fp)
|
||||||
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
||||||
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant)
|
b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
|
||||||
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
|
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
|
||||||
b.putJSON(t, delivery.PresenceBucket, "ace", on)
|
b.putJSON(t, delivery.PresenceBucket, "ace", on)
|
||||||
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(`{"node":"ace","module":"`+name+`","build":"b-1"}`))
|
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(`{"node":"ace","module":"`+name+`","build":"b-1"}`))
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ func main() {
|
|||||||
var readBuckets = []string{
|
var readBuckets = []string{
|
||||||
buscompose.ResolvedBucket,
|
buscompose.ResolvedBucket,
|
||||||
provider.AssignmentsBucket,
|
provider.AssignmentsBucket,
|
||||||
delivery.ControllerGrantsBucket,
|
|
||||||
delivery.PartGrantsBucket,
|
delivery.PartGrantsBucket,
|
||||||
buscompose.LeaseBucket,
|
buscompose.LeaseBucket,
|
||||||
delivery.ControllerComposedBucket,
|
delivery.ControllerComposedBucket,
|
||||||
@@ -219,8 +218,8 @@ func startsWithout(name string) bool {
|
|||||||
return slices.Contains(seatBuckets, name) || sourceOfGrants(name)
|
return slices.Contains(seatBuckets, name) || sourceOfGrants(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceOfGrants says the bucket is one of the two that choose the grants' source, the grants part's grants and the
|
// sourceOfGrants says the bucket is the grants part's grants or the controller's lease: neither stops the assigner when it
|
||||||
// controller's lease: neither stops the assigner when it is not there (delivery.GrantSource).
|
// is not there.
|
||||||
func sourceOfGrants(name string) bool {
|
func sourceOfGrants(name string) bool {
|
||||||
return name == delivery.PartGrantsBucket || name == buscompose.LeaseBucket
|
return name == delivery.PartGrantsBucket || name == buscompose.LeaseBucket
|
||||||
}
|
}
|
||||||
@@ -333,7 +332,6 @@ func (e emitting) Emit(eventType string, body any) error { return e.Conn.Emit(ev
|
|||||||
var watched = []string{
|
var watched = []string{
|
||||||
buscompose.ResolvedBucket,
|
buscompose.ResolvedBucket,
|
||||||
provider.AssignmentsBucket,
|
provider.AssignmentsBucket,
|
||||||
delivery.ControllerGrantsBucket,
|
|
||||||
delivery.PartGrantsBucket,
|
delivery.PartGrantsBucket,
|
||||||
buscompose.LeaseBucket,
|
buscompose.LeaseBucket,
|
||||||
delivery.ControllerComposedBucket,
|
delivery.ControllerComposedBucket,
|
||||||
@@ -419,11 +417,10 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
|||||||
if e.Operation() == jetstream.KeyValuePut {
|
if e.Operation() == jetstream.KeyValuePut {
|
||||||
value = e.Value()
|
value = e.Value()
|
||||||
}
|
}
|
||||||
before := w.grantsBucket()
|
before := w.handedAway()
|
||||||
w.putEntry(name, e.Key(), value, e.Revision(), e.Created())
|
w.putEntry(name, e.Key(), value, e.Revision(), e.Created())
|
||||||
if name == buscompose.LeaseBucket && w.grantsBucket() == before {
|
if name == buscompose.LeaseBucket && w.handedAway() == before {
|
||||||
// The controller renews its lease every few seconds: only a change of where grants are read from
|
// The controller renews its lease every few seconds: only the lease's being handed away makes a pass.
|
||||||
// makes a pass (delivery.GrantSource).
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wake()
|
wake()
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ func putOfTheBus(t *testing.T) {
|
|||||||
world(delivery.ControllerComposedBucket, ckey, cd)
|
world(delivery.ControllerComposedBucket, ckey, cd)
|
||||||
gkey, _ := delivery.ControllerGrantKey("nats", fp)
|
gkey, _ := delivery.ControllerGrantKey("nats", fp)
|
||||||
g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
|
||||||
world(delivery.ControllerGrantsBucket, gkey, g)
|
world(delivery.PartGrantsBucket, gkey, g)
|
||||||
got := step()
|
got := step()
|
||||||
akey, _ := delivery.RecordAskKey("novox", "nats", digest)
|
akey, _ := delivery.RecordAskKey("novox", "nats", digest)
|
||||||
if got.Step != delivery.PutPerson || got.Ask != akey {
|
if got.Step != delivery.PutPerson || got.Ask != akey {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/health"
|
||||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
|
|
||||||
@@ -400,6 +402,47 @@ func wantsVerbs(t *testing.T) {
|
|||||||
if splits := out.(map[string]any)["splits"].([]assign.Split); len(splits) != 1 || splits[0].Module != "web" || out.(map[string]any)["findings"] == nil {
|
if splits := out.(map[string]any)["splits"].([]assign.Split); len(splits) != 1 || splits[0].Module != "web" || out.(map[string]any)["findings"] == nil {
|
||||||
t.Errorf("splits = %+v", splits)
|
t.Errorf("splits = %+v", splits)
|
||||||
}
|
}
|
||||||
|
// Read by watch (issue 1953 L5f): the same answer, each watched state started once, and no get per key of the
|
||||||
|
// large states (the modules, their manifests, the composed record).
|
||||||
|
watches := map[string]int{}
|
||||||
|
byWatch := newWantsTools(st, nil, nil)
|
||||||
|
byWatch.watched = stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
|
||||||
|
watches[state]++
|
||||||
|
for k, v := range st[state].values {
|
||||||
|
_ = handle(stdio.StateChange{Key: k, Op: stdio.OpPut, Value: v, Current: true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, func(state, key string) ([]byte, bool, error) {
|
||||||
|
watches[state+" by key"]++
|
||||||
|
v, ok := st[state].values[key]
|
||||||
|
return v, ok, nil
|
||||||
|
})
|
||||||
|
for range 2 {
|
||||||
|
wout, werr := byWatch.doUpgrade(map[string]any{"compare": "true"}, "op")
|
||||||
|
if werr != nil {
|
||||||
|
t.Fatal(werr)
|
||||||
|
}
|
||||||
|
if ws := wout.(map[string]any)["splits"].([]assign.Split); len(ws) != 1 || ws[0].Module != "web" {
|
||||||
|
t.Errorf("splits by watch = %+v", ws)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n := watches[manifestsState+" by key"]; n == 0 || n > len(st[manifestsState].values) {
|
||||||
|
t.Errorf("the manifests were read by key %d times, want each once", n)
|
||||||
|
}
|
||||||
|
for _, state := range []string{modulesState, composedState, health.FindingsState} {
|
||||||
|
if watches[state] != 1 {
|
||||||
|
t.Errorf("%s was watched %d times, want once", state, watches[state])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ADR 0580 clause 3: the compare reads through mesh-sdk's watched reader and the seat keeps no mirror of its own.
|
||||||
|
for f := range reflect.TypeFor[wantsTools]().Fields() {
|
||||||
|
if f.Type.Kind() == reflect.Map || (f.Type.Kind() == reflect.Pointer && f.Type.Elem().Kind() == reflect.Map) {
|
||||||
|
t.Errorf("wantsTools keeps %s, a copy of its own beside the watched reader", f.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if watches[policiesState] != 0 {
|
||||||
|
t.Errorf("the seat's own small policies state was watched: %v", watches)
|
||||||
|
}
|
||||||
// The process says the split as its finding, and deletes it once they agree.
|
// The process says the split as its finding, and deletes it once they agree.
|
||||||
findings := map[string][]byte{}
|
findings := map[string][]byte{}
|
||||||
p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{
|
p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{
|
||||||
|
|||||||
@@ -352,10 +352,12 @@ func serveTools() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
tools := append(s.tools(), newSeatTools(runtimeStore{}, stdio.Ask).tools()...)
|
tools := append(s.tools(), newSeatTools(runtimeStore{}, stdio.Ask).tools()...)
|
||||||
tools = append(tools, newWantsTools(runtimeStore{}, stdio.Ask, func(e, id string, b any) error {
|
wants := newWantsTools(runtimeStore{}, stdio.Ask, func(e, id string, b any) error {
|
||||||
// The seat's event, said on the seat: a claimed seat's event is never the module's own.
|
// The seat's event, said on the seat: a claimed seat's event is never the module's own.
|
||||||
return stdio.SeatPublish(wantsEventSubject(e), b, id)
|
return stdio.SeatPublish(wantsEventSubject(e), b, id)
|
||||||
}).tools()...)
|
})
|
||||||
|
wants.watched = stdio.NewWatchedStates() // the large states read whole from one watched copy (ADR 0580 clause 3)
|
||||||
|
tools = append(tools, wants.tools()...)
|
||||||
return stdio.Serve("", tools)
|
return stdio.Serve("", tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ type wantsTools struct {
|
|||||||
ask func(key string, body any) (json.RawMessage, error)
|
ask func(key string, body any) (json.RawMessage, error)
|
||||||
emit func(event, id string, body any) error
|
emit func(event, id string, body any) error
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
|
// watched is the copy of the large states it reads whole, each by one watch started on its first read and kept by
|
||||||
|
// its changes after (mesh-sdk's WatchedStates, novox/hq ADR 0580 clause 3), never a get per key, which timed out on
|
||||||
|
// a large state (issue 1953 L5f). Nil reads key by key.
|
||||||
|
watched *stdio.WatchedStates
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantsEventSubject is the subject of one of the seat's events.
|
// wantsEventSubject is the subject of one of the seat's events.
|
||||||
@@ -59,8 +63,16 @@ func (s *wantsTools) leased() bool {
|
|||||||
return away
|
return away
|
||||||
}
|
}
|
||||||
|
|
||||||
// values reads every key of a state.
|
// values reads every key of a state: from the watched copy when there is one, else key by key.
|
||||||
func (s *wantsTools) values(state string) (map[string][]byte, error) {
|
func (s *wantsTools) values(state string) (map[string][]byte, error) {
|
||||||
|
if s.watched != nil {
|
||||||
|
return s.watched.Values(state)
|
||||||
|
}
|
||||||
|
return s.direct(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// direct reads a state key by key, for a small state of the seat's own.
|
||||||
|
func (s *wantsTools) direct(state string) (map[string][]byte, error) {
|
||||||
keys, err := s.st.keys(state)
|
keys, err := s.st.keys(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s cannot be listed: %w", state, err)
|
return nil, fmt.Errorf("%s cannot be listed: %w", state, err)
|
||||||
@@ -78,6 +90,16 @@ func (s *wantsTools) values(state string) (map[string][]byte, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manifest reads a manifest by its digest: kept once read when there is a watched copy, a manifest never changing
|
||||||
|
// under its digest.
|
||||||
|
func (s *wantsTools) manifest(digest string) ([]byte, bool, error) {
|
||||||
|
if s.watched != nil {
|
||||||
|
return s.watched.Once(manifestsState, digest)
|
||||||
|
}
|
||||||
|
raw, _, found, err := s.st.get(manifestsState, digest)
|
||||||
|
return raw, found, err
|
||||||
|
}
|
||||||
|
|
||||||
// manifests is every module the indexer holds, with its trunk manifest and fingerprint.
|
// manifests is every module the indexer holds, with its trunk manifest and fingerprint.
|
||||||
func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]delivery.Module, error) {
|
func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]delivery.Module, error) {
|
||||||
mods, err := s.values(modulesState)
|
mods, err := s.values(modulesState)
|
||||||
@@ -94,7 +116,7 @@ func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]deli
|
|||||||
if m.Manifest == "" {
|
if m.Manifest == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mraw, _, found, err := s.st.get(manifestsState, m.Manifest)
|
mraw, found, err := s.manifest(m.Manifest)
|
||||||
if err != nil || !found {
|
if err != nil || !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -126,7 +148,7 @@ func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
raw, err := s.values(policiesState)
|
raw, err := s.direct(policiesState) // the seat's own small state, read whole after its own writes
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -584,15 +584,9 @@ func (w *world) value(bucket, key string) []byte {
|
|||||||
return w.values[bucket][key]
|
return w.values[bucket][key]
|
||||||
}
|
}
|
||||||
|
|
||||||
// grantsBucket is the bucket the grants are read from now (delivery.GrantSource): the grants part's while the controller's
|
// grantsBucket is the bucket the grants are read from: the grants part's state `grants`, always (ADR 0571 clause 9); no
|
||||||
// lease is handed away, as the lease bucket's key says it (buscompose.LeaseHandedAway), the controller's in every other
|
// manifest but the controller's reads the controller's grants.
|
||||||
// case, a lease not held here included.
|
func (w *world) grantsBucket() string { return delivery.PartGrantsBucket }
|
||||||
func (w *world) grantsBucket() string {
|
|
||||||
raw := w.value(buscompose.LeaseBucket, buscompose.ControllerLeaseKey)
|
|
||||||
away, _ := buscompose.LeaseHandedAway(raw, raw != nil, nil)
|
|
||||||
bucket, _ := delivery.GrantSource(away)
|
|
||||||
return bucket
|
|
||||||
}
|
|
||||||
|
|
||||||
// has says whether a bucket holds a key now.
|
// has says whether a bucket holds a key now.
|
||||||
func (w *world) has(bucket, key string) bool {
|
func (w *world) has(bucket, key string) bool {
|
||||||
|
|||||||
@@ -128,24 +128,22 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) {
|
|||||||
if got := w.changed(delivery.HaveBucket, "gone"); got != 0 {
|
if got := w.changed(delivery.HaveBucket, "gone"); got != 0 {
|
||||||
t.Errorf("changed = %d for a key never seen", got)
|
t.Errorf("changed = %d for a key never seen", got)
|
||||||
}
|
}
|
||||||
// Issue 774: grants are read from the grants part's bucket only while the controller's lease is handed away: a grant
|
// ADR 0571 clause 9: grants are read from the grants part's bucket in every case, a lease held, handed away, absent or
|
||||||
// only the part holds lets a node's put through then, and with a controller's lease (or none, or an unreadable one) it
|
// unreadable alike, and the controller's own grants count for nothing.
|
||||||
// holds it, a grant only the controller holds being the one that counts instead.
|
|
||||||
fp := delivery.SourcePrefix2 + strings.Repeat("d", 64)
|
fp := delivery.SourcePrefix2 + strings.Repeat("d", 64)
|
||||||
gkey, _ := delivery.ControllerGrantKey("web", fp)
|
gkey, _ := delivery.ControllerGrantKey("web", fp)
|
||||||
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}})
|
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}})
|
||||||
for _, row := range []struct {
|
for _, row := range []struct {
|
||||||
name, lease string
|
name, lease string
|
||||||
inPart, inController bool
|
inPart, inController bool
|
||||||
bucket string
|
|
||||||
granted bool
|
granted bool
|
||||||
}{
|
}{
|
||||||
{"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, delivery.PartGrantsBucket, true},
|
{"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, true},
|
||||||
{"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, delivery.PartGrantsBucket, false},
|
{"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, false},
|
||||||
{"a controller holds it, only the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, delivery.ControllerGrantsBucket, false},
|
{"a controller holds it, the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, true},
|
||||||
{"a controller holds it, its own grant", `{"instance":"novox/1"}`, false, true, delivery.ControllerGrantsBucket, true},
|
{"a controller holds it, only its own grant", `{"instance":"novox/1"}`, false, true, false},
|
||||||
{"no lease read", ``, true, false, delivery.ControllerGrantsBucket, false},
|
{"no lease read", ``, true, false, true},
|
||||||
{"an unreadable lease", `[1]`, true, false, delivery.ControllerGrantsBucket, false},
|
{"an unreadable lease", `[1]`, true, false, true},
|
||||||
} {
|
} {
|
||||||
p := newPasser(buckets{}, time.Now())
|
p := newPasser(buckets{}, time.Now())
|
||||||
if row.lease != "" {
|
if row.lease != "" {
|
||||||
@@ -157,8 +155,8 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) {
|
|||||||
if row.inController {
|
if row.inController {
|
||||||
p.w.put(delivery.ControllerGrantsBucket, gkey, grant)
|
p.w.put(delivery.ControllerGrantsBucket, gkey, grant)
|
||||||
}
|
}
|
||||||
if got := p.w.grantsBucket(); got != row.bucket {
|
if got := p.w.grantsBucket(); got != delivery.PartGrantsBucket {
|
||||||
t.Errorf("%s: grants are read from %s, want %s", row.name, got, row.bucket)
|
t.Errorf("%s: grants are read from %s, want the part's", row.name, got)
|
||||||
}
|
}
|
||||||
if _, granted := p.composedFor("ace", "web", fp); granted != row.granted {
|
if _, granted := p.composedFor("ace", "web", fp); granted != row.granted {
|
||||||
t.Errorf("%s: granted = %v, want %v", row.name, granted, row.granted)
|
t.Errorf("%s: granted = %v, want %v", row.name, granted, row.granted)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
reads := slices.Clone(m.Reads)
|
reads := slices.Clone(m.Reads)
|
||||||
slices.Sort(reads)
|
slices.Sort(reads)
|
||||||
want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.composed", "mesh-controller.grants", "mesh-controller.lease",
|
want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.composed", "mesh-controller.lease",
|
||||||
"mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests",
|
"mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests",
|
||||||
"mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-provisioner.provisions", "node-engine.have"}
|
"mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-provisioner.provisions", "node-engine.have"}
|
||||||
if !reflect.DeepEqual(reads, want) {
|
if !reflect.DeepEqual(reads, want) {
|
||||||
@@ -59,7 +59,7 @@ func TestManifest(t *testing.T) {
|
|||||||
t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses)
|
t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses)
|
||||||
}
|
}
|
||||||
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
|
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
|
||||||
// module), says each put's step as the event put, and calls the controller's warranted and the bus node's backup holder.
|
// module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder.
|
||||||
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream
|
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream
|
||||||
// placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its
|
// placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its
|
||||||
// hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches.
|
// hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches.
|
||||||
@@ -82,9 +82,9 @@ func TestManifest(t *testing.T) {
|
|||||||
!slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) {
|
!slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) {
|
||||||
t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays)
|
t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays)
|
||||||
}
|
}
|
||||||
if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.hand-act", "seat:mesh-controller.pin",
|
if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.pin",
|
||||||
"seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin",
|
"seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin",
|
||||||
"seat:mesh-controller.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat, the controller's only while it has no holder
|
"seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone
|
||||||
"seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) {
|
"seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) {
|
||||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"mesh-config.resolved",
|
"mesh-config.resolved",
|
||||||
"mesh-controller.assignments",
|
"mesh-controller.assignments",
|
||||||
"mesh-controller.composed",
|
"mesh-controller.composed",
|
||||||
"mesh-controller.grants",
|
|
||||||
"mesh-controller.lease",
|
"mesh-controller.lease",
|
||||||
"mesh-delivery-builder.build-log",
|
"mesh-delivery-builder.build-log",
|
||||||
"mesh-delivery-builder.builds",
|
"mesh-delivery-builder.builds",
|
||||||
@@ -51,12 +50,10 @@
|
|||||||
],
|
],
|
||||||
"invokes": [
|
"invokes": [
|
||||||
"seat:mesh-controller.assign",
|
"seat:mesh-controller.assign",
|
||||||
"seat:mesh-controller.hand-act",
|
|
||||||
"seat:mesh-controller.pin",
|
"seat:mesh-controller.pin",
|
||||||
"seat:mesh-controller.placement-export",
|
"seat:mesh-controller.placement-export",
|
||||||
"seat:mesh-controller.unassign",
|
"seat:mesh-controller.unassign",
|
||||||
"seat:mesh-controller.unpin",
|
"seat:mesh-controller.unpin",
|
||||||
"seat:mesh-controller.warranted",
|
|
||||||
"seat:mesh-hand-acts.hand-act",
|
"seat:mesh-hand-acts.hand-act",
|
||||||
"seat:mesh-hand-acts.hand-acts",
|
"seat:mesh-hand-acts.hand-acts",
|
||||||
"seat:mesh-hand-acts.warranted",
|
"seat:mesh-hand-acts.warranted",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ import (
|
|||||||
// (novox/hq issue 1937): on 2026-10-24 the store's 03:30 collection deleted the bare blobs of three done keys the
|
// (novox/hq issue 1937): on 2026-10-24 the store's 03:30 collection deleted the bare blobs of three done keys the
|
||||||
// controller had never taken in, and the builder kept them done.
|
// controller had never taken in, and the builder kept them done.
|
||||||
|
|
||||||
// heldVerb answers the controller seat's verb held on the bus, as the serving controller does: {result: {output, ok,
|
// heldVerb answers the seat mesh-artifact-sweep's verb held on the bus, as its holder does: {result: {output, ok,
|
||||||
// answer}}; a reference is gone while gone says so.
|
// answer}}; a reference is gone while gone says so.
|
||||||
type heldVerb struct {
|
type heldVerb struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -29,7 +30,7 @@ type heldVerb struct {
|
|||||||
|
|
||||||
func (h *heldVerb) serve(t *testing.T, b *bed) {
|
func (h *heldVerb) serve(t *testing.T, b *bed) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
sub, err := b.js.Conn().Subscribe("mesh.seat.mesh-controller.call.held.>", func(m *nats.Msg) {
|
sub, err := b.js.Conn().Subscribe("mesh.seat."+catalogue.ArtifactSweepSeat+".call.held.>", func(m *nats.Msg) {
|
||||||
var args struct {
|
var args struct {
|
||||||
References []string `json:"references"`
|
References []string `json:"references"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
|
||||||
|
|
||||||
"mesh-delivery-builder/internal/build"
|
"mesh-delivery-builder/internal/build"
|
||||||
)
|
)
|
||||||
@@ -26,19 +25,14 @@ const SeatHeld = "seat:" + catalogue.ArtifactSweepSeat + ".held"
|
|||||||
// toolCaller calls a tool or a seat verb as the builder's own account (mesh-sdk process.Conn.Call).
|
// toolCaller calls a tool or a seat verb as the builder's own account (mesh-sdk process.Conn.Call).
|
||||||
type toolCaller func(key string, body any) (json.RawMessage, error)
|
type toolCaller func(key string, body any) (json.RawMessage, error)
|
||||||
|
|
||||||
// storedVia is build.Builder's Stored: it asks the controller's verb held (mesh-sdk delivery.HeldVerb; novox/hq issue
|
// storedVia is build.Builder's Stored: it asks the seat mesh-artifact-sweep's `held` (novox/hq ADR 0571 §2, issue 1937)
|
||||||
// 1937) and answers the references the store no longer holds. One it could not ask about, with none gone, is an error:
|
// and answers the references the store no longer holds. One it could not ask about, with none gone, is an error: the key
|
||||||
// the key is then left done, and the controller's take-in still refuses a gone blob.
|
// is then left done, and the controller's take-in still refuses a gone blob. The controller's `held` is not asked: it
|
||||||
//
|
// is no longer a fallback (issue 1955).
|
||||||
// It asks the seat mesh-artifact-sweep's `held` (novox/hq ADR 0571 §2), and the controller's while that seat has no holder that
|
|
||||||
// answers (a named exception of clause 10, until the lease write).
|
|
||||||
func storedVia(call toolCaller) func(ctx context.Context, references []string) ([]string, error) {
|
func storedVia(call toolCaller) func(ctx context.Context, references []string) ([]string, error) {
|
||||||
return func(_ context.Context, references []string) ([]string, error) {
|
return func(_ context.Context, references []string) ([]string, error) {
|
||||||
body := map[string]any{"references": references}
|
body := map[string]any{"references": references}
|
||||||
raw, err := call(SeatHeld, body)
|
raw, err := call(SeatHeld, body)
|
||||||
if handacts.NoResponder(err) {
|
|
||||||
raw, err = call(delivery.HeldVerb, body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("asking held: %w", err)
|
return nil, fmt.Errorf("asking held: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-builder
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a h1:AChSJUmHyOZ5smiuTrtDZNjetYNTFb+anRFjXIQHbSA=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -177,6 +178,42 @@ func (b *Builder) drop(ctx context.Context, ask delivery.StepAsk, old string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultDeclineBound is how long one ask may go on being declined, from its first decline, before its key fails (the
|
||||||
|
// checker's DefaultDeclineBound, issue 890); DefaultDeclinePause is how long the builder waits before it puts a declined
|
||||||
|
// ask again, so a lone node off the agreed toolchain cannot spin on it.
|
||||||
|
const (
|
||||||
|
DefaultDeclineBound = 30 * time.Minute
|
||||||
|
DefaultDeclinePause = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *Builder) declineBound() time.Duration {
|
||||||
|
if b.DeclineBound > 0 {
|
||||||
|
return b.DeclineBound
|
||||||
|
}
|
||||||
|
return DefaultDeclineBound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) declinePause() time.Duration {
|
||||||
|
if b.DeclinePause != 0 {
|
||||||
|
return max(b.DeclinePause, 0)
|
||||||
|
}
|
||||||
|
return DefaultDeclinePause
|
||||||
|
}
|
||||||
|
|
||||||
|
// declinedWords names the nodes that declined an ask and why, in the order of their names.
|
||||||
|
func declinedWords(by map[string]string) string {
|
||||||
|
nodes := make([]string, 0, len(by))
|
||||||
|
for n := range by {
|
||||||
|
nodes = append(nodes, n)
|
||||||
|
}
|
||||||
|
sort.Strings(nodes)
|
||||||
|
var parts []string
|
||||||
|
for _, n := range nodes {
|
||||||
|
parts = append(parts, n+": "+by[n])
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Builder) askings() int {
|
func (b *Builder) askings() int {
|
||||||
if b.Askings > 0 {
|
if b.Askings > 0 {
|
||||||
return b.Askings
|
return b.Askings
|
||||||
@@ -255,6 +292,8 @@ func (b *Builder) unlisten(key, language string, c chan string) {
|
|||||||
var (
|
var (
|
||||||
errCheck = errors.New("the check bound passed")
|
errCheck = errors.New("the check bound passed")
|
||||||
errAgain = errors.New("asked again")
|
errAgain = errors.New("asked again")
|
||||||
|
// errDeclined: a build node on another toolchain than the agreed one sat out of the ask (ADR 0584 clause 2).
|
||||||
|
errDeclined = errors.New("declined")
|
||||||
)
|
)
|
||||||
|
|
||||||
type byHand struct{ why string }
|
type byHand struct{ why string }
|
||||||
@@ -268,11 +307,42 @@ func (b *Builder) answer(ctx context.Context, l *lane, ask delivery.StepAsk, ask
|
|||||||
again := b.listen(key, ask.Language)
|
again := b.listen(key, ask.Language)
|
||||||
defer b.unlisten(key, ask.Language, again)
|
defer b.unlisten(key, ask.Language, again)
|
||||||
asking := askingOf(asked.step, l.module)
|
asking := askingOf(asked.step, l.module)
|
||||||
|
var firstDecline time.Time
|
||||||
|
declines := map[string]string{} // node -> why it declined, the last
|
||||||
for {
|
for {
|
||||||
step, why, err := b.waitOnce(ctx, ask, again)
|
step, why, err := b.waitOnce(ctx, ask, again)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, errCheck):
|
case errors.Is(err, errCheck):
|
||||||
continue
|
continue
|
||||||
|
case errors.Is(err, errDeclined):
|
||||||
|
// Not an asking: a node that sits out would use them all while it idles and pulls the ask again at once.
|
||||||
|
// Bounded by time from the first decline, then failed loudly naming the nodes and their reasons (as the
|
||||||
|
// checker's decline loop is, issue 890).
|
||||||
|
now := time.Now()
|
||||||
|
if firstDecline.IsZero() {
|
||||||
|
firstDecline = now
|
||||||
|
}
|
||||||
|
declines[step.Node] = why
|
||||||
|
if bound := b.declineBound(); now.Sub(firstDecline) > bound {
|
||||||
|
return step, ask, fmt.Errorf("the ask %s of %s was declined for %s and no build node took it: %s", ask.ID, l.module, bound.Round(time.Second), declinedWords(declines))
|
||||||
|
}
|
||||||
|
// The pause is a context's bound, not a timer (ADR 0437 §8).
|
||||||
|
pause, stop := context.WithTimeout(ctx, b.declinePause())
|
||||||
|
<-pause.Done()
|
||||||
|
stop()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return step, ask, ctx.Err()
|
||||||
|
}
|
||||||
|
old := ask.ID
|
||||||
|
ask.ID, ask.At = buildstep.NewID(), time.Now()
|
||||||
|
if err := asked.note(ctx, ask.Language, ask.ID, fmt.Sprintf("put %s again after %s declined it", l.module, step.Node)); err != nil {
|
||||||
|
return step, ask, fmt.Errorf("the ask of %s put again after a decline could not be written to the key: %w", l.module, err)
|
||||||
|
}
|
||||||
|
if err := b.Asks.Put(ctx, ask); err != nil {
|
||||||
|
return step, ask, err
|
||||||
|
}
|
||||||
|
b.drop(ctx, ask, old)
|
||||||
|
b.logf("%s: the ask %s of %s was declined on %s and is put again as %s: %s", key, old, l.module, step.Node, ask.ID, why)
|
||||||
case errors.Is(err, errAgain):
|
case errors.Is(err, errAgain):
|
||||||
if asking >= b.askings() {
|
if asking >= b.askings() {
|
||||||
return step, ask, fmt.Errorf("the ask %s could not be answered and was asked %d times, the most it is: %s", ask.ID, asking, why)
|
return step, ask, fmt.Errorf("the ask %s could not be answered and was asked %d times, the most it is: %s", ask.ID, asking, why)
|
||||||
@@ -317,6 +387,11 @@ func (b *Builder) waitOnce(ctx context.Context, ask delivery.StepAsk, again <-ch
|
|||||||
defer stop()
|
defer stop()
|
||||||
}
|
}
|
||||||
step, err := b.Asks.Wait(wait, ask)
|
step, err := b.Asks.Wait(wait, ask)
|
||||||
|
if _, declined := delivery.DeclinedOf(step); err == nil && declined {
|
||||||
|
// A build node off the agreed toolchain sat out (ADR 0584 clause 2). It is no asking: answer() puts the ask again
|
||||||
|
// after a pause, within DeclineBound.
|
||||||
|
return step, step.Reason, errDeclined
|
||||||
|
}
|
||||||
if err == nil && buildrun.IsLost(step) {
|
if err == nil && buildrun.IsLost(step) {
|
||||||
// The build seat gave the ask up (issue 734): it can no longer be answered, so it is asked again now, with the
|
// The build seat gave the ask up (issue 734): it can no longer be answered, so it is asked again now, with the
|
||||||
// words of why, not at the check bound.
|
// words of why, not at the check bound.
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ type ClaimOptions struct {
|
|||||||
// a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500
|
// a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500
|
||||||
// clause 6). Nil means none is.
|
// clause 6). Nil means none is.
|
||||||
Published func(ref string) bool
|
Published func(ref string) bool
|
||||||
// Stored answers which of the references given the artifact store no longer holds (the controller's verb held,
|
// Stored answers which of the references given the artifact store no longer holds (the seat mesh-artifact-sweep's verb held,
|
||||||
// mesh-sdk delivery.HeldVerb; novox/hq issue 1937). A done key of the trunk's fingerprint is asked before it is left
|
// novox/hq ADR 0571 §2, issue 1937). A done key of the trunk's fingerprint is asked before it is left
|
||||||
// done: the store's nightly collector deletes a blob nothing holds, and a key the controller never took in holds
|
// done: the store's nightly collector deletes a blob nothing holds, and a key the controller never took in holds
|
||||||
// nothing, so a key whose blob is gone is claimed building as a new revision and built again. Nil asks nothing.
|
// nothing, so a key whose blob is gone is claimed building as a new revision and built again. Nil asks nothing.
|
||||||
Stored func(ctx context.Context, references []string) (gone []string, err error)
|
Stored func(ctx context.Context, references []string) (gone []string, err error)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type Builder struct {
|
|||||||
// Answered is told the build module whose step answered a reference (issue 1886): what the builder says it writes
|
// Answered is told the build module whose step answered a reference (issue 1886): what the builder says it writes
|
||||||
// rests on it. Nil tells nobody.
|
// rests on it. Nil tells nobody.
|
||||||
Answered func(buildModule string)
|
Answered func(buildModule string)
|
||||||
// Stored answers which references the artifact store no longer holds (the controller's verb held; issue 1937): a done
|
// Stored answers which references the artifact store no longer holds (the seat mesh-artifact-sweep's verb held; issue 1937): a done
|
||||||
// key of the trunk's fingerprint is asked before it is left done (ClaimOptions.Stored). Nil asks nothing.
|
// key of the trunk's fingerprint is asked before it is left done (ClaimOptions.Stored). Nil asks nothing.
|
||||||
Stored func(ctx context.Context, references []string) (gone []string, err error)
|
Stored func(ctx context.Context, references []string) (gone []string, err error)
|
||||||
// Trunk answers the head of a repository in the gate's source revision rev (source.main at that revision): the trunk a
|
// Trunk answers the head of a repository in the gate's source revision rev (source.main at that revision): the trunk a
|
||||||
@@ -67,6 +67,9 @@ type Builder struct {
|
|||||||
// Askings is how many times one build module's part of a build is asked, the first included, before its key is failed
|
// Askings is how many times one build module's part of a build is asked, the first included, before its key is failed
|
||||||
// with why; DefaultAskings when 0.
|
// with why; DefaultAskings when 0.
|
||||||
Askings int
|
Askings int
|
||||||
|
// DeclineBound and DeclinePause bound and pace the asks build nodes off the agreed toolchain declined (ADR 0584 clause 2):
|
||||||
|
// they are no askings. DefaultDeclineBound and DefaultDeclinePause when 0; a negative DeclinePause puts it again at once.
|
||||||
|
DeclineBound, DeclinePause time.Duration
|
||||||
// Said is told each ask asked again (the builder's event build.asked-again). Nil tells nobody.
|
// Said is told each ask asked again (the builder's event build.asked-again). Nil tells nobody.
|
||||||
Said func(AskedAgain)
|
Said func(AskedAgain)
|
||||||
// Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody.
|
// Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody.
|
||||||
@@ -415,7 +418,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
|
|||||||
if planErr == nil && !nothing {
|
if planErr == nil && !nothing {
|
||||||
if why := b.stale(m, plan); why != "" {
|
if why := b.stale(m, plan); why != "" {
|
||||||
// The fingerprint names a toolchain the build nodes no longer agree on: a new fingerprint is coming, and a build
|
// The fingerprint names a toolchain the build nodes no longer agree on: a new fingerprint is coming, and a build
|
||||||
// now would be of this source with another toolchain (ADR 0477 clause 2). Nothing is written.
|
// now would be of this source with another toolchain (ADR 0584 clause 1, which changes ADR 0477 clause 2). Nothing is written.
|
||||||
b.logf("%s at %s is not built: %s", module, m.FP2, why)
|
b.logf("%s at %s is not built: %s", module, m.FP2, why)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -506,10 +509,11 @@ func (b *Builder) stale(m delivery.Module, plan Plan) string {
|
|||||||
continue // an archive is packed with no toolchain
|
continue // an archive is packed with no toolchain
|
||||||
}
|
}
|
||||||
language := buildrun.LanguageOf[bm]
|
language := buildrun.LanguageOf[bm]
|
||||||
tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language))
|
ag := delivery.ToolchainAgreement(b.Toolchains.Entries(language))
|
||||||
if none != "" {
|
if ag.Wait != "" {
|
||||||
return fmt.Sprintf("the %s toolchain is not agreed: %s", language, none)
|
return fmt.Sprintf("the %s toolchain is not agreed: %s", language, ag.Wait)
|
||||||
}
|
}
|
||||||
|
tc := ag.Toolchain
|
||||||
want, ok := m.Toolchains[language]
|
want, ok := m.Toolchains[language]
|
||||||
if !ok || want.Version != tc.Version || want.Recipe != tc.Recipe {
|
if !ok || want.Version != tc.Version || want.Recipe != tc.Recipe {
|
||||||
return fmt.Sprintf("the fingerprint names %s %s (recipe %s) and the nodes agree on %s (recipe %s)", language, want.Version, want.Recipe, tc.Version, tc.Recipe)
|
return fmt.Sprintf("the fingerprint names %s %s (recipe %s) and the nodes agree on %s (recipe %s)", language, want.Version, want.Recipe, tc.Version, tc.Recipe)
|
||||||
@@ -543,7 +547,8 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
|||||||
repository := delivery.RepositoryName(m.Repository)
|
repository := delivery.RepositoryName(m.Repository)
|
||||||
var lanes []*lane
|
var lanes []*lane
|
||||||
used := map[string]delivery.ToolchainUsed{}
|
used := map[string]delivery.ToolchainUsed{}
|
||||||
var kept []string // build modules whose artifacts stay as the prior done key has them
|
var sitting []string // the nodes that sit out of a language's build, said in the key's step (ADR 0584 clause 2)
|
||||||
|
var kept []string // build modules whose artifacts stay as the prior done key has them
|
||||||
for _, bm := range plan.Modules() {
|
for _, bm := range plan.Modules() {
|
||||||
if prior != nil && len(only) > 0 && !only[bm] {
|
if prior != nil && len(only) > 0 && !only[bm] {
|
||||||
kept = append(kept, bm)
|
kept = append(kept, bm)
|
||||||
@@ -553,10 +558,12 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
|||||||
if bm != ImageBuildModule && plan.NeedsToolchain(bm) {
|
if bm != ImageBuildModule && plan.NeedsToolchain(bm) {
|
||||||
// The ask names the toolchain the fingerprint was computed with; a node building with another refuses it.
|
// The ask names the toolchain the fingerprint was computed with; a node building with another refuses it.
|
||||||
language := buildrun.LanguageOf[bm]
|
language := buildrun.LanguageOf[bm]
|
||||||
tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language))
|
ag := delivery.ToolchainAgreement(b.Toolchains.Entries(language))
|
||||||
if none != "" {
|
if ag.Wait != "" {
|
||||||
return failed("the %s toolchain is not agreed: %s", language, none)
|
return failed("the %s toolchain is not agreed: %s", language, ag.Wait)
|
||||||
}
|
}
|
||||||
|
tc := ag.Toolchain
|
||||||
|
sitting = append(sitting, SittingOutWords(language, ag)...)
|
||||||
if want := m.Toolchains[language]; want.Version != tc.Version || want.Recipe != tc.Recipe {
|
if want := m.Toolchains[language]; want.Version != tc.Version || want.Recipe != tc.Recipe {
|
||||||
return delivery.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written
|
return delivery.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written
|
||||||
}
|
}
|
||||||
@@ -653,6 +660,9 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
step := "built " + strings.Join(steps, "; ")
|
step := "built " + strings.Join(steps, "; ")
|
||||||
|
if len(sitting) > 0 {
|
||||||
|
step += " (" + strings.Join(sitting, "; ") + ")"
|
||||||
|
}
|
||||||
if len(used) == 0 {
|
if len(used) == 0 {
|
||||||
used = nil
|
used = nil
|
||||||
}
|
}
|
||||||
@@ -942,3 +952,13 @@ func LogEnded(module string, m delivery.Module, ref, by string, at time.Time, re
|
|||||||
e.Made = append([]delivery.BuiltArtifact(nil), result.Artifacts...)
|
e.Made = append([]delivery.BuiltArtifact(nil), result.Artifacts...)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SittingOutWords says, per node that is not on the agreed toolchain of a language, that it sits out and which version
|
||||||
|
// it has against the agreed one: "ace sits out: typescript 26.12.1, agreed 26.13.0" (novox/hq ADR 0584 clause 2).
|
||||||
|
func SittingOutWords(language string, a delivery.Agreement) []string {
|
||||||
|
var out []string
|
||||||
|
for _, s := range a.SittingOut {
|
||||||
|
out = append(out, fmt.Sprintf("%s sits out: %s %s, agreed %s", s.Node, language, s.Version, a.Toolchain.Version))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ var (
|
|||||||
|
|
||||||
func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7, ADR 0478 clauses 1, 4
|
func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7, ADR 0478 clauses 1, 4
|
||||||
r := newRig(t, goBundle, archiveA, tsBundle)
|
r := newRig(t, goBundle, archiveA, tsBundle)
|
||||||
|
// ADR 0584: a third node agrees with novox and ace's typescript is the odd one; the build is done and its step says ace sits out.
|
||||||
|
r.tools.Put(buildrun.NodeBuildModule, "g14.typescript", tcValue(t, "typescript", "tsc5"), false)
|
||||||
|
r.tools.Put(buildrun.NodeBuildModule, "ace.typescript", tcValue(t, "typescript", "tsc4"), false)
|
||||||
|
defer func() {
|
||||||
|
if st := r.st.vals[r.key]; !strings.Contains(string(st), "ace sits out: typescript tsc4, agreed tsc5") {
|
||||||
|
t.Errorf("the key does not say ace sits out: %s", st)
|
||||||
|
}
|
||||||
|
}()
|
||||||
var logged []delivery.BuildLogEntry
|
var logged []delivery.BuildLogEntry
|
||||||
var lmu sync.Mutex
|
var lmu sync.Mutex
|
||||||
r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { lmu.Lock(); logged = append(logged, e); lmu.Unlock() }
|
r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { lmu.Lock(); logged = append(logged, e); lmu.Unlock() }
|
||||||
@@ -730,6 +738,25 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|||||||
t.Fatalf("the key = %+v after %d asks, want done from the one ask", v, len(r.asks.Asks()))
|
t.Fatalf("the key = %+v after %d asks, want done from the one ask", v, len(r.asks.Asks()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("declined by a node off the agreed toolchain: put again, no asking used, the bound ends it loudly", func(t *testing.T) { // ADR 0584 clause 2
|
||||||
|
r := newRig(t, goBundle)
|
||||||
|
r.asks.Answer = nil
|
||||||
|
r.b.DeclinePause, r.b.DeclineBound = -1, 150*time.Millisecond
|
||||||
|
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
|
||||||
|
declined, _ := json.Marshal(delivery.StepDeclined{Declined: true, Lacks: []string{"the agreed toolchain go1.27.1 (recipe 1)"}})
|
||||||
|
waitForAsks(t, r, 1)
|
||||||
|
for ended, deadline := 0, time.Now().Add(5*time.Second); valueOf(t, r.st, r.key).State == delivery.BuildBuilding && time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) {
|
||||||
|
if asks := r.asks.Asks(); len(asks) > ended { // each ask put again is declined in its turn
|
||||||
|
r.asks.End(delivery.Step{ID: asks[ended].ID, Node: "ace", State: delivery.StepRefused, Reason: "this node builds with go1.26.0", Result: declined})
|
||||||
|
ended++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.b.Wait()
|
||||||
|
v := valueOf(t, r.st, r.key)
|
||||||
|
if n := len(r.asks.Asks()); v.State != delivery.BuildFailed || n <= DefaultAskings || !strings.Contains(v.Error, "declined for") || !strings.Contains(v.Error, "ace: this node builds with go1.26.0") {
|
||||||
|
t.Fatalf("the key = %+v after %d asks, want it failed after more than %d asks, naming ace and its reason", v, n, DefaultAskings)
|
||||||
|
}
|
||||||
|
})
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
name string
|
name string
|
||||||
askings int
|
askings int
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
|||||||
}
|
}
|
||||||
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
|
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
|
||||||
// other tool; it holds no capacity.
|
// other tool; it holds no capacity.
|
||||||
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's only while the seat has no holder (clause 10).
|
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955).
|
||||||
if want := []string{"seat:mesh-artifact-sweep.held", "seat:mesh-controller.held"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
|
if want := []string{"seat:mesh-artifact-sweep.held"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
|
||||||
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
|
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
|
||||||
}
|
}
|
||||||
// The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it
|
// The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
"mesh-controller.artifacts-gone"
|
"mesh-controller.artifacts-gone"
|
||||||
],
|
],
|
||||||
"invokes": [
|
"invokes": [
|
||||||
"seat:mesh-artifact-sweep.held",
|
"seat:mesh-artifact-sweep.held"
|
||||||
"seat:mesh-controller.held"
|
|
||||||
],
|
],
|
||||||
"state": [
|
"state": [
|
||||||
{ "name": "builds", "history": 5 },
|
{ "name": "builds", "history": 5 },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a h1:AChSJUmHyOZ5smiuTrtDZNjetYNTFb+anRFjXIQHbSA=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -342,6 +342,11 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
|
|||||||
if out.Verdict.State != delivery.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 {
|
if out.Verdict.State != delivery.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 {
|
||||||
t.Fatalf("verdict %+v, asks %+v: want a pass of three asks, the first naming no need", out.Verdict, asks)
|
t.Fatalf("verdict %+v, asks %+v: want a pass of three asks, the first naming no need", out.Verdict, asks)
|
||||||
}
|
}
|
||||||
|
for _, a := range asks { // ADR 0584: every check ask names the agreed toolchain, so a node off it declines
|
||||||
|
if a.Check.Toolchain == nil || a.Check.Toolchain.Version != "go1.27.1" || a.Check.Toolchain.Recipe != "7" {
|
||||||
|
t.Errorf("a check ask names toolchain %+v, want go1.27.1 recipe 7", a.Check.Toolchain)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, a := range asks[1:] {
|
for _, a := range asks[1:] {
|
||||||
if strings.Join(a.Needs, ",") != delivery.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != delivery.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" {
|
if strings.Join(a.Needs, ",") != delivery.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != delivery.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" {
|
||||||
t.Errorf("a placed ask %+v, want the integration level needing node-build-node", a)
|
t.Errorf("a placed ask %+v, want the integration level needing node-build-node", a)
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ type target struct {
|
|||||||
module string
|
module string
|
||||||
node string
|
node string
|
||||||
toolchain string
|
toolchain string
|
||||||
fetched Fetched
|
// agreed is the toolchain the ask names, so a node off it declines the check (ADR 0584); nil when none is agreed.
|
||||||
|
agreed *delivery.ToolchainUsed
|
||||||
|
fetched Fetched
|
||||||
// fetchArgs are the arguments of the fetch its check ask runs.
|
// fetchArgs are the arguments of the fetch its check ask runs.
|
||||||
fetchArgs map[string]any
|
fetchArgs map[string]any
|
||||||
// priority are the Go packages (import paths) or the Node module directories whose test files the merge changes.
|
// priority are the Go packages (import paths) or the Node module directories whose test files the merge changes.
|
||||||
@@ -374,6 +376,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
|
|||||||
t := &target{pull: p, module: bm}
|
t := &target{pull: p, module: bm}
|
||||||
if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
|
if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
|
||||||
t.toolchain = tc.Version
|
t.toolchain = tc.Version
|
||||||
|
t.agreed = &delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe}
|
||||||
}
|
}
|
||||||
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
|
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
|
||||||
if bm == buildrun.NodeBuildModule && groupPackage != nil && p.Repository != SDKRepository {
|
if bm == buildrun.NodeBuildModule && groupPackage != nil && p.Repository != SDKRepository {
|
||||||
@@ -428,7 +431,7 @@ func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *deliver
|
|||||||
func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *delivery.Verdict {
|
func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *delivery.Verdict {
|
||||||
p := t.pull
|
p := t.pull
|
||||||
placed := len(needs) > 0
|
placed := len(needs) > 0
|
||||||
job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages}
|
job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages, Toolchain: t.agreed}
|
||||||
if st.j.Nodes != nil {
|
if st.j.Nodes != nil {
|
||||||
names, err := st.j.Nodes.Names(ctx)
|
names, err := st.j.Nodes.Names(ctx)
|
||||||
if err == nil && len(names) == 0 {
|
if err == nil && len(names) == 0 {
|
||||||
@@ -495,8 +498,12 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
|
|||||||
bound = DefaultDeclineBound
|
bound = DefaultDeclineBound
|
||||||
}
|
}
|
||||||
if now.Sub(firstDecline) > bound {
|
if now.Sub(firstDecline) > bound {
|
||||||
|
lacks := strings.Join(needs, ", ")
|
||||||
|
if lacks == "" {
|
||||||
|
lacks = strings.Join(d.Lacks, ", ") // a node off the agreed toolchain declined it (ADR 0584)
|
||||||
|
}
|
||||||
return fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)",
|
return fail(step.Node, fmt.Sprintf("the check of %s#%d needs a node with %s, and none took it in %s: every node that pulled it lacks it (last %s)",
|
||||||
p.Repository, p.Number, strings.Join(needs, ", "), bound, step.Node))
|
p.Repository, p.Number, lacks, bound, step.Node))
|
||||||
}
|
}
|
||||||
st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", "))
|
st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", "))
|
||||||
a.ID, a.At = buildstep.NewID(), now.UTC()
|
a.ID, a.At = buildstep.NewID(), now.UTC()
|
||||||
|
|||||||
+3
-1
@@ -423,7 +423,9 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
|
|||||||
want := delivery.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
|
want := delivery.Forge{V: delivery.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"},
|
Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"},
|
||||||
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
|
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
|
||||||
Locks: map[string]map[string]string{"modules/a": {"go.sum": hex.EncodeToString(sha256Sum(sum))}}}
|
Locks: map[string]map[string]string{"modules/a": {"go.sum": hex.EncodeToString(sha256Sum(sum))}},
|
||||||
|
// The mesh packages its go.sum names (ADR 0573 §2, issue 1585): this module's own go.sum names mesh-sdk.
|
||||||
|
Packages: map[string][]string{"modules/a": {"git.novox.be/novox/mesh-sdk/go"}}}
|
||||||
if !delivery.SameForge(f, want) {
|
if !delivery.SameForge(f, want) {
|
||||||
t.Errorf("message = %+v, want %+v", f, want)
|
t.Errorf("message = %+v, want %+v", f, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005726-af4e4e60a3a0 h1:q6ywchpTM9fwaJ1UNwDSuOjb5FYDFcsaeiTrKoB4scc=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005726-af4e4e60a3a0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a h1:AChSJUmHyOZ5smiuTrtDZNjetYNTFb+anRFjXIQHbSA=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101143343-67354f8e019a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -50,23 +50,37 @@ const Env = "MESH_CHECK_BESIDE"
|
|||||||
// CapturedSuffix is what a captured file's name carries that the repository's own does not.
|
// CapturedSuffix is what a captured file's name carries that the repository's own does not.
|
||||||
const CapturedSuffix = ".captured"
|
const CapturedSuffix = ".captured"
|
||||||
|
|
||||||
|
// notPlaced are the repositories the checker places beside no mesh-delivery check (besideRules in
|
||||||
|
// mesh-delivery-checker/internal/check/beside.go places mesh-sdk and the catalogue): a test reads them from the captured
|
||||||
|
// copy even in a merge check, and says so (issue 1585). Every other sibling missing from a merge check fails the test.
|
||||||
|
var notPlaced = map[string]bool{"mesh-host": true}
|
||||||
|
|
||||||
// Dir is the named repository a test reads — the clone beside a merge check, or the captured copy — and
|
// Dir is the named repository a test reads — the clone beside a merge check, or the captured copy — and
|
||||||
// says which in the test's log.
|
// says which in the test's log.
|
||||||
func Dir(t testing.TB, repository string) string {
|
func Dir(t testing.TB, repository string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if root := os.Getenv(Env); root != "" {
|
root := os.Getenv(Env)
|
||||||
|
if root != "" {
|
||||||
dir := filepath.Join(root, repository)
|
dir := filepath.Join(root, repository)
|
||||||
if _, err := os.Stat(dir); err != nil {
|
_, err := os.Stat(dir)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Logf("judged against %s as cloned beside this check", dir)
|
||||||
|
return dir
|
||||||
|
case !notPlaced[repository]:
|
||||||
|
// A sibling the checker places that is missing is the checker's fault, never judged against a stale copy.
|
||||||
t.Fatalf("%s is not beside this check in %s=%s, so its agreement with this repository cannot be "+
|
t.Fatalf("%s is not beside this check in %s=%s, so its agreement with this repository cannot be "+
|
||||||
"judged here: %v", repository, Env, root, err)
|
"judged here: %v", repository, Env, root, err)
|
||||||
}
|
}
|
||||||
t.Logf("judged against %s as cloned beside this check", dir)
|
|
||||||
return dir
|
|
||||||
}
|
}
|
||||||
from := filepath.Join(Captured(), repository)
|
from := filepath.Join(Captured(), repository)
|
||||||
if _, err := os.Stat(from); err != nil {
|
if _, err := os.Stat(from); err != nil {
|
||||||
t.Fatalf("no captured copy of %s at %s: %v", repository, from, err)
|
t.Fatalf("no captured copy of %s at %s: %v", repository, from, err)
|
||||||
}
|
}
|
||||||
|
if root != "" {
|
||||||
|
t.Logf("%s is not beside this check in %s=%s (the checker places it beside no mesh-delivery check): read from "+
|
||||||
|
"its captured copy", repository, Env, root)
|
||||||
|
}
|
||||||
dir, err := layOut(repository, from)
|
dir, err := layOut(repository, from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("the captured copy of %s could not be laid out: %v", repository, err)
|
t.Fatalf("the captured copy of %s could not be laid out: %v", repository, err)
|
||||||
|
|||||||
+8
-6
@@ -7,20 +7,22 @@ The copy carries the catalogue's private details: domains, first names in module
|
|||||||
private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is
|
private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is
|
||||||
a second place to clean besides the catalogue itself.
|
a second place to clean besides the catalogue itself.
|
||||||
|
|
||||||
Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 2026-10-21 from the mains of both
|
Only what go/catalogue's tests read is captured (R4b, issue 1379), copied here from mesh-sdk with the judge's tests
|
||||||
repositories; before that it was mesh-controller's copy, which still held two modules that had left the catalogue.
|
(issue 1585). mesh-host refreshed 2026-11-01 from its main, the catalogue 2026-10-21 from its main; before that it
|
||||||
|
was mesh-controller's copy, which still held two modules that had left the catalogue.
|
||||||
|
|
||||||
mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json
|
mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json
|
||||||
mesh-host cdcba9ca73755854d21e4e657d7e46c32b9b5265 internal/apply/placement_guard.go
|
mesh-host 233bb23bbcb1a829ce2a6164a31a5c5cc4f3d950 internal/apply/placement_guard.go
|
||||||
|
|
||||||
To move them, from this repository's go/ directory, with the two repositories checked out beside mesh-sdk:
|
To move them, from mesh-delivery's modules/mesh-delivery-indexer directory, with the two repositories checked out
|
||||||
|
beside mesh-delivery:
|
||||||
|
|
||||||
rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host
|
rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host
|
||||||
mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host/internal/apply
|
mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host/internal/apply
|
||||||
git -C ../../mesh-catalog archive <commit> -- predefined.json ':(glob)modules/*/module.json' \
|
git -C ../../../mesh-catalog archive <commit> -- predefined.json ':(glob)modules/*/module.json' \
|
||||||
':(glob)modules/*/files/**' ':(glob)modules/*/templates/**' | tar -x -C testdata/beside/mesh-catalog
|
':(glob)modules/*/files/**' ':(glob)modules/*/templates/**' | tar -x -C testdata/beside/mesh-catalog
|
||||||
find testdata/beside -name module.json -exec mv {} {}.captured \;
|
find testdata/beside -name module.json -exec mv {} {}.captured \;
|
||||||
git -C ../../mesh-host show <commit>:internal/apply/placement_guard.go \
|
git -C ../../../mesh-host show <commit>:internal/apply/placement_guard.go \
|
||||||
> testdata/beside/mesh-host/internal/apply/placement_guard.go.captured
|
> testdata/beside/mesh-host/internal/apply/placement_guard.go.captured
|
||||||
|
|
||||||
and write the commits here. The placement guard is kept as .captured so no Go tool reads it as this
|
and write the commits here. The placement guard is kept as .captured so no Go tool reads it as this
|
||||||
|
|||||||
Vendored
+99
-4
@@ -56,7 +56,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-host/internal/declaration"
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
)
|
)
|
||||||
|
|
||||||
// protectedRoots are paths no directory, access or mount source may be, nor hold.
|
// protectedRoots are paths no directory, access or mount source may be, nor hold.
|
||||||
@@ -384,6 +384,9 @@ func ownedByAccount(owner string, a homeAccount) bool {
|
|||||||
if owner == a.Name {
|
if owner == a.Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if a.UID < 0 {
|
||||||
|
return false // an account the declaration makes has no uid yet: by name alone, never by a number
|
||||||
|
}
|
||||||
user, _, _ := strings.Cut(owner, ":")
|
user, _, _ := strings.Cut(owner, ":")
|
||||||
if uid, err := strconv.Atoi(user); err == nil {
|
if uid, err := strconv.Atoi(user); err == nil {
|
||||||
return uid == a.UID
|
return uid == a.UID
|
||||||
@@ -413,12 +416,91 @@ const (
|
|||||||
// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module,
|
// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module,
|
||||||
// owner a directory's declared owner.
|
// owner a directory's declared owner.
|
||||||
func refusePath(path string, kind placing, module, owner string) error {
|
func refusePath(path string, kind placing, module, owner string) error {
|
||||||
|
return refusePathWith(path, kind, module, owner, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// declaredHomes is the home of each account the declaration itself makes (a user resource with a home), by the
|
||||||
|
// account's name; the account is not in the user database yet when the declaration is judged, and the engine makes
|
||||||
|
// it where it is declared, before the directory that is owned by it is reached (novox/hq issue 1542). Its uid is
|
||||||
|
// not known, so a directory is owned by it by name alone. Only for the directory placement guard; no other
|
||||||
|
// reader of accountsOfHomes sees these.
|
||||||
|
func declaredHomes(resources []declaration.Resource) map[string]homeAccount {
|
||||||
|
var accounts []passwdEntry
|
||||||
|
var out map[string]homeAccount
|
||||||
|
for _, r := range resources {
|
||||||
|
u, ok := r.(*declaration.User)
|
||||||
|
if !ok || u.Name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
home := filepath.Clean(u.Home)
|
||||||
|
if u.Home == "" || !filepath.IsAbs(home) || home == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if accounts == nil {
|
||||||
|
accounts = passwdEntries()
|
||||||
|
}
|
||||||
|
// Only ever adds a home. An account the machine has keeps the home it has (applyUser never moves it), so
|
||||||
|
// a declared home of one is not its home, unless it is the very home it has: the account an earlier apply
|
||||||
|
// made, as declared, which must be judged the same at every later apply. And a home at or below another
|
||||||
|
// account's, or at or above a protected root, is not a new one.
|
||||||
|
taken := false
|
||||||
|
for _, e := range accounts {
|
||||||
|
if e.name == u.Name && e.home != home {
|
||||||
|
taken = true
|
||||||
|
}
|
||||||
|
if e.name != u.Name && e.home != "" && atOrBelow(home, e.home) {
|
||||||
|
taken = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, root := range protectedRoots {
|
||||||
|
taken = taken || home == root || below(root, home)
|
||||||
|
}
|
||||||
|
if taken {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = map[string]homeAccount{}
|
||||||
|
}
|
||||||
|
out[home] = homeAccount{Name: u.Name, UID: -1}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwdEntry is an account of the user database and its home, whatever its uid.
|
||||||
|
type passwdEntry struct{ name, home string }
|
||||||
|
|
||||||
|
// passwdEntries is every account of the user database with its home (empty and root homes as "").
|
||||||
|
func passwdEntries() []passwdEntry {
|
||||||
|
out := []passwdEntry{}
|
||||||
|
f, err := os.Open(passwdFile)
|
||||||
|
if err != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
fields := strings.Split(sc.Text(), ":")
|
||||||
|
if len(fields) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
home := filepath.Clean(fields[5])
|
||||||
|
if home == "." || home == "/" || home == "/nonexistent" {
|
||||||
|
home = ""
|
||||||
|
}
|
||||||
|
out = append(out, passwdEntry{fields[0], home})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// refusePathWith is refusePath with the homes of the accounts the declaration makes (declaredHomes) counted
|
||||||
|
// among the machine's.
|
||||||
|
func refusePathWith(path string, kind placing, module, owner string, declared map[string]homeAccount) error {
|
||||||
clean := filepath.Clean(path)
|
clean := filepath.Clean(path)
|
||||||
if !filepath.IsAbs(clean) {
|
if !filepath.IsAbs(clean) {
|
||||||
return nil // the declaration refuses a relative path already; a named volume is not a path
|
return nil // the declaration refuses a relative path already; a named volume is not a path
|
||||||
}
|
}
|
||||||
for _, p := range []string{clean, resolved(clean)} {
|
for _, p := range []string{clean, resolved(clean)} {
|
||||||
if err := refuseOne(p, kind, module, owner); err != nil {
|
if err := refuseOne(p, kind, module, owner, declared); err != nil {
|
||||||
if p != clean {
|
if p != clean {
|
||||||
err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why)
|
err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why)
|
||||||
err.Path = clean
|
err.Path = clean
|
||||||
@@ -429,7 +511,7 @@ func refusePath(path string, kind placing, module, owner string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func refuseOne(path string, kind placing, module, owner string) *PlacementRefusedError {
|
func refuseOne(path string, kind placing, module, owner string, declared map[string]homeAccount) *PlacementRefusedError {
|
||||||
for _, root := range protectedRoots {
|
for _, root := range protectedRoots {
|
||||||
if path == root || below(root, path) {
|
if path == root || below(root, path) {
|
||||||
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
||||||
@@ -482,6 +564,18 @@ func refuseOne(path string, kind placing, module, owner string) *PlacementRefuse
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
homes := accountsOfHomes()
|
homes := accountsOfHomes()
|
||||||
|
if ssh && len(declared) > 0 {
|
||||||
|
merged := map[string]homeAccount{}
|
||||||
|
for home, a := range homes {
|
||||||
|
merged[home] = a
|
||||||
|
}
|
||||||
|
for home, a := range declared {
|
||||||
|
if _, held := merged[home]; !held {
|
||||||
|
merged[home] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
homes = merged
|
||||||
|
}
|
||||||
var deepest string
|
var deepest string
|
||||||
for home := range homes {
|
for home := range homes {
|
||||||
if below(path, home) && len(home) > len(deepest) {
|
if below(path, home) && len(home) > len(deepest) {
|
||||||
@@ -518,11 +612,12 @@ func moduleOfID(id string) string {
|
|||||||
// a file into it, because nothing judged the file's path.
|
// a file into it, because nothing judged the file's path.
|
||||||
func refusedPlaces(resources []declaration.Resource) map[string]error {
|
func refusedPlaces(resources []declaration.Resource) map[string]error {
|
||||||
out := map[string]error{}
|
out := map[string]error{}
|
||||||
|
declared := declaredHomes(resources)
|
||||||
for _, r := range resources {
|
for _, r := range resources {
|
||||||
var err error
|
var err error
|
||||||
switch res := r.(type) {
|
switch res := r.(type) {
|
||||||
case *declaration.Directory:
|
case *declaration.Directory:
|
||||||
err = refusePath(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner)
|
err = refusePathWith(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner, declared)
|
||||||
// And outside every home, the walk's judgement before anything is applied (novox/hq issue 358): a
|
// And outside every home, the walk's judgement before anything is applied (novox/hq issue 358): a
|
||||||
// link another account could make in its path refuses it, and with it what is below it, whatever
|
// link another account could make in its path refuses it, and with it what is below it, whatever
|
||||||
// order they are declared in. The apply walks again at the moment of use. A directory handed over
|
// order they are declared in. The apply walks again at the moment of use. A directory handed over
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-teller
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-view
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-watchman
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ func schema(properties map[string]string, required ...string) map[string]any {
|
|||||||
// toolsOf are the part's tools over a part (grants.Tools names them).
|
// toolsOf are the part's tools over a part (grants.Tools names them).
|
||||||
func toolsOf(p *grants.Part) []stdio.Tool {
|
func toolsOf(p *grants.Part) []stdio.Tool {
|
||||||
return []stdio.Tool{
|
return []stdio.Tool{
|
||||||
{Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 6): in this milestone it " +
|
{Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 5, ADR 0571 clause 9): the grants it wrote " +
|
||||||
"writes none (grants.<module>.<fp> from the third milestone, clause 5), and shows beside it the grants the controller " +
|
"(grants.<module>.<fp> in its state grants, the one key the assigner reads), all or one module's with the accounts each names. " +
|
||||||
"wrote (mesh-controller_grants), all or one module's with the accounts each names. Read-only.",
|
"Read-only.",
|
||||||
Input: schema(map[string]string{"module": "one module"}),
|
Input: schema(map[string]string{"module": "one module"}),
|
||||||
Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }},
|
Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }},
|
||||||
{Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " +
|
{Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " +
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-grants
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406 h1:Z4RcH5Ft4OAAG3F538NXIHP9F3pkyk3j0LtP9mqu1qQ=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031230538-c28c645ca406/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3 h1:an2f0MLx9OhaDLu5i51OMPBbuGwX+aoTzqOkFLMpVfE=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101012031-7cab079e2ec3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c h1:rlUCZ5ifLojqDUTkcCCcPrugTzV2RT6F6JbIVTJkaC8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101013319-25a2cecd989c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236 h1:JEkSnrmHs5QIRJGhtrghLC1+C0AxqMpef8a32d/BLPg=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014012-a20bdf795236/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc h1:kVq7sS4I0EnL8DcAA2a3YstnXS6WmYYzJz7eK7IZ9RY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101014310-19c204273bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ type fakeBus struct {
|
|||||||
refuseCreate bool
|
refuseCreate bool
|
||||||
// refuseUsers refuses the bus module's nats_users: the loaded list cannot be read.
|
// refuseUsers refuses the bus module's nats_users: the loaded list cannot be read.
|
||||||
refuseUsers bool
|
refuseUsers bool
|
||||||
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders).
|
// rootFree answers a machine's engine's root-free verb: nil is no answerer at all (no responders).
|
||||||
rootFree func(machines []string) (any, error)
|
rootFree func(machines []string) (any, error)
|
||||||
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
|
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
|
||||||
// consumerCreates each consumer the part asked to create, `<stream>.<name>`.
|
// consumerCreates each consumer the part asked to create, `<stream>.<name>`.
|
||||||
@@ -191,8 +191,13 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
|
|||||||
}
|
}
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
b.calls = append(b.calls, key)
|
|
||||||
args, _ := body.(map[string]any)
|
args, _ := body.(map[string]any)
|
||||||
|
// A node seat's verb is asked of one machine: `<verb>@<machine>`; the call is kept and judged by the verb alone.
|
||||||
|
machine := ""
|
||||||
|
if base, m, ok := strings.Cut(key, "@"); ok {
|
||||||
|
key, machine = base, m
|
||||||
|
}
|
||||||
|
b.calls = append(b.calls, key)
|
||||||
switch key {
|
switch key {
|
||||||
case grants.CallUsers:
|
case grants.CallUsers:
|
||||||
if b.refuseUsers {
|
if b.refuseUsers {
|
||||||
@@ -222,7 +227,7 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
|
|||||||
if b.rootFree == nil {
|
if b.rootFree == nil {
|
||||||
return nil, errors.New("nats: no responders available for request")
|
return nil, errors.New("nats: no responders available for request")
|
||||||
}
|
}
|
||||||
out, err := b.rootFree(args["machines"].([]string))
|
out, err := b.rootFree([]string{machine})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -350,7 +355,6 @@ func fixture() *fakeBus {
|
|||||||
b.put(grants.ReadHave, "b.node", map[string]any{"v": 1})
|
b.put(grants.ReadHave, "b.node", map[string]any{"v": 1})
|
||||||
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
||||||
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}})
|
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}})
|
||||||
b.put(grants.ReadControllerGrant, "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123", delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +617,7 @@ func TestGrants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("root-free: the controller's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
|
t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
|
||||||
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
|
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
|
||||||
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
|
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
|
||||||
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
|
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
|
||||||
@@ -1317,7 +1321,7 @@ func TestGrants(t *testing.T) {
|
|||||||
t.Errorf("membership = %v", ms)
|
t.Errorf("membership = %v", ms)
|
||||||
}
|
}
|
||||||
gr, _ := p.Grants("mod-x")
|
gr, _ := p.Grants("mod-x")
|
||||||
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller_count"] != 1 {
|
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller"] != nil || g["controller_count"] != nil {
|
||||||
t.Errorf("grants = %v", gr)
|
t.Errorf("grants = %v", gr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2064,9 +2068,6 @@ func TestGrants(t *testing.T) {
|
|||||||
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
|
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
|
||||||
}
|
}
|
||||||
held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) }
|
held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) }
|
||||||
// The controller's grant for this fingerprint, beside the stale one the fixture holds for another.
|
|
||||||
delete(b.states[grants.ReadControllerGrant], "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123")
|
|
||||||
b.put(grants.ReadControllerGrant, key("mod-x"), delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
|
|
||||||
hash("a.mod-x")
|
hash("a.mod-x")
|
||||||
hash("a.nats")
|
hash("a.nats")
|
||||||
res := p.Compose()
|
res := p.Compose()
|
||||||
@@ -2091,11 +2092,10 @@ func TestGrants(t *testing.T) {
|
|||||||
if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 {
|
if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 {
|
||||||
t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said())
|
t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said())
|
||||||
}
|
}
|
||||||
// The tool: equal on mod-x, and nats is the part's alone (the controller's fixture holds none).
|
// The tool answers what the part wrote, and reads nothing of the controller's.
|
||||||
got, err := p.Grants("")
|
got, err := p.Grants("")
|
||||||
gm, _ := got.(map[string]any)
|
gm, _ := got.(map[string]any)
|
||||||
diff, _ := gm["differences"].([]grants.GrantDifference)
|
if _, ok := gm["controller"]; err != nil || gm["count"] != 2 || ok {
|
||||||
if err != nil || gm["equal"] != false || len(diff) != 1 || diff[0].Key != key("nats") || diff[0].Kind != "only the part's" {
|
|
||||||
t.Errorf("grants = %+v, %v", got, err)
|
t.Errorf("grants = %+v, %v", got, err)
|
||||||
}
|
}
|
||||||
// A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted.
|
// A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
|
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
|
||||||
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
|
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
|
||||||
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
|
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
|
||||||
// (delivery.GrantSource), and the tool grants says every difference, so the switch is made on a measured equality.
|
//, and the tool grants says every difference, so the switch is made on a measured equality.
|
||||||
//
|
//
|
||||||
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
|
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
|
||||||
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
|
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
|
||||||
@@ -469,38 +469,3 @@ func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant {
|
|||||||
g.V = delivery.Version
|
g.V = delivery.Version
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
// GrantDifference is one key on which the part's grants and the controller's differ.
|
|
||||||
type GrantDifference struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
// Kind is `only the controller's`, `only the part's` or `accounts differ`.
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
Controller []string `json:"controller_accounts,omitempty"`
|
|
||||||
Part []string `json:"part_accounts,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other
|
|
||||||
// accounts. Pure.
|
|
||||||
func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference {
|
|
||||||
keys := map[string]bool{}
|
|
||||||
for k := range controller {
|
|
||||||
keys[k] = true
|
|
||||||
}
|
|
||||||
for k := range part {
|
|
||||||
keys[k] = true
|
|
||||||
}
|
|
||||||
out := []GrantDifference{}
|
|
||||||
for _, k := range sortedKeys(keys) {
|
|
||||||
c, inC := controller[k]
|
|
||||||
g, inP := part[k]
|
|
||||||
switch {
|
|
||||||
case inC && !inP:
|
|
||||||
out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts})
|
|
||||||
case inP && !inC:
|
|
||||||
out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts})
|
|
||||||
case !delivery.SameControllerGrant(withVersion(c), withVersion(g)):
|
|
||||||
out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,14 +24,13 @@ import (
|
|||||||
|
|
||||||
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
|
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
|
||||||
const (
|
const (
|
||||||
ReadAssignments = "mesh-controller.assignments"
|
ReadAssignments = "mesh-controller.assignments"
|
||||||
ReadControllerGrant = "mesh-controller.grants"
|
ReadModules = "mesh-delivery-indexer.modules"
|
||||||
ReadModules = "mesh-delivery-indexer.modules"
|
ReadManifests = "mesh-delivery-indexer.manifests"
|
||||||
ReadManifests = "mesh-delivery-indexer.manifests"
|
ReadHave = "node-engine.have"
|
||||||
ReadHave = "node-engine.have"
|
ReadResolved = "mesh-config.resolved"
|
||||||
ReadResolved = "mesh-config.resolved"
|
ReadWants = "mesh-delivery-assigner.wants"
|
||||||
ReadWants = "mesh-delivery-assigner.wants"
|
ReadBuilds = "mesh-delivery-builder.builds"
|
||||||
ReadBuilds = "mesh-delivery-builder.builds"
|
|
||||||
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
|
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
|
||||||
ReadAccounts = "mesh-vault.bus-accounts"
|
ReadAccounts = "mesh-vault.bus-accounts"
|
||||||
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
|
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
|
||||||
@@ -42,7 +41,7 @@ const (
|
|||||||
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
|
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
|
||||||
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
|
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
|
||||||
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
|
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
|
||||||
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
var Reads = []string{ReadAssignments, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||||
ReadAccounts, ReadPlacement, ReadLease, ReadTakeover}
|
ReadAccounts, ReadPlacement, ReadLease, ReadTakeover}
|
||||||
|
|
||||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||||
@@ -53,7 +52,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Invokes is every tool the part calls.
|
// Invokes is every tool the part calls.
|
||||||
var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant, CallRecordWarrantController}
|
var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant}
|
||||||
|
|
||||||
// Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is
|
// Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is
|
||||||
// an error the part names as a gap and composes past: it never stops for one.
|
// an error the part names as a gap and composes past: it never stops for one.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ var EndedBy = map[string]string{
|
|||||||
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
|
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
|
||||||
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
||||||
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
||||||
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: root-free is the self-check's probe",
|
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)",
|
||||||
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
|
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
|
||||||
DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages",
|
DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import (
|
|||||||
"git.novox.be/novox/mesh-sdk/go/health"
|
"git.novox.be/novox/mesh-sdk/go/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CallRootFree is the controller seat's verb that judges machines root-free (novox/hq ADR 0259 §8): the one judgement
|
// CallRootFree is the node seat's verb that judges one machine root-free (novox/hq ADR 0259 §8, ADR 0563 §6), asked of
|
||||||
// the composer needs and the part cannot make from the bus (issue 1945). Only the controller answers it, on purpose.
|
// that machine's own engine as `seat:node-engine.root-free@<machine>`: the judgement the composer needs and the part
|
||||||
const CallRootFree = "seat:mesh-controller.root-free"
|
// cannot make from the bus (issue 1945). Only that machine's engine answers it, and the controller takes no part in it
|
||||||
|
// (ADR 0571 §11, issue 1955). The messenger asks the same verb the same way.
|
||||||
|
const CallRootFree = "seat:node-engine.root-free"
|
||||||
|
|
||||||
// The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the
|
// The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the
|
||||||
// clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the
|
// clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the
|
||||||
@@ -99,36 +101,45 @@ func RootFreeNodes(r buscompose.Records) []string {
|
|||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// verdict is one machine's answer of the controller's root-free verb.
|
// verdict is one machine's answer of its engine's root-free verb.
|
||||||
type verdict struct {
|
type verdict struct {
|
||||||
Machine string `json:"machine"`
|
Machine string `json:"machine"`
|
||||||
Free bool `json:"free"`
|
Free bool `json:"free"`
|
||||||
Why string `json:"why"`
|
Why string `json:"why"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithRootFree fills r.RootFree for the machines the composition needs by asking the controller once, and answers a
|
// WithRootFree fills r.RootFree for the machines the composition needs by asking each machine's own engine about that
|
||||||
// line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no answer or a machine
|
// machine, and answers a line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no
|
||||||
// left out is not free, and says so. Pure but for the one call; the answer is kept for this pass alone.
|
// answer or a verdict an engine gives on another machine is not free, and says so. Pure but for the calls; the answers
|
||||||
|
// are kept for this pass alone.
|
||||||
func WithRootFree(r *buscompose.Records, call Caller) []string {
|
func WithRootFree(r *buscompose.Records, call Caller) []string {
|
||||||
nodes := RootFreeNodes(*r)
|
nodes := RootFreeNodes(*r)
|
||||||
if len(nodes) == 0 {
|
if len(nodes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
free := map[string]verdict{}
|
free := map[string]verdict{}
|
||||||
var failure string
|
failures := map[string]string{}
|
||||||
if call == nil {
|
for _, n := range nodes {
|
||||||
failure = "no way to ask the controller here"
|
if call == nil {
|
||||||
} else if raw, err := call.Call(CallRootFree, map[string]any{"machines": nodes}); err != nil {
|
failures[n] = "no way to ask the machine's engine here"
|
||||||
failure = "the controller's root-free word could not be asked: " + err.Error()
|
continue
|
||||||
} else {
|
}
|
||||||
|
raw, err := call.Call(CallRootFree+"@"+n, map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
failures[n] = "the engine's root-free word could not be asked: " + err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Machines []verdict `json:"machines"`
|
Machines []verdict `json:"machines"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &body); err != nil {
|
if err := json.Unmarshal(raw, &body); err != nil {
|
||||||
failure = "the controller's root-free answer could not be read: " + err.Error()
|
failures[n] = "the engine's root-free answer could not be read: " + err.Error()
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
for _, v := range body.Machines {
|
for _, v := range body.Machines {
|
||||||
free[v.Machine] = v
|
if v.Machine == n {
|
||||||
|
free[n] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.RootFree = map[string]bool{}
|
r.RootFree = map[string]bool{}
|
||||||
@@ -138,10 +149,10 @@ func WithRootFree(r *buscompose.Records, call Caller) []string {
|
|||||||
switch {
|
switch {
|
||||||
case answered && v.Free:
|
case answered && v.Free:
|
||||||
r.RootFree[n] = true
|
r.RootFree[n] = true
|
||||||
case failure != "":
|
case failures[n] != "":
|
||||||
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failure))
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failures[n]))
|
||||||
case !answered:
|
case !answered:
|
||||||
notes = append(notes, fmt.Sprintf("%s: not free, the controller named no verdict for it", n))
|
notes = append(notes, fmt.Sprintf("%s: not free, its engine named no verdict for it", n))
|
||||||
default:
|
default:
|
||||||
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why))
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,12 +519,8 @@ func (p *Part) TakeOverState() (any, error) {
|
|||||||
// Module is the grants part's module, the asker of its take-over and hand-back.
|
// Module is the grants part's module, the asker of its take-over and hand-back.
|
||||||
const Module = "mesh-grants"
|
const Module = "mesh-grants"
|
||||||
|
|
||||||
// The hand-act log's verbs the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's, and the
|
// The hand-act log's verb the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's.
|
||||||
// controller's while the seat has no holder and the controller holds its lease (handacts.Recorder).
|
const CallRecordWarrant = "seat:" + handacts.Seat + ".warranted"
|
||||||
const (
|
|
||||||
CallRecordWarrant = "seat:" + handacts.Seat + ".warranted"
|
|
||||||
CallRecordWarrantController = "seat:mesh-controller.warranted"
|
|
||||||
)
|
|
||||||
|
|
||||||
// recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask.
|
// recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask.
|
||||||
// Said, never fatal: the switch stands either way.
|
// Said, never fatal: the switch stands either way.
|
||||||
|
|||||||
@@ -31,12 +31,11 @@ const (
|
|||||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
||||||
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck}
|
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck}
|
||||||
|
|
||||||
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the
|
// WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's
|
||||||
// controller's lease is handed away (delivery.GrantSource).
|
// accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5).
|
||||||
const WritesGrants = "in shadow: the grants part writes grants.<module>.<fp> into its state grants, which the assigner reads only once the controller's lease is handed away (ADR 0547 clause 5); the controller's are beside, with every difference"
|
const WritesGrants = "the grants part writes grants.<module>.<fp> into its state grants, once the list the bus loaded carries that build's accounts; it is the one key the assigner reads (ADR 0571 clause 9)"
|
||||||
|
|
||||||
// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, one
|
// Grants answers the tool grants: the grants the part wrote, one module's or all.
|
||||||
// module's or all.
|
|
||||||
func (p *Part) Grants(module string) (any, error) {
|
func (p *Part) Grants(module string) (any, error) {
|
||||||
out := map[string]any{"writes": WritesGrants}
|
out := map[string]any{"writes": WritesGrants}
|
||||||
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) {
|
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) {
|
||||||
@@ -65,10 +64,6 @@ func (p *Part) Grants(module string) (any, error) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
controller, cerr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
|
|
||||||
keys, err := p.Read.Keys(ReadControllerGrant)
|
|
||||||
return keys, func(k string) ([]byte, bool, error) { return p.Read.Get(ReadControllerGrant, k) }, err
|
|
||||||
})
|
|
||||||
mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
|
mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
|
||||||
keys, err := p.Store.Keys(StateGrants)
|
keys, err := p.Store.Keys(StateGrants)
|
||||||
return keys, func(k string) ([]byte, bool, error) {
|
return keys, func(k string) ([]byte, bool, error) {
|
||||||
@@ -89,21 +84,11 @@ func (p *Part) Grants(module string) (any, error) {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
if cerr != nil {
|
|
||||||
out["controller_unread"] = cerr.Error()
|
|
||||||
} else {
|
|
||||||
out["controller"], out["controller_count"] = list(controller), len(controller)
|
|
||||||
}
|
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
out["written_unread"] = perr.Error()
|
out["written_unread"] = perr.Error()
|
||||||
} else {
|
} else {
|
||||||
out["written"], out["count"] = list(mine), len(mine)
|
out["written"], out["count"] = list(mine), len(mine)
|
||||||
}
|
}
|
||||||
if cerr == nil && perr == nil {
|
|
||||||
diff := CompareGrants(controller, mine)
|
|
||||||
out["differences"], out["differing"] = diff, len(diff)
|
|
||||||
out["equal"] = len(diff) == 0
|
|
||||||
}
|
|
||||||
if last := p.Last(); last.Grants != nil {
|
if last := p.Last(); last.Grants != nil {
|
||||||
out["last_pass"] = last.Grants
|
out["last_pass"] = last.Grants
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"reads": [
|
"reads": [
|
||||||
"mesh-config.resolved",
|
"mesh-config.resolved",
|
||||||
"mesh-controller.assignments",
|
"mesh-controller.assignments",
|
||||||
"mesh-controller.grants",
|
|
||||||
"mesh-delivery-assigner.wants",
|
"mesh-delivery-assigner.wants",
|
||||||
"mesh-delivery-builder.builds",
|
"mesh-delivery-builder.builds",
|
||||||
"mesh-delivery-indexer.manifests",
|
"mesh-delivery-indexer.manifests",
|
||||||
@@ -30,9 +29,8 @@
|
|||||||
"nats.nats_streams",
|
"nats.nats_streams",
|
||||||
"nats.nats_user_can",
|
"nats.nats_user_can",
|
||||||
"nats.nats_users",
|
"nats.nats_users",
|
||||||
"seat:mesh-controller.root-free",
|
"seat:node-engine.root-free",
|
||||||
"seat:mesh-hand-acts.warranted",
|
"seat:mesh-hand-acts.warranted"
|
||||||
"seat:mesh-controller.warranted"
|
|
||||||
],
|
],
|
||||||
"emits": [
|
"emits": [
|
||||||
"asserted",
|
"asserted",
|
||||||
|
|||||||
Reference in New Issue
Block a user