Serve route-proxy's routes through its own contract: decode each plan entry's opaque contribution with it, say each name served, withdrawn or refused on the bus, and take who is inside from the seat mesh-placement, since the membership went with the assignments stream (issue 774, research 072 rows 33 and 34)

This commit is contained in:
2026-10-31 09:44:07 +01:00
parent efe745f2be
commit 22935b9202
15 changed files with 410 additions and 477 deletions
@@ -14,7 +14,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
"route-proxy/internal/routestate"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
)
func say(format string, args ...any) {
@@ -36,7 +36,7 @@ func main() {
}
}()
}
tools := front{routes: stdio.State(routestate.RoutesState), unrouted: stdio.State(routestate.UnroutedState),
tools := front{routes: stdio.State(contract.RoutesState), unrouted: stdio.State(contract.UnroutedState),
now: time.Now}.tools()
if err := stdio.Serve("", tools); err != nil {
say("%v", err)
@@ -13,13 +13,13 @@ package main
import (
"git.novox.be/novox/mesh-sdk/go/provider"
"route-proxy/internal/routestate"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
)
// ProvisionName is what route-proxy provides, as its manifest says, and ModuleName this module.
const (
ProvisionName = routestate.Provision
ModuleName = routestate.Module
ProvisionName = contract.Provision
ModuleName = contract.Module
)
// newProvider is route-proxy answering its consumers over bus on node: the route, served as the manifest serves it
+20 -20
View File
@@ -14,14 +14,14 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"route-proxy/internal/routestate"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
)
// The tools' names, as the manifest declares them.
const (
RoutesTool = "route_proxy_routes"
RouteTool = "route_proxy_route"
UnroutedTool = "route_proxy_unrouted"
RoutesTool = contract.ToolRoutes
RouteTool = contract.ToolRoute
UnroutedTool = contract.ToolUnrouted
)
// reader is one of the module's states, as stdio.KeptState reads it.
@@ -46,9 +46,9 @@ type RouteLine struct {
// unknown before any request.
Reachable string `json:"reachable"`
// Certificate is its outcome, issuer and expiry in words; ExpiresInDays is the whole days until not-after.
Certificate string `json:"certificate"`
ExpiresInDays *int `json:"expires_in_days,omitempty"`
LastAnswer *routestate.Answer `json:"last_answer,omitempty"`
Certificate string `json:"certificate"`
ExpiresInDays *int `json:"expires_in_days,omitempty"`
LastAnswer *contract.Answer `json:"last_answer,omitempty"`
}
func (f front) tools() []stdio.Tool {
@@ -99,7 +99,7 @@ func read[T any](state reader, node string) ([]T, error) {
}
var out []T
for _, key := range keys {
at, _ := routestate.Split(key)
at, _ := contract.Split(key)
if node != "" && at != node {
continue
}
@@ -120,7 +120,7 @@ func read[T any](state reader, node string) ([]T, error) {
}
func (f front) list(node string) (map[string]any, error) {
routes, err := read[routestate.Route](f.routes, node)
routes, err := read[contract.Route](f.routes, node)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func (f front) list(node string) (map[string]any, error) {
if line.Reachable == "no" {
unreachable++
}
if line.ExpiresInDays != nil && *line.ExpiresInDays < 14 || r.Certificate.Outcome == routestate.Failed {
if line.ExpiresInDays != nil && *line.ExpiresInDays < 14 || r.Certificate.Outcome == contract.Failed {
expiring++
}
lines = append(lines, line)
@@ -146,7 +146,7 @@ func (f front) list(node string) (map[string]any, error) {
"certificates_failing_or_within_14_days": expiring}, nil
}
func (f front) line(r routestate.Route) RouteLine {
func (f front) line(r contract.Route) RouteLine {
line := RouteLine{Name: r.Name, Proxy: r.Proxy, Reach: r.Reach, LastAnswer: r.LastAnswer, Reachable: "unknown",
Targets: []string{}}
for _, t := range r.Targets {
@@ -182,7 +182,7 @@ func (f front) line(r routestate.Route) RouteLine {
}
// describe is one rule in a few words: path → module@node at target, or what it answers with.
func describe(t routestate.Target) string {
func describe(t contract.Target) string {
path := t.Path
if path == "" {
path = "/"
@@ -209,11 +209,11 @@ func (f front) one(name, node string) (map[string]any, error) {
if name == "" {
return nil, fmt.Errorf("name is the site to read, such as app.example.org")
}
routes, err := read[routestate.Route](f.routes, node)
routes, err := read[contract.Route](f.routes, node)
if err != nil {
return nil, err
}
var found []routestate.Route
var found []contract.Route
for _, r := range routes {
if r.Name == name {
found = append(found, r)
@@ -248,32 +248,32 @@ func (f front) refusals(node string) (map[string]any, error) {
}
sort.Slice(asked, func(i, j int) bool { return asked[i].AskedAt.After(asked[j].AskedAt) })
if asked == nil {
asked = []routestate.Unrouted{}
asked = []contract.Unrouted{}
}
return map[string]any{"unrouted": asked, "names": len(asked), "other_refusals": others}, nil
}
// asked is the names refused and kept by name, and each proxy's count of the refusals not kept by name.
func (f front) asked(node string) ([]routestate.Unrouted, []routestate.Other, error) {
func (f front) asked(node string) ([]contract.Unrouted, []contract.Other, error) {
keys, err := f.unrouted.Keys()
if err != nil {
return nil, nil, err
}
names, others := keySet{}, keySet{}
for _, key := range keys {
if _, name := routestate.Split(key); name == routestate.OtherName {
if _, name := contract.Split(key); name == contract.OtherName {
others[key] = true
} else {
names[key] = true
}
}
asked, err := read[routestate.Unrouted](only{f.unrouted, names}, node)
asked, err := read[contract.Unrouted](only{f.unrouted, names}, node)
if err != nil {
return nil, nil, err
}
counted, err := read[routestate.Other](only{f.unrouted, others}, node)
counted, err := read[contract.Other](only{f.unrouted, others}, node)
if counted == nil {
counted = []routestate.Other{}
counted = []contract.Other{}
}
return asked, counted, err
}
@@ -9,7 +9,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"route-proxy/internal/routestate"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
)
// held is a state as the bus holds it, read-only.
@@ -40,19 +40,19 @@ func TestTheWebFrontsToolsAnswerFromTheBus(t *testing.T) {
now := time.Date(2026, 10, 30, 21, 0, 0, 0, time.UTC)
expires := now.Add(30 * 24 * time.Hour)
routes := held{
"anchor.grafana.example": routestate.Route{Name: "grafana.example", Proxy: "anchor", Reach: routestate.ReachPublic,
Targets: []routestate.Target{{Module: "grafana", Node: "home-server", Target: "http://10.0.0.2:3000"}},
Certificate: routestate.Certificate{Authority: routestate.ReachPublic, Issuer: "Let's Encrypt R11", NotAfter: &expires, Outcome: routestate.Issued},
LastAnswer: &routestate.Answer{Status: 502, At: now, Error: "dial tcp 10.0.0.2:3000: connect: connection refused"}},
"anchor.drive.example": routestate.Route{Name: "drive.example", Proxy: "anchor", Reach: routestate.ReachPublic,
Targets: []routestate.Target{{Module: "nextcloud", Node: "home-server", Target: "http://10.0.0.2:8080"}},
Certificate: routestate.Certificate{Authority: routestate.ReachPublic},
LastAnswer: &routestate.Answer{Status: 200, At: now}},
"anchor.grafana.example": contract.Route{Name: "grafana.example", Proxy: "anchor", Reach: contract.ReachPublic,
Targets: []contract.Target{{Module: "grafana", Node: "home-server", Target: "http://10.0.0.2:3000"}},
Certificate: contract.Certificate{Authority: contract.ReachPublic, Issuer: "Let's Encrypt R11", NotAfter: &expires, Outcome: contract.Issued},
LastAnswer: &contract.Answer{Status: 502, At: now, Error: "dial tcp 10.0.0.2:3000: connect: connection refused"}},
"anchor.drive.example": contract.Route{Name: "drive.example", Proxy: "anchor", Reach: contract.ReachPublic,
Targets: []contract.Target{{Module: "nextcloud", Node: "home-server", Target: "http://10.0.0.2:8080"}},
Certificate: contract.Certificate{Authority: contract.ReachPublic},
LastAnswer: &contract.Answer{Status: 200, At: now}},
}
unrouted := held{
"anchor.old.example": routestate.Unrouted{Name: "old.example", Proxy: "anchor", AskedAt: now.Add(-time.Hour), Times: 3},
"anchor.wiki.example": routestate.Unrouted{Name: "wiki.example", Proxy: "anchor", AskedAt: now, Times: 1},
"anchor._other": routestate.Other{Proxy: "anchor", Minute: now, LastMinute: 40, SinceStart: 900},
"anchor.old.example": contract.Unrouted{Name: "old.example", Proxy: "anchor", AskedAt: now.Add(-time.Hour), Times: 3},
"anchor.wiki.example": contract.Unrouted{Name: "wiki.example", Proxy: "anchor", AskedAt: now, Times: 1},
"anchor._other": contract.Other{Proxy: "anchor", Minute: now, LastMinute: 40, SinceStart: 900},
}
tools := map[string]stdio.Tool{}
for _, tool := range (front{routes: routes, unrouted: unrouted, now: func() time.Time { return now }}).tools() {