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.
216 lines
9.5 KiB
Go
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
|
|
}
|