The registry's public name is served by the predecessor with a twenty-gigabyte buffering middleware, because a registry takes image layers in single requests of gigabytes and a proxy's default turns every push into a 413 the registry never sees. A route contribution had no way to say so, so the mesh could not take the name over without losing what made it usable. The contribution now carries `max-request-body`, a whole positive number of bytes, and the catalogue holds every route to an agreed vocabulary — label or name, port, and the limit — refusing a key no proxy reads (a field that parses cleanly and does nothing is a promise nobody keeps) and a route with no port (unreachable by the proxy it just asked for, found at parse time rather than in a proxy's log). The mesh's own proxy reads the limit as written, refuses a body past it as 413 rather than the 502 the transport would have reported, and skips a route whose limit it cannot read rather than carrying what the module said not to. The registry's hand-over itself is read from the catalogue beside this checkout: the store still resolves with no proxy, the gate beside it pulls the store in, contributes the predecessor's name on the port the node gave it, and locks only the door that faces the world. hq ADR 0082/0104, the registry hand-over.
194 lines
8.0 KiB
Go
194 lines
8.0 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.
|
|
|
|
// 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: assigning the gate
|
|
// brings the store in beside it — the two share a volume, and a gate on a machine without the
|
|
// store would serve an empty one. And `route` resolves from the adapter exactly as from the
|
|
// proxy (ADR 0104).
|
|
together, err := Resolve(shelf(store, gate, adapter),
|
|
[]string{"distribution-gate", "route-adapter"}, withDomain("example.test"), World{})
|
|
if err != nil {
|
|
t.Fatalf("the gate does not resolve beside 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))
|
|
}
|
|
}
|
|
if because := together.Because["distribution"]; !strings.Contains(because, "distribution-gate") {
|
|
t.Errorf("the store was not pulled in by the gate: %q", because)
|
|
}
|
|
}
|
|
|
|
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"])
|
|
}
|
|
if !strings.Contains(cfg["content"].(string), "delete:\n enabled: true") {
|
|
t.Errorf("%v lost the predecessor's delete setting, which tag retention depends on", cfg["path"])
|
|
}
|
|
}
|
|
// 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
|
|
}
|