InstallationProblems judges every value the mesh acts on for a name under a public top-level domain
or a public address, with prose, the world's registries, resolvers and certificate authorities
exempt, and a name a resource means on purpose declared with its reason (names-on-purpose). Run by
module check and a catalogue-wide test, not yet at registration, while the declared list shrinks.
${setting:<key>} fills a file from the assignment's settings and is refused when nothing set it.
A build context may live on the git seat; the request carries the seat's clone base (novox/hq ADR
0112, ADR 0155, issues 122 and 134).
228 lines
9.1 KiB
Go
228 lines
9.1 KiB
Go
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
|
|
}
|