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/cmd/route-proxy/routes_test.go b/modules/route-proxy/cmd/route-proxy/routes_test.go index 836dbb56..edf01d0a 100644 --- a/modules/route-proxy/cmd/route-proxy/routes_test.go +++ b/modules/route-proxy/cmd/route-proxy/routes_test.go @@ -10,6 +10,9 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/provisions" + "git.novox.be/novox/mesh-controller/modules/route-proxy/contract" ) @@ -151,17 +154,81 @@ func TestARouteWithNoInternalNameGetsNoAlias(t *testing.T) { } } -// A workload beside the proxy is ordinary, and reaching it over loopback is both correct and the -// only thing that works when there is no private network. -func TestAConsumerOnTheProxysOwnMachineIsReachedOverLoopback(t *testing.T) { - routes, _, err := routesFrom(`{"given":[ - {"from":"app","node":"anchor","values":{"name":"app.example","port":9000}} - ]}`) - if err != nil { - t.Fatal(err) - } - if targetOf(routes, "app.example") != "http://127.0.0.1:9000" { - t.Fatalf("a workload on this machine was not reachable: %v", routes) +// A route is dialled where the machine publishes its module's endpoint, as the module's declaration publishes it (issue +// 774, research 072 row 34; 2026-10-31: gitea's 3000, published at 20000, and grafana's 3000 on another machine, +// published at 20012, answered 502): over loopback beside the proxy, which is the only thing that works without a +// private network, and at its machine's address otherwise. Each plan is composed by the SDK as the seat +// mesh-provisioner composes it, its ports as compose's portsFor gives them (the placement's for a port the mesh may +// move, the manifest's own otherwise), and read as this proxy reads it: the live shapes the old proxy served alike. +func TestAComposedPlanIsDialledWhereTheMachinePublishesIt(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, module string + port int // the endpoint's port, as listens says it + published []any // the container's ports; nil for the host's network + placed map[int]int // what mesh-placement holds for the module + target string + }{ + {"beside the proxy, moved by the placement (gitea)", "anchor", "gitea", 3000, []any{"3000"}, map[int]int{3000: 20000}, + "http://127.0.0.1:20000"}, + {"on another machine, moved by the placement (grafana)", "home", "grafana", 3000, []any{"3000"}, + map[int]int{3000: 20012}, "http://home.internal:20012"}, + {"a bare port the placement holds none for (influxdb)", "home", "influxdb", 8086, []any{"8086"}, nil, + "http://home.internal:8086"}, + {"a fixed mapping (mailu 7080:80)", "anchor", "mailu", 7080, []any{"25", "7080:80"}, nil, "http://127.0.0.1:7080"}, + {"a fixed mapping the placement names another port of (photos 9102:9000)", "anchor", "photos", 9102, + []any{"9102:9000"}, map[int]int{9000: 20011}, "http://127.0.0.1:9102"}, + {"the host's network (home-assistant)", "anchor", "home-assistant", 8123, nil, nil, "http://127.0.0.1:8123"}, + } { + container := map[string]any{"type": "container", "name": c.module, "image": "x"} + if c.published != nil { + container["ports"] = c.published + } else { + container["network"] = "host" + } + module := catalogue.Manifest{Module: c.module, Version: "1", Requires: []string{"route"}, + Listens: []catalogue.Listening{{Name: "web", Port: c.port, Protocol: "tcp", From: "mesh"}}, + Contributes: map[string]map[string]any{"route": {"label": c.module, "endpoint": "web"}}, + Resources: []map[string]any{container}} + node := catalogue.Node{Name: c.node, At: c.node + ".internal", 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) + } + ports := map[int]int{} + for _, l := range module.Listens { // compose's portsFor + where, mayAssign := module.MachineSide(l.Port) + if at, known := c.placed[l.Port]; mayAssign && known { + where = at + } + ports[l.Port] = where + } + 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: ports}}) + 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", c.name, got, c.target) + } } } diff --git a/modules/route-proxy/go.mod b/modules/route-proxy/go.mod index 8be93094..2978c2dc 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.20261031122137-bb01a656d024 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..a0b5454c 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.20261031122137-bb01a656d024 h1:Uhg2DGxqh9+44ccSbF87v7joSodSwUEGUhhqtrGbP+k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031122137-bb01a656d024/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=