Merge branch 'feat/47-G-fix9-routeproxy' into feat/47-G-join9

This commit is contained in:
2026-11-12 01:46:51 +01:00
4 changed files with 96 additions and 0 deletions
@@ -5529,6 +5529,19 @@ func StateProblems(m catalogue.Manifest) []string {
}
}
for _, r := range m.Reads {
if seat, state, isSeat, err := catalogue.SeatRead(r); isSeat {
// A read of a seat holder's state (design 47 G A-116, A-128): the seat declares the state; the holder is resolved
// where it is read.
switch s, known := catalogue.SeatNamed(seat); {
case err != nil:
problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err))
case !known:
problems = append(problems, fmt.Sprintf("%s reads %q, a state of the seat %q, "+catalogue.UndeclaredSeat, m.Module, r, seat))
case !slices.Contains(s.States, state):
problems = append(problems, fmt.Sprintf("%s reads %q, and the seat %s declares no state %q (design 47 G A-128)", m.Module, r, seat, state))
}
continue
}
module, _, err := catalogue.ReadState(r)
if err != nil {
problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err))
@@ -6416,3 +6416,22 @@ func TestRulesOther(t *testing.T) {
t.Run("TakesSettingsIsRefusedWhereItCannotMeanAnything", caseTakesSettingsIsRefusedWhereItCannotMeanAnything)
t.Run("TheInstallationCheckReadsADefault", caseTheInstallationCheckReadsADefault)
}
// A read of a seat holder's state, `seat:<seat>.<state>`, is accepted where the seat declares the state and refused, by name,
// where it does not or the seat is unknown (design 47 G A-116, A-128).
func TestASeatNamedReadIsJudgedAgainstTheSeatsStates(t *testing.T) {
was := catalogue.Seats()
t.Cleanup(func() { catalogue.UseSeats(was) })
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-provisioner", Scope: catalogue.ScopeMesh, Decision: "test", States: []string{"plans"}}})
for _, row := range []struct{ read, says string }{
{"seat:mesh-provisioner.plans", ""},
{"seat:mesh-provisioner.nothing", `declares no state "nothing"`},
{"seat:mesh-unknown.plans", "mesh-unknown"},
{"seat:mesh-provisioner", "does not name a seat and its state"},
} {
problems := strings.Join(StateProblems(catalogue.Manifest{Module: "reader", Reads: []string{row.read}}), "\n")
if row.says == "" && problems != "" || row.says != "" && !strings.Contains(problems, row.says) {
t.Errorf("%s: problems %q, want %q", row.read, problems, row.says)
}
}
}
+33
View File
@@ -4964,6 +4964,39 @@ func TestTheGrantsPartKeepsWhatEachNodeRuns(t *testing.T) {
}
}
// A module reading a seat holder's state, `seat:<seat>.<state>`, is granted the read of the module holding the seat on record
// (design 47 G A-116).
func TestASeatNamedReadIsResolvedToItsHolder(t *testing.T) {
for _, row := range []struct {
name, holder string
reads, want []string
}{
{"resolved to the holder on record", "prov-one", []string{"seat:mesh-provisioner.plans", "mesh-vault.node-logins"},
[]string{"prov-one.plans", "mesh-vault.node-logins"}},
{"a new holder is read with no change of the manifest", "prov-two", []string{"seat:mesh-provisioner.provisions"},
[]string{"prov-two.provisions"}},
{"no holder on record stays as written", "", []string{"seat:mesh-provisioner.plans"}, []string{"seat:mesh-provisioner.plans"}},
} {
t.Run(row.name, func(t *testing.T) {
in := grants.Inputs{Nodes: []string{"a"}, Assigned: map[string][]string{"a": {"mod-t"}},
Manifests: map[string]catalogue.Manifest{"mod-t": {Module: "mod-t", Version: "1", RunsAs: "mod-t", Reads: row.reads,
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mod-t/broker"}}}}}
if row.holder != "" {
in.Placed = map[string]meshmoduleassigner.Assignments{"a": {Node: "a",
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-provisioner", Scope: "mesh", Module: row.holder}}}}
}
recs, _ := grants.ListRecords(in)
var got []string
for _, d := range recs.Assigned["a"] {
got = d.Reads
}
if !slices.Equal(got, row.want) {
t.Errorf("reads = %v, want %v", got, row.want)
}
})
}
}
// A module naming a seat's view, `seat:<seat>.views.<state>`, is granted the view of the module holding the seat on record, so a
// new holder is granted with no change of its manifest (design 47 G §616).
func TestASeatNamedViewIsResolvedToItsHolder(t *testing.T) {
@@ -88,6 +88,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
}
declare := func(m catalogue.Manifest) buscompose.Declared {
m.Consumes = resolveSeatViews(m.Consumes, holders)
m.Reads = resolveSeatReads(m.Reads, holders)
d := declaredFor(m, seats, declarers)
if len(in.Placed) > 0 {
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
@@ -213,6 +214,36 @@ func resolveSeatViews(consumes []string, holders []catalogue.Held) []string {
return out
}
// resolveSeatReads resolves each `seat:<seat>.<state>` of a module's reads to `<holder>.<state>`, the module holding the seat
// on record, as resolveSeatViews does a view: a new holder is read with no change of the manifest (design 47 G A-116). An entry
// whose seat has no holder on record stays as written, and grants nothing. Pure.
func resolveSeatReads(reads []string, holders []catalogue.Held) []string {
var out []string
for i, r := range reads {
rest, isSeat := strings.CutPrefix(r, seatViewConsume)
seatName, state, named := strings.Cut(rest, ".")
if !isSeat || !named || state == "" || strings.Contains(state, ".") {
if out != nil {
out = append(out, r)
}
continue
}
if out == nil {
out = slices.Clone(reads[:i])
}
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
if holder < 0 {
out = append(out, r)
continue
}
out = append(out, holders[holder].Module+"."+state)
}
if out == nil {
return reads
}
return out
}
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
// alike.