From 14d10051e51c86e514f2b6e49ede5da3068e1b66 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 13:11:00 +0100 Subject: [PATCH] Dial a route at the machine port its module's placement assigned, from the mesh-sdk whose plans carry it (issue 774, research 072 row 34) route-proxy from plans dialled every route at the software's own port: gitea at 127.0.0.1:3000 (published at 20000) and grafana on ace at 3000 (published at 20012), and public sites answered 502. The proxy already reaches a module beside it over loopback and one elsewhere at the plan's address; the plan now carries the assigned port, and a test composes the plan with the SDK and reads it as the proxy does, so the two cannot drift again. The controller, NATS-native: no piece moves; route-proxy only. --- .../cmd/route-proxy/composed_test.go | 71 +++++++++++++++++++ modules/route-proxy/cmd/route-proxy/plans.go | 6 +- modules/route-proxy/go.mod | 2 +- modules/route-proxy/go.sum | 4 +- 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 modules/route-proxy/cmd/route-proxy/composed_test.go diff --git a/modules/route-proxy/cmd/route-proxy/composed_test.go b/modules/route-proxy/cmd/route-proxy/composed_test.go new file mode 100644 index 00000000..e9bab983 --- /dev/null +++ b/modules/route-proxy/cmd/route-proxy/composed_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/provisions" +) + +// A route is dialled at the machine port the module's placement assigned, never its software's own (issue 774, research +// 072 row 34; 2026-10-31: gitea's 3000 for git, published at 20000, and grafana's 3000 on another machine, published at +// 20012, answered 502): over loopback when the module runs beside the proxy, at its machine's address otherwise. The +// plan is composed by the SDK as the seat mesh-provisioner composes it, and read as this proxy reads it; a port the +// placement assigned none is refused there, so no plan names it. +func TestAComposedPlanIsDialledAtTheAssignedPort(t *testing.T) { + proxy := catalogue.Manifest{Module: "route-proxy", Version: "1", + Provides: []catalogue.Offer{{Name: "route", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}} + world := catalogue.World{Offered: map[string][]catalogue.Provider{ + "route": {{Node: "anchor", At: "anchor.internal", Module: "route-proxy"}}}} + for _, c := range []struct { + name, node, at, module string + ports map[int]int + target, refused string + }{ + {"a module beside the proxy", "anchor", "anchor.internal", "gitea", map[int]int{3000: 20000}, + "http://127.0.0.1:20000", ""}, + {"a module on another machine", "home", "home.internal", "grafana", map[int]int{3000: 20012}, + "http://home.internal:20012", ""}, + {"a module whose port the placement assigned none", "home", "home.internal", "grafana", nil, + "", "grafana contributing to route: its port 3000 on home has no machine port assigned"}, + } { + module := catalogue.Manifest{Module: c.module, Version: "1", Requires: []string{"route"}, + Listens: []catalogue.Listening{{Name: "web", Port: 3000, Protocol: "tcp", From: "mesh"}}, + Contributes: map[string]map[string]any{"route": {"label": c.module, "endpoint": "web"}}} + node := catalogue.Node{Name: c.node, At: c.at, PublicDomain: "example.org", + Capabilities: map[string]bool{"container-runtime": true}} + r, err := catalogue.Resolve(map[string]catalogue.Manifest{module.Module: module, proxy.Module: proxy}, + []string{module.Module}, node, world) + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + r.Account = "op" + for i, n := range r.Needs { + r.Needs[i].AnsweredByHelper = n.Name == "route" // as the plan sets it (compose's answeredByHelper) + } + composed, err := r.Compose(catalogue.Rendering{Hands: true, Provisioner: "mesh-provisioner-client", + ArtifactStore: "store:5000", Planned: true, Ports: map[string]map[int]int{c.module: c.ports}}) + if c.refused != "" { + if err == nil || !strings.Contains(err.Error(), c.refused) { + t.Errorf("%s: composed with %v, want refused with %q", c.name, err, c.refused) + } + continue + } + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + value, err := provisions.EncodePlan(provisions.Plan{Node: c.node, Module: c.module, Plan: composed.Plans[c.module]}) + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + given, refused, err := routesOfPlan("anchor", c.node+"."+c.module, value) + if err != nil || len(refused) != 0 { + t.Fatalf("%s: the plan gives %v, refused %v (%v)", c.name, given, refused, err) + } + routes, _ := routesOf(given) + if got := targetOf(routes, c.module+".example.org"); got != c.target { + t.Errorf("%s: dialled at %q, want %q (routes %v)", c.name, got, c.target, routes) + } + } +} diff --git a/modules/route-proxy/cmd/route-proxy/plans.go b/modules/route-proxy/cmd/route-proxy/plans.go index d2623b87..e10c6275 100644 --- a/modules/route-proxy/cmd/route-proxy/plans.go +++ b/modules/route-proxy/cmd/route-proxy/plans.go @@ -5,9 +5,9 @@ package main // // **A route is a provision, in route-proxy's own contract.** A module that must be reachable requires `route`; the // controller composes its plan on its node, and the plan's entry for the route names this proxy as its provider and -// carries what the module contributes, composed (its names joined to the node's domains, its endpoint's port), and -// where the module's machine is on the private network, as one opaque payload the seat never reads (mesh-sdk -// catalogue.PlanProvision.Contributed). The proxy decodes each contribution with its contract (contract.DecodeRequest) +// carries what the module contributes, composed (its names joined to the node's domains, the machine port its +// placement assigned its endpoint, never the software's own), and where the module's machine is on the private +// network, as one opaque payload the seat never reads (mesh-sdk catalogue.PlanProvision.Contributed). The proxy decodes each contribution with its contract (contract.DecodeRequest) // and serves every entry naming it. There is no file and no second source: a route the plans do not name is not // served, and a route the contract refuses is said refused (contract.EventRefused), never served half. diff --git a/modules/route-proxy/go.mod b/modules/route-proxy/go.mod index 8be93094..00a3c7e7 100644 --- a/modules/route-proxy/go.mod +++ b/modules/route-proxy/go.mod @@ -3,7 +3,7 @@ module route-proxy go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031120835-b61102245706 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 ) diff --git a/modules/route-proxy/go.sum b/modules/route-proxy/go.sum index 6fbcb8c2..5c38d7e5 100644 --- a/modules/route-proxy/go.sum +++ b/modules/route-proxy/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 h1:89jE1oBK+l7gFJjz6wx8mUD3KxrqSDa7pUTlQkzC1do= git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0/go.mod h1:Yr+Xp89kpMiKH6kA06EU+d9I2CyzrK9yMX7J3pSv0hE= -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.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031120835-b61102245706 h1:wQ+AhLzvRYyWY58lYaAp/P3OdAZZKIHDKj6oEDd/O6U= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031120835-b61102245706/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/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=