package catalogue import ( "fmt" "math" "sort" "strings" ) // What a module may say when it contributes a route. // // **The contract is the file, and this is its vocabulary** (novox/hq ADR 0007, 08-connectivity §3). // A module reachable by name requires `route` and contributes what the proxy has to know; the // mesh's own proxy (`examples/route-proxy`) and the adapter to a predecessor's (the catalogue's // `route-adapter`, ADR 0104) both read the same contribution, which is what lets one stand in for // the other without a module that publishes through them noticing. So the keys are agreed here, // once, and a manifest is refused for a key no proxy reads: a field that parses cleanly and does // nothing is a promise nobody keeps, and the module goes on believing its route is limited when it // is not. // // The control plane still does not know what a reverse proxy *is* — it validates the shape and // carries the values, and turning them into a router, a middleware or a body limit is the // provider's. What is checked is exactly what every provider needs to be true: a name to serve, a // port to send to, and a limit that is a number of bytes. // RouteProvision is the provision a module reachable by name requires. const RouteProvision = "route" // RouteLabel is the subdomain a module asks to be published under; the node's public domain is // joined to it (ADR 0066, composeName). const RouteLabel = "label" // RouteName is the legacy full name, left untouched when a module still writes one. const RouteName = "name" // RoutePort is the port the contributing workload's software uses. The mesh redirects it to // wherever the machine published it (ADR 0038, atMachinePort) before the proxy sees it. const RoutePort = "port" // RouteMaxRequestBody is the largest request body, in bytes, the proxy may accept for this route. // // **The registry's hand-over is why it exists** (novox/hq ADR 0082, the registry hand-over). A // registry takes image layers in single requests of gigabytes, and a proxy's default limit — a // megabyte, in some — turns every push into a 413 that the registry never sees. The predecessor // served the registry's public name with exactly this limit as a middleware; a route that could not // say it would have a public name it could not be pushed to. Absent, the proxy applies whatever it // does by default, which for the mesh's own is no limit at all. const RouteMaxRequestBody = "max-request-body" // routeKeys is every key a route contribution may carry, in the order a refusal names them. var routeKeys = []string{RouteLabel, RouteName, RoutePort, RouteMaxRequestBody} // routeProblems is everything wrong with one module's route contribution, empty when nothing is. // // Read from the manifest as written: a per-node setting laid over it (settle) is a fact about one // machine and is checked where settings are; this is the module's own promise. func routeProblems(module string, values map[string]any) []string { var problems []string known := map[string]bool{} for _, k := range routeKeys { known[k] = true } var unknown []string for k := range values { if !known[k] { unknown = append(unknown, k) } } sort.Strings(unknown) if len(unknown) > 0 { problems = append(problems, fmt.Sprintf( "%s contributes %s to %q, which no proxy reads — a route carries %s", module, quoted(unknown), RouteProvision, strings.Join(routeKeys, ", "))) } label, hasLabel := values[RouteLabel] name, hasName := values[RouteName] if !hasLabel && !hasName { problems = append(problems, fmt.Sprintf( "%s contributes a route and names nothing — a %q is the subdomain the node's public "+ "domain is joined to", module, RouteLabel)) } if hasLabel && !aText(label) { problems = append(problems, fmt.Sprintf( "%s contributes a route whose %q is %v, and a label is a non-empty string", module, RouteLabel, label)) } if hasName && !aText(name) { problems = append(problems, fmt.Sprintf( "%s contributes a route whose %q is %v, and a name is a non-empty string", module, RouteName, name)) } port, hasPort := values[RoutePort] if !hasPort { // Refused here rather than skipped by the proxy: a module that asked for a route and not // for the port is unreachable by the proxy it just asked for (08-connectivity §3), and the // proxy saying so in a log is the fault found at the wrong end. problems = append(problems, fmt.Sprintf( "%s contributes a route and no %q — the proxy has nowhere to send it", module, RoutePort)) } else if n, ok := asPort(port); !ok || float64(n) != asNumber(port) || n < 1 || n > 65535 { problems = append(problems, fmt.Sprintf( "%s contributes a route on port %v, which is not a port", module, port)) } if limit, said := values[RouteMaxRequestBody]; said { if _, ok := RouteBodyLimit(limit); !ok { problems = append(problems, fmt.Sprintf( "%s contributes a route whose %q is %v, and a limit is a whole number of bytes, "+ "at least one", module, RouteMaxRequestBody, limit)) } } return problems } // RouteBodyLimit reads a contribution's request-body limit: a whole, positive number of bytes. // // Shared with nothing that runs on a machine — the proxies read the file with their own parsers — // but the rule they apply is this one, and a test holds each of them to it. func RouteBodyLimit(v any) (int64, bool) { var n float64 switch x := v.(type) { case float64: n = x case int: n = float64(x) case int64: n = float64(x) default: return 0, false } if n < 1 || n != math.Trunc(n) || n > math.MaxInt64 { return 0, false } return int64(n), true } // aText is a non-empty string. func aText(v any) bool { s, ok := v.(string) return ok && strings.TrimSpace(s) != "" } // asNumber is a number's value whatever JSON or a test made of it, NaN otherwise. func asNumber(v any) float64 { switch n := v.(type) { case float64: return n case int: return float64(n) } return math.NaN() } // quoted is a list as a refusal names it. func quoted(keys []string) string { out := make([]string, len(keys)) for i, k := range keys { out[i] = fmt.Sprintf("%q", k) } return strings.Join(out, ", ") }