package catalogue import ( "encoding/json" "fmt" "net" "regexp" "sort" "strings" ) // A definition names no installation (novox/hq ADR 0112, ADR 0155, issues 122 and 134). // // A module definition holds what is true of the module everywhere; what is particular to one mesh — // a public name, a forge's address, a node's public address — is resolved at assignment. The rule // stood for a month with nothing checking it, and a sweep found thirty of seventy-one definitions // naming the installation they were written in. This is the check. // // **What is judged is what the mesh acts on, not what a person reads.** A domain in a `why` or a // `description` is documentation the mesh never reads; reporting it beside `KC_HOSTNAME` would teach // people to ignore the report. What is judged is every other string value: a name under a public // top-level domain, or a public address. Two families of name are the world's and not this mesh's, // and are allowed where they can only mean the world: the public registries an `image` may be pulled // from, and the public resolvers a machine may forward to. The container runtime's own alias for // its host is the runtime's, true on every machine that runs it. // // **A name that is right where it stands is declared, one by one, with its reason.** A federated // server's config names the federation's public directory; an application built outside the mesh // is pulled from the registry that built it, until the mesh builds it. The resource carries // `names-on-purpose`, a map from each such name to why — the shape `secrets-in-environment` has, // per name — so a reader sees which names a definition means to carry and why, a name the map does // not cover is still reported, and the catalogue-wide test is the list that shrinks as names move. // NamesOnPurpose is the catalogue-level word a resource carries for the names it means to name: // each name mapped to its reason. The host never sees it. const NamesOnPurpose = "names-on-purpose" // prose is every key whose value the mesh never reads. var prose = map[string]bool{"why": true, "description": true} // Registries the world runs, which an image may name because an image reference must say where it // is pulled from. Anything else in an image reference is a registry of some installation. var worldsRegistries = map[string]bool{ "docker.io": true, "registry-1.docker.io": true, "index.docker.io": true, "ghcr.io": true, "quay.io": true, "gcr.io": true, "registry.k8s.io": true, "k8s.gcr.io": true, "mcr.microsoft.com": true, "lscr.io": true, "public.ecr.aws": true, "registry.gitlab.com": true, "codeberg.org": true, "cgr.dev": true, } // Services the world runs that a definition may name as a policy default, the way it may name a // public resolver: the public certificate authorities' ACME directories. Anything else a served // fact or a file names is somebody's installation. var worldsServices = map[string]bool{ "acme-v02.api.letsencrypt.org": true, "acme-staging-v02.api.letsencrypt.org": true, "api.buypass.com": true, "api.test4.buypass.no": true, "dv.acme-v02.api.pki.goog": true, "acme.zerossl.com": true, } // Resolvers the world runs, which a machine's resolver may forward to as a policy default. var worldsResolvers = map[string]bool{ "1.1.1.1": true, "1.0.0.1": true, "8.8.8.8": true, "8.8.4.4": true, "9.9.9.9": true, "149.112.112.112": true, "208.67.222.222": true, "208.67.220.220": true, } // hostname is a dotted name whose last label is a top-level domain a real installation would have. // Not every dotted token: `module.json`, `index.html` and `docker.sock` are dotted and name nothing. // Boundaries are checked by hand rather than in the pattern, because two names one character apart // — `a.example.tld,b.example.tld` — would otherwise share the delimiter and the second would be lost. var hostname = regexp.MustCompile( `(?i)(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+` + `(?:be|nl|de|fr|uk|eu|com|net|org|io|dev|app|cloud|site|online|me|co|ch|at|lu|` + `internal|example|tld|test|invalid)`) // address is a dotted quad. var address = regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}`) // isName is whether a byte may be part of a name; a match bordered by one is a longer token. func isName(b byte) bool { return b == '.' || b == '-' || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') } // standalone are the matches of re in value that are whole tokens, not parts of a longer one. func standalone(re *regexp.Regexp, value string) []string { var out []string for _, span := range re.FindAllStringIndex(value, -1) { if span[0] > 0 && isName(value[span[0]-1]) { continue } if span[1] < len(value) && isName(value[span[1]]) { continue } out = append(out, value[span[0]:span[1]]) } return out } // InstallationProblems is every value of a definition that names an installation, in the // definition's own words: where it is, and what it names. func InstallationProblems(m Manifest) []string { raw, err := json.Marshal(m) if err != nil { return []string{fmt.Sprintf("%s could not be read back: %v", m.Module, err)} } var tree any if err := json.Unmarshal(raw, &tree); err != nil { return []string{fmt.Sprintf("%s could not be read back: %v", m.Module, err)} } var problems []string // The module's own name is a value too: a module named after the domain it serves is a // definition that can only be installed there (issue 134). for _, name := range namesIn(m.Module) { problems = append(problems, fmt.Sprintf( "%s is named after %s, and a module is named for what it is, not for where it runs", m.Module, name)) } walk(tree, "", nil, func(at string, value string, meant map[string]bool, isImage bool) { for _, name := range namesIn(value) { if (isImage && worldsRegistries[strings.ToLower(name)]) || meant[name] { continue } problems = append(problems, fmt.Sprintf("%s names %s at %s", m.Module, name, at)) } for _, ip := range addressesIn(value) { if meant[ip] { continue } problems = append(problems, fmt.Sprintf("%s names the public address %s at %s", m.Module, ip, at)) } }) sort.Strings(problems) return problems } // walk visits every string in the tree with its path, the names the enclosing resource means to // name (with a reason), and whether it is an image reference. func walk(node any, at string, meant map[string]bool, visit func(at, value string, meant map[string]bool, isImage bool)) { switch v := node.(type) { case map[string]any: if declared, has := v[NamesOnPurpose].(map[string]any); has { widened := map[string]bool{} for name := range meant { widened[name] = true } for name, reason := range declared { if r, ok := reason.(string); ok && strings.TrimSpace(r) != "" { widened[strings.ToLower(name)] = true } } meant = widened } keys := make([]string, 0, len(v)) for k := range v { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if prose[k] || k == NamesOnPurpose || (at == "" && k == "module") { continue } child := at + "." + k if at == "" { child = k } if s, isString := v[k].(string); isString { visit(child, s, meant, k == "image") continue } walk(v[k], child, meant, visit) } case []any: for i, item := range v { child := fmt.Sprintf("%s[%d]", at, i) if s, isString := item.(string); isString { visit(child, s, meant, false) continue } walk(item, child, meant, visit) } } } // namesIn is every hostname in a value that could belong to an installation. func namesIn(value string) []string { var out []string for _, found := range standalone(hostname, value) { name := strings.ToLower(found) switch { case strings.HasSuffix(name, ".docker.internal"): // The container runtime's alias for its own host: every machine running it has one. case worldsServices[name]: // A public authority named as a policy default, true of any mesh that wants it. case name == "example.tld", strings.HasSuffix(name, ".example.tld"), name == "example.com", name == "example.net", name == "example.org", strings.HasSuffix(name, ".example.com"), strings.HasSuffix(name, ".example.net"), strings.HasSuffix(name, ".example.org"), strings.HasSuffix(name, ".example"), strings.HasSuffix(name, ".test"), strings.HasSuffix(name, ".invalid"): // Documentation names, which is what a definition's own example should use. default: out = append(out, name) } } return out } // addressesIn is every public address in a value: not a private range, loopback, link-local, the // unspecified address, a documentation range, or a resolver the world runs. func addressesIn(value string) []string { var out []string for _, found := range standalone(address, value) { ip := net.ParseIP(found) if ip == nil || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() || ip.IsMulticast() || worldsResolvers[found] || documentation(ip) { continue } out = append(out, found) } return out } func documentation(ip net.IP) bool { for _, cidr := range []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24", "100.64.0.0/10"} { _, block, _ := net.ParseCIDR(cidr) if block.Contains(ip) { return true } } return false }