Add route-proxy provider and hello-web consumer modules
route-proxy is the shipping form of the reference reverse proxy (novox/hq ADR 0007, 08-connectivity section 3): it provides route, is given every consumer as the file at receives.route, and forwards by the Host header. It ships the Go proxy from mesh-control/examples/route-proxy via a multi-stage Dockerfile; no broker, own-secret or provisioner, since it only reads the file the mesh writes. ACME_DIRECTORY defaults to Let's Encrypt staging and is overridable per node to production, so there is no hardcoded production default -- resolving novox/hq 04-ISSUES/004. hello-web is a minimal consumer that requires route and contributes name+port, to exercise the grant. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"module": "hello-web",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"requires": [
|
||||
"route"
|
||||
],
|
||||
"contributes": {
|
||||
"route": {
|
||||
"name": "hello.example",
|
||||
"port": 8080
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"route": "/var/lib/hello-web/route.json"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
"port": 8080,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the demo web page; only the route-proxy reaches it, and the public name is a route grant"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/hello-web",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "page",
|
||||
"type": "file",
|
||||
"path": "/var/lib/hello-web/index.html",
|
||||
"mode": "0644",
|
||||
"content": "hello from hello-web, routed by the mesh\n"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
"type": "network",
|
||||
"name": "hello-web"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "hello-web",
|
||||
"image": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
|
||||
"network": "hello-web",
|
||||
"ports": [
|
||||
"8080:8080"
|
||||
],
|
||||
"volumes": [
|
||||
"/var/lib/hello-web/index.html:/www/index.html:ro"
|
||||
],
|
||||
"args": [
|
||||
"sh",
|
||||
"-c",
|
||||
"while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\n'; cat /www/index.html; } | nc -l -p 8080; done"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# The route-proxy module's runtime image: the reference reverse proxy compiled into a container.
|
||||
#
|
||||
# **The proxy source is not vendored here.** The canonical proxy — the contract written as something
|
||||
# that runs — lives in the mesh-control repository at examples/route-proxy (novox/hq 08-connectivity
|
||||
# §3). This module ships the *packaging*, not a second copy of the contract, so the build context is
|
||||
# the mesh-control repository root, and this Dockerfile compiles ./examples/route-proxy from it.
|
||||
#
|
||||
# docker build -f mesh-catalog/modules/route-proxy/Dockerfile \
|
||||
# -t mesh-route-proxy:development <path-to>/mesh-control
|
||||
#
|
||||
# The mesh pins the digest of what this produces; the committed module.json carries the placeholder
|
||||
# digest every mesh-built image does, replaced at publish.
|
||||
FROM golang:1.25 AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /mesh-route-proxy ./examples/route-proxy
|
||||
|
||||
# A small runtime with the public CA roots the ACME client needs to reach a real authority, and run
|
||||
# as root so it can bind :80 and :443 — the two privileged ports a public front door listens on.
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=build /mesh-route-proxy /usr/local/bin/mesh-route-proxy
|
||||
ENTRYPOINT ["/usr/local/bin/mesh-route-proxy"]
|
||||
@@ -0,0 +1,69 @@
|
||||
# route-proxy — the mesh's public ingress
|
||||
|
||||
The shipping form of the reference reverse proxy (novox/hq ADR 0007, novox/hq 08-connectivity §3
|
||||
"Exposure"). It is the `traefik` replacement: the module that **provides `route`**.
|
||||
|
||||
## What it is
|
||||
|
||||
**A route is a grant.** A module that must be reachable declares `requires: ["route"]` and
|
||||
contributes the name it wants and the port it listens on; this module provides `route`, receives
|
||||
every consumer that asked as a file, and forwards by the `Host` header of each request to
|
||||
`http://<at>:<port>` — where `at` is where the mesh says that consumer's machine is (empty means
|
||||
this machine, reached over loopback).
|
||||
|
||||
The control plane does not write the proxy's configuration. It delivers the facts — the
|
||||
contributions file at `receives.route` — and the proxy turns them into whatever it runs. That
|
||||
boundary is why swapping this proxy for another costs nothing to the modules that publish through
|
||||
it.
|
||||
|
||||
| field | value | why |
|
||||
|---|---|---|
|
||||
| `provides` | `route` (scope `mesh`) | a module on any node may require it |
|
||||
| `serves.route` | `{}` | a route hands back a name, not a credential — the consumer already knows the name it asked for |
|
||||
| `receives.route` | `…/routes/mesh.json` | where the mesh writes every contribution; the proxy reads it as `$ROUTES` and re-reads on change |
|
||||
| `listens` | `80` and `443`, both `from: anywhere` | the one machine with a public opening; the firewall opens these for free (ADR 0045) |
|
||||
|
||||
No broker account, no own-secrets, no provisioner: the proxy neither mints a credential nor emits
|
||||
an event. It only reads the file the mesh writes. (Contrast `redis`, which mints passwords, and
|
||||
`cloudflare-dns`, which emits record events.)
|
||||
|
||||
## How it ships the Go proxy
|
||||
|
||||
The proxy is a Go program, unlike the TypeScript tool-runtime modules. The canonical source is
|
||||
**not vendored here** — it lives in the mesh-control repository at `examples/route-proxy`, the
|
||||
contract written as something that runs. This module ships only the packaging: a multi-stage
|
||||
[`Dockerfile`](Dockerfile) whose build context is the mesh-control repository root and which
|
||||
compiles `./examples/route-proxy` into `mesh-route-proxy`. The committed `module.json` carries the
|
||||
placeholder digest every mesh-built image does (`@sha256:0000…`); the mesh pins the real digest at
|
||||
publish.
|
||||
|
||||
In the lab, `mesh-lab/scripts/build-route-proxy-image.sh` builds this into the local daemon as
|
||||
`mesh-route-proxy:development`, and a scenario stocks it and serves it by digest.
|
||||
|
||||
## Certificates, and the endpoint (resolves 04-ISSUES/004)
|
||||
|
||||
TLS is opt-in and terminates here for public names, via ACME (autocert). A proxy with no
|
||||
`TLS_LISTEN` serves plain HTTP; with it, it obtains a publicly-trusted certificate for exactly the
|
||||
names the mesh routes here (`HostPolicy` = only routed names, so a scan cannot spend the account's
|
||||
issuance quota) and answers the HTTP-01 challenge on `:80` at the name being certified.
|
||||
|
||||
**`ACME_DIRECTORY` defaults to Let's Encrypt *staging*, not production.** This closes
|
||||
novox/hq 04-ISSUES/004 (certificate issuance targets production):
|
||||
the issuer is a per-node choice, and the safe endpoint is the default. Production issuance is
|
||||
rate-limited per domain and per account and does not replenish quickly; defaulting to production
|
||||
would leave the safe path depending on somebody remembering to opt out of it, on exactly the work
|
||||
most likely to iterate. A staging certificate is trusted by no browser, so the mistake announces
|
||||
itself on the first request rather than a fortnight later at the rate limit.
|
||||
|
||||
**A node that serves real public traffic states so**, by overriding `ACME_DIRECTORY` to the
|
||||
production directory `https://acme-v02.api.letsencrypt.org/directory` for this module on that node.
|
||||
It is a node property, and the node facing the public internet is the one that opts in.
|
||||
|
||||
| env | default | meaning |
|
||||
|---|---|---|
|
||||
| `ROUTES` | `/routes/mesh.json` | the contributions file the mesh writes (`receives.route`) |
|
||||
| `LISTEN` | `:80` | HTTP, and the ACME HTTP-01 challenge |
|
||||
| `TLS_LISTEN` | `:443` | HTTPS; unset to serve plain HTTP only |
|
||||
| `ACME_CACHE` | `/acme` | where issued certificates persist; required when `TLS_LISTEN` is set, so a restart does not re-order |
|
||||
| `ACME_DIRECTORY` | Let's Encrypt **staging** | the issuer; a public-serving node overrides it to production |
|
||||
| `ACME_CA_BUNDLE` | *(unset)* | a file of roots to trust for the issuer's own API — set only for a private/lab authority whose API certificate the world does not yet trust |
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"module": "route-proxy",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"provides": [
|
||||
{
|
||||
"name": "route",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"serves": {
|
||||
"route": {}
|
||||
},
|
||||
"receives": {
|
||||
"route": "/var/lib/route-proxy/routes/mesh.json"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
"port": 80,
|
||||
"protocol": "tcp",
|
||||
"from": "anywhere",
|
||||
"why": "public HTTP, and the ACME HTTP-01 challenge answered at the name being certified"
|
||||
},
|
||||
{
|
||||
"port": 443,
|
||||
"protocol": "tcp",
|
||||
"from": "anywhere",
|
||||
"why": "public HTTPS for every name the mesh routes here"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/route-proxy",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "routes-dir",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/route-proxy/routes",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "acme-cache",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/route-proxy/acme",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "route-proxy",
|
||||
"image": "mesh-route-proxy@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/route-proxy/routes:/routes:ro",
|
||||
"/var/lib/route-proxy/acme:/acme"
|
||||
],
|
||||
"env": {
|
||||
"ROUTES": "/routes/mesh.json",
|
||||
"LISTEN": ":80",
|
||||
"TLS_LISTEN": ":443",
|
||||
"ACME_CACHE": "/acme",
|
||||
"ACME_DIRECTORY": "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user