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 }