Files
mesh-controller/internal/catalogue/registry_gate_test.go
T
jschoubben b48572bdfc A null body limit is refused, not read as no limit; the gate on the wrong machine is refused
Review of the registry hand-over. The proxy's bodyLimit treated an absent key and a JSON
null alike, so a `max-request-body: null` was served unlimited here while the adapter
skipped it and the catalogue refused it — one provider carrying what the others refuse.
Presence is now checked before the value is read.

The catalogue-backed test asserted the gate pulling the store in beside it as the feature.
It was the fault: a node-scoped requirement with one candidate installs that candidate, so
a gate assigned to a machine without the store raised a second, empty one there behind the
real credentials and the public name. The store's seat is one per mesh now (mesh-catalog),
and the test asserts the refusal by name. Delete is asserted only behind the lock.

hq ADR 0082/0104, the registry hand-over.
2026-09-23 23:35:29 +02:00

216 lines
9.5 KiB
Go

package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// The registry's public name, taken over from the predecessor (novox/hq ADR 0082, ADR 0104, the
// registry hand-over) — read from the catalogue beside this checkout, parsed by the real parser.
//
// **The public door is a second module beside the store, not a route on the store.** Contributing
// a route is requiring one, and the store is raised at genesis on a node with no proxy; a store
// that required a route would be a store no first node could have. So `distribution` stays the
// registry ADR 0082 describes — reached by name, over the private network, with no account — and
// `distribution-gate` is a second registry process on the same volume, behind the registry's own
// basic auth, with the public name. The mesh's own pulls never pass through it, which is provable
// from the two manifests: the store's container carries no auth and mounts no htpasswd.
//
// And the gate can only ever stand beside THE store: the store's seat is one per mesh, so a gate
// assigned to a machine without it does not quietly raise a second, empty store there.
// aStubProxy provides `route` so a declaration can be made without a built artifact: the real
// providers are the adapter (whose container is a mesh-built artifact) and the proxy.
func aStubProxy() Manifest {
return Manifest{Module: "proxy", Version: "1",
Provides: FromAnywhere("route"),
Receives: map[string]string{"route": "/var/lib/proxy/routes.json"}}
}
func TestTheStoreNeedsNoRouteAndTheGateBringsTheStoreBesideIt(t *testing.T) {
store := catalogueManifest(t, "distribution")
gate := catalogueManifest(t, "distribution-gate")
adapter := catalogueManifest(t, "route-adapter")
// The store alone, with nothing providing a route — genesis' own set — still resolves.
alone, err := Resolve(shelf(store, gate, adapter), []string{"distribution"}, workstation(), World{})
if err != nil {
t.Fatalf("the store cannot be resolved without a proxy, so no first node could have one: %v", err)
}
if got := names(alone); len(got) != 1 || got[0] != "distribution" {
t.Fatalf("the store alone resolved to %v", got)
}
// The gate wants the store's storage, which is a node-scoped provision: assigned beside the
// store it resolves, and `route` resolves from the adapter exactly as from the proxy (ADR 0104).
together, err := Resolve(shelf(store, gate, adapter),
[]string{"distribution", "distribution-gate", "route-adapter"}, withDomain("example.test"), World{})
if err != nil {
t.Fatalf("the gate does not resolve beside the store and the adapter: %v", err)
}
for _, want := range []string{"distribution", "distribution-gate", "route-adapter"} {
if !among(names(together), want) {
t.Errorf("%s is missing from %v", want, names(together))
}
}
// **Assigned to the wrong machine, it is refused rather than served.** A node-scoped
// requirement with one candidate installs that candidate on the node — which for the gate
// would be a second, empty store behind the real credentials and the public name, and a
// second `artifact-store` offered to the mesh so every consumer elsewhere refuses. The store's
// claim is mesh-scoped for exactly this: a second store anywhere is refused by name.
elsewhere := World{Held: []Held{{Claim: "the-artifact-store", Scope: ScopeMesh,
Node: "anchor", Module: "distribution"}}}
other := withDomain("example.test")
other.Name = "laptop"
_, err = Resolve(shelf(store, gate, adapter), []string{"distribution-gate", "route-adapter"}, other, elsewhere)
if err == nil {
t.Fatal("the gate on a machine without the store was accepted, and would have raised an " +
"empty second store behind the public name")
}
if !strings.Contains(err.Error(), "the-artifact-store") || !strings.Contains(err.Error(), "one per mesh") {
t.Fatalf("refused without naming the store's seat: %v", err)
}
}
func TestTheGateContributesTheRegistrysPublicNameAndAGivenPortFollowsIntoIt(t *testing.T) {
store := catalogueManifest(t, "distribution")
gate := catalogueManifest(t, "distribution-gate")
got, err := Resolve(shelf(store, gate, aStubProxy()),
[]string{"distribution-gate", "proxy"}, withDomain("example.test"), World{})
if err != nil {
t.Fatal(err)
}
// The node gave the gate's port a machine port (novox/hq ADR 0100) — on the machine being
// migrated the predecessor's interface still holds the default — as the operator does, with the
// `ports` setting.
moved, err := GivenPorts(gate, []Layer{{From: "node anchor",
Values: map[string]any{PortsSetting: map[string]any{"5001": float64(5101)}}}})
if err != nil {
t.Fatalf("the gate's port cannot be given a machine port: %v", err)
}
out, err := got.Declaration(Rendering{
Ports: map[string]map[int]int{"distribution-gate": moved},
Given: map[string]map[int]int{"distribution-gate": moved},
Needed: map[string]map[string]string{
"distribution": {"broker": "sealed-broker"},
"distribution-gate": {"htpasswd": "sealed"},
},
})
if err != nil {
t.Fatal(err)
}
var given []Contribution
var storeContainer, gateContainer, storeConfig, gateConfig, htpasswd map[string]any
for _, r := range out {
switch {
case r["path"] == "/var/lib/proxy/routes.json":
var parsed struct {
Given []Contribution `json:"given"`
}
if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil {
t.Fatal(err)
}
given = parsed.Given
case r["name"] == "mesh-registry":
storeContainer = r
case r["name"] == "mesh-registry-gate":
gateContainer = r
case r["path"] == "/var/lib/mesh/registry/config.yml":
storeConfig = r
case r["path"] == "/var/lib/mesh/registry-gate/config.yml":
gateConfig = r
case r["path"] == "/var/lib/mesh/registry-gate/htpasswd":
htpasswd = r
}
}
// One route: the predecessor's name, composed from the label and the node's domain, on the
// port the machine actually published, with the limit a layer push needs.
if len(given) != 1 || given[0].From != "distribution-gate" {
t.Fatalf("the proxy was given %v", given)
}
v := given[0].Values
if v["name"] != "registry-api.example.test" {
t.Errorf("the public name did not compose: %v", v["name"])
}
if port, _ := asPort(v["port"]); port != 5101 {
t.Errorf("the route points at %v, and the machine published the gate on 5101", v["port"])
}
if limit, ok := RouteBodyLimit(v["max-request-body"]); !ok || limit != 21474836480 {
t.Errorf("the route carries a body limit of %v; a layer push needs the predecessor's twenty gigabytes", v["max-request-body"])
}
// The lock is the registry's own, and only on the door that faces the world: the gate mounts
// the operator's htpasswd and its configuration names it; the store does neither, so what the
// mesh pulls over the private network needs no account (ADR 0082).
if htpasswd == nil || htpasswd["sealed"] != "sealed" {
t.Fatalf("the gate's htpasswd is not delivered as a sealed own secret: %v", htpasswd)
}
if !mounts(gateContainer, "/var/lib/mesh/registry-gate/htpasswd:/etc/docker/registry/htpasswd:ro") {
t.Errorf("the gate does not mount the htpasswd: %v", gateContainer["volumes"])
}
if !strings.Contains(gateConfig["content"].(string), "auth:\n htpasswd:") {
t.Errorf("the gate's configuration does not lock the door: %s", gateConfig["content"])
}
if strings.Contains(storeConfig["content"].(string), "auth:") {
t.Errorf("the store's configuration asks for an account, and the mesh has none to give (ADR 0082): %s", storeConfig["content"])
}
for _, v := range storeContainer["volumes"].([]any) {
if strings.Contains(v.(string), "htpasswd") {
t.Errorf("the store mounts an htpasswd: %v", v)
}
}
if env, has := storeContainer["env"]; has {
t.Errorf("the store's container carries an environment it did not before: %v", env)
}
// Two processes, one store: both containers mount the same volume, and neither keeps a
// per-process descriptor cache over it.
for _, c := range []map[string]any{storeContainer, gateContainer} {
if !mounts(c, "mesh-registry-data:/var/lib/registry") {
t.Errorf("%v does not mount the registry's volume: %v", c["name"], c["volumes"])
}
}
for _, cfg := range []map[string]any{storeConfig, gateConfig} {
if strings.Contains(cfg["content"].(string), "blobdescriptor") {
t.Errorf("%v keeps a per-process blob cache over a store two processes write: %s", cfg["path"], cfg["content"])
}
}
// Delete is the predecessor's setting and tag retention depends on it — but only behind the
// lock. The store's door is reached by the whole private network with no account (ADR 0082),
// and a delete anything on the overlay may send is not a setting to carry there.
if !strings.Contains(gateConfig["content"].(string), "delete:\n enabled: true") {
t.Errorf("the gate lost the predecessor's delete setting, which tag retention depends on")
}
if strings.Contains(storeConfig["content"].(string), "delete:\n enabled: true") {
t.Errorf("the account-free store accepts DELETE from anything on the overlay: %s", storeConfig["content"])
}
// And the machine published the gate where the node said.
if ports, _ := gateContainer["ports"].([]any); len(ports) != 1 || ports[0] != "5101:5001" {
t.Errorf("the gate is published as %v, and the node gave its 5001 the machine port 5101", gateContainer["ports"])
}
}
func mounts(container map[string]any, volume string) bool {
listed, _ := container["volumes"].([]any)
for _, v := range listed {
if v == volume {
return true
}
}
return false
}
func among(list []string, want string) bool {
for _, s := range list {
if s == want {
return true
}
}
return false
}