Move route-proxy out of the controller's repository, which is archived with the controller

route-proxy provides `route` to five modules and lived in novox/mesh-controller, archived once the controller is
removed (issue 1126). Its history comes along (git filter-repo of modules/route-proxy and its earlier home
examples/route-proxy). Its contract's Go module path names this repository, and route-proxy builds the contract from
its own tree through a replace, so no tag is needed before the first build. The two manifest tests that held its
public ACME account and mounted program lived in the controller's internal/ownmanifest; they move beside the module.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 11:45:17 +01:00
parent 3ef81c0fe6
commit 6f038810fd
19 changed files with 199 additions and 21 deletions
+4
View File
@@ -0,0 +1,4 @@
vendor/
# A compiled program never enters a commit (issue 1577): `go build .` in a command directory leaves one there.
modules/*/cmd/*/route-*
!modules/*/cmd/*/*.go
+11
View File
@@ -0,0 +1,11 @@
# mesh-networking
Novox Mesh — networking: the modules that carry traffic into and across the mesh, each under `modules/<name>` with its
own manifest (`module.json`) and Go module.
- **route-proxy** provides `route`: the public HTTP and HTTPS entry, serving each name a consumer asks for from the
seat mesh-provisioner's plans (its README says how). It moved here from the controller's repository, history kept,
when the controller was removed (issue 1126).
Made by the mesh from the gitea module's config value `repositories` (novox/hq ADR 0428). Main takes no push: a change
reaches it by a pull request whose `mesh/check` passed and the operator approved.
+6 -5
View File
@@ -18,9 +18,10 @@ consumer on its own machine.
## Its contract: a package of its own
**route-proxy defines its own contract, and other modules use it** (the operator, 2026-10-31). It is the Go module
`git.novox.be/novox/mesh-controller/modules/route-proxy/contract`, in `contract/` here with its own `go.mod`, standard
library only, released by tags `modules/route-proxy/contract/vX.Y.Z`, the way mesh-sdk is released (`go/vX.Y.Z`); this
module depends on it by its version like any other. It holds:
`git.novox.be/novox/mesh-networking/modules/route-proxy/contract`, in `contract/` here with its own `go.mod`, standard
library only, released by tags `modules/route-proxy/contract/vX.Y.Z` of this repository, the way mesh-sdk is released
(`go/vX.Y.Z`); another module depends on it by its version like any other, and this module builds it from its own
tree (a `replace` of `./contract` in `go.mod`), so the two never drift. It holds:
- **the route a consumer asks** (`Request`, read strictly by `DecodeRequest`: no field it does not name, a name of
either kind, a port where the rule forwards, http or https, a positive body limit, a secret's file never a
@@ -64,8 +65,8 @@ directory URL — `https://acme-v02.api.letsencrypt.org:443/directory`, port inc
spelled it — and of the root bundle the `trust` container copies in. Any change to either is a new
authority to the proxy: a new account, and every routed name ordered again against Let's Encrypt's
rate limits. So the URL keeps the `:443`, `ACME_ROOTS_PATH` stays empty, and the `trust` artifact's
pinned image is not moved without a decision; mesh-controller's
`TestRouteProxyKeepsItsPublicAccountDirectory` holds all three.
pinned image is not moved without a decision; `TestRouteProxyKeepsItsPublicAccountDirectory`
(cmd/route-provider) holds all three.
## How it ships the Go proxy
@@ -14,7 +14,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
func say(format string, args ...any) {
@@ -0,0 +1,161 @@
package main
import (
"path/filepath"
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Moved from mesh-controller's internal/ownmanifest (issue 1126), which is archived with the controller: the two tests
// that held route-proxy's manifest to what keeps its public account and runs its program, now beside the module.
// The public issuer is the proxy's own fact (novox/hq ADR 0226), and the folding of it must not move the proxy's account.
//
// route-proxy keeps each ACME authority's account and certificates in a directory named after a digest of the directory
// URL and of the root bundle its `trust` container copies in (cmd/route-proxy, forThisAuthority). Until ADR 0226 the URL
// was rendered from a binding to `public-acme`'s `acme-ca`, spelled with the port. A URL spelled any other way (even the
// same authority without `:443`) or a root bundle from another image is a new authority to the proxy: a new account, and
// every routed name ordered again, on two machines at once, against Let's Encrypt's rate limits. So what the module now
// states is held to exactly what the binding rendered.
// renderedBeforeTheFold is route-proxy's acme.env as the binding to public-acme rendered it on every machine running the
// proxy, read from the controller's plan on 2026-10-06.
const renderedBeforeTheFold = "ACME_DIRECTORY=https://acme-v02.api.letsencrypt.org:443/directory\n" +
"ACME_ROOTS=https://acme-v02.api.letsencrypt.org:443\n" +
"ACME_ROOTS_PATH=\n"
// trustImage is the image whose system bundle becomes the public root the account directory is named after. Moving it
// renames that directory.
const trustImage = "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
// ownManifest is route-proxy's manifest as its tree holds it: this repository's root and the module's directory.
func ownManifest(t *testing.T) catalogue.Manifest {
t.Helper()
m, err := catalogue.ReadTreeManifest(filepath.Join("..", "..", "..", ".."), filepath.Join("..", ".."))
if err != nil {
t.Fatalf("route-proxy's manifest does not parse:\n%v", err)
}
return m
}
func resourceNamed(out []map[string]any, id string) map[string]any {
for _, r := range out {
if r["id"] == id {
return r
}
}
return nil
}
func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) {
m := ownManifest(t)
if slices.Contains(m.Requires, "acme-ca") {
t.Fatal("route-proxy still asks for acme-ca; the public issuer is its own fact (ADR 0226)")
}
// As a build would leave it: each artifact a container names resolved to an image. Which image does not matter to
// the file checked here. An archive resource carries no image: it unpacks a bundle.
for _, res := range m.Resources {
if res["type"] == "archive" {
continue
}
if artifact, ok := res["artifact"].(string); ok {
delete(res, "artifact")
res["image"] = "registry.invalid/route-proxy/" + artifact +
"@sha256:1111111111111111111111111111111111111111111111111111111111111111"
}
}
r := catalogue.Resolution{
Node: "anchor",
Modules: []catalogue.Manifest{m},
Needs: []catalogue.Needed{{
Name: "internal-acme-ca", From: "home", At: "home.internal", For: "route-proxy",
Serves: map[string]any{"port": float64(9000), "path": "/acme/acme/directory", "roots": "/roots.pem"},
}},
}
// Its own broker credential, sealed as the mesh would; what it is does not matter here.
out, err := r.Declaration(catalogue.Rendering{Needed: map[string]map[string]string{"route-proxy": {"broker": "sealed"}}})
if err != nil {
t.Fatalf("route-proxy could not be composed for a machine: %v", err)
}
env := resourceNamed(out, "route-proxy.acme-env")
if env == nil {
t.Fatalf("nothing writes the public issuer's environment: %v", out)
}
if got, _ := env["content"].(string); got != renderedBeforeTheFold {
t.Fatalf("acme.env changed, which moves the proxy's account and reorders every certificate:\ngot %q\nwant %q",
got, renderedBeforeTheFold)
}
if resourceNamed(out, "route-proxy.bound-acme-ca") != nil {
t.Error("a binding to acme-ca is still written")
}
var trust string
if m.Build != nil {
for _, a := range m.Build.Artifacts {
if a.Name == "trust" {
trust = a.From
}
}
}
if trust != trustImage {
t.Errorf("the trust image is %q, not %q: its system bundle is the public root the account directory is named "+
"after, so moving it reorders every certificate. Move it only with a plan for the account (ADR 0226)",
trust, trustImage)
}
}
// The proxy's program is mounted into the pinned trust image, not baked into an image (novox/hq ADR 0480 clauses 1, 2
// and 4): a Go bundle of the module's own Go module stamped for alpine, unpacked by an archive resource, mounted
// read-only at /opt/mesh, called through args, and named under restart-on so a new program never leaves the old one
// running. The container's name, network and logging are what the jail and the routes rely on. And its routes are the
// seat mesh-provisioner's plans alone (issue 774, research 072 row 34): it reads them, and receives no file.
func TestRouteProxyRunsItsMountedProgramOnTheTrustImage(t *testing.T) {
m := ownManifest(t)
if !slices.Contains(m.Reads, "mesh-provisioner.plans") {
t.Errorf("route-proxy does not read mesh-provisioner.plans, where its routes are: %v", m.Reads)
}
if len(m.Receives) > 0 {
t.Errorf("route-proxy receives %v: a route is served from the seat's plans, never a received file", m.Receives)
}
var bundle *catalogue.Artifact
for i, a := range m.Build.Artifacts {
if a.Name == "server" {
bundle = &m.Build.Artifacts[i]
}
if a.Kind == "image" {
t.Errorf("artifact %s is an image built from a Dockerfile (ADR 0452 §1)", a.Name)
}
}
if bundle == nil || bundle.Kind != "bundle" || bundle.Language != "go" || bundle.System != "alpine" ||
bundle.From != "cmd/route-proxy" || bundle.Binary != "route-proxy" {
t.Fatalf("the server artifact is not a Go bundle of cmd/route-proxy stamped for alpine: %+v", bundle)
}
var archive, server map[string]any
for _, r := range m.Resources {
switch {
case r["type"] == "archive" && r["artifact"] == "server":
archive = r
case r["id"] == "server":
server = r
}
}
if archive == nil || server == nil {
t.Fatalf("archive %v or server container %v is missing", archive, server)
}
if server["artifact"] != "trust" || server["name"] != "route-proxy" || server["network"] != "host" || server["logging"] != "journald" {
t.Errorf("the server container moved: %v", server)
}
mount := archive["path"].(string) + ":/opt/mesh:ro"
volumes, _ := server["volumes"].([]any)
if !slices.Contains(volumes, any(mount)) {
t.Errorf("the archive is not mounted read-only at /opt/mesh (%s): %v", mount, server["volumes"])
}
if args, _ := server["args"].([]any); len(args) != 1 || args[0] != "/opt/mesh/route-proxy" {
t.Errorf("args are %v, want the mounted program", server["args"])
}
restartOn, _ := server["restart-on"].([]any)
if !slices.Contains(restartOn, archive["id"]) {
t.Errorf("restart-on %v does not name the archive %v", server["restart-on"], archive["id"])
}
}
@@ -13,7 +13,7 @@ package main
import (
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// ProvisionName is what route-proxy provides, as its manifest says, and ModuleName this module.
@@ -14,7 +14,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// The tools' names, as the manifest declares them.
@@ -9,7 +9,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// held is a state as the bus holds it, read-only.
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// keptStates is every state route-proxy keeps, as both its halves write them: the routes and the refusals this
@@ -17,7 +17,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// What this proxy says on the bus about the names it serves and the names it refuses (issue 774).
@@ -25,7 +25,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// fakeState is a state on the bus, in memory, counting what was written to it.
+1 -1
View File
@@ -65,7 +65,7 @@ import (
"golang.org/x/crypto/acme/autocert"
"golang.org/x/crypto/bcrypt"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// Where public certificates come from when nothing says otherwise.
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"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"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// planned is the routes the seat's plans give this proxy on its node, by plan key, kept so a plan that cannot be read
@@ -18,7 +18,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// behind is a workload the proxy can send to, and a table routing one public name and one
@@ -13,7 +13,7 @@ import (
"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"
"git.novox.be/novox/mesh-networking/modules/route-proxy/contract"
)
// routesFrom is the table a test's routes make, written as `{"given":[{"from","node","at","values"}…]}`: the
+1 -1
View File
@@ -1,3 +1,3 @@
module git.novox.be/novox/mesh-controller/modules/route-proxy/contract
module git.novox.be/novox/mesh-networking/modules/route-proxy/contract
go 1.26.0
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.novox.be/novox/mesh-controller/modules/route-proxy/contract/route.schema.json",
"$id": "https://git.novox.be/novox/mesh-networking/modules/route-proxy/contract/route.schema.json",
"title": "route",
"description": "One route a consumer asks route-proxy for, as a manifest's contributes.route says it (one, or several by local name) and the controller composes it. Version 1 of route-proxy's contract.",
"type": "object",
+4 -1
View File
@@ -9,7 +9,7 @@ require (
)
require (
git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0
git.novox.be/novox/mesh-networking/modules/route-proxy/contract v0.1.0
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
@@ -17,3 +17,6 @@ require (
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
)
// The contract is this repository's own Go module, beside this one (contract/): built from the tree, not fetched.
replace git.novox.be/novox/mesh-networking/modules/route-proxy/contract => ./contract
-2
View File
@@ -1,5 +1,3 @@
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.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=