package catalogue import ( "fmt" "regexp" "sort" "strings" ) // What a module may say about the machine it is running on. // // **A module cannot know where it will be assigned, and sometimes it must say so anyway.** Every // other name in a declaration is either the module's own — which it wrote — or something it // requires, which arrives as a binding. The machine underneath is neither: it is chosen when the // module is assigned, long after the manifest was written, and until now nothing carried it into a // file. // // The case that found this is a certificate authority inside the mesh (novox/hq ADR 0066). A proxy // reaches it at the address the mesh handed over, `.internal` — so the authority's own // certificate has to be issued for that name, or the first thing that happens is the proxy refusing // to talk to it. The authority is the one thing that cannot be told its name by a binding: it // provides, it does not require. Written as a literal it would be a manifest carrying one // deployment's machine name, which is the shape [ADR 0066] exists to remove. // // Three facts, all the mesh's own vocabulary — the same `node` and `at` a contribution already // carries, and the address behind `at`, for software that takes an address and not a name. The // case that found the third is a resolver pointing the container runtime at itself: the runtime's // list of resolvers is addresses, because a name there would have to be resolved by the resolver // it names. Nothing about what a machine is *for*: that would be the mesh learning what a module // means, which it does not do. // ofMachine is where a module says a fact about the machine underneath it belongs: // ${machine:}. var ofMachine = regexp.MustCompile(`\$\{machine:([a-z0-9][a-z0-9_-]*)\}`) // machineUsed are the keys a file's content asks for, first appearance first. func machineUsed(content string) []string { var used []string seen := map[string]bool{} for _, m := range ofMachine.FindAllStringSubmatch(content, -1) { if key := m[1]; !seen[key] { seen[key] = true used = append(used, key) } } return used } // machineFacts is what a module may name about the machine it was assigned to. // // `at` is absent rather than empty when the machine is not on the private network. A module asking // to be reached at an address that does not exist is a misconfiguration, and it is said here — // where the module and the machine are both named — rather than discovered later as a certificate // nobody can verify. // // `address` is what `at` resolves to, read from the names the control plane composed — the same map // the hosts file and the resolver's wildcards are written from, so a file naming the machine's // address and the file every other machine reaches it by cannot disagree. Absent, like `at`, when // the machine is off the network or the mesh has not placed it. func machineFacts(r Resolution, names map[string]string, meshRange string) map[string]string { out := map[string]string{"name": r.Node} if r.At != "" { out["at"] = r.At if address := names[r.At]; address != "" { out["address"] = address } } // The private network's whole range — a mesh-wide fact, not this machine's, but named here // because a module cannot know it and sometimes must (an intrusion filter that must never ban a // tunnel peer). Absent when the mesh has no range to give. if meshRange != "" { out["mesh-range"] = meshRange } // The operator's login on this machine and where its home is (novox/hq to-be 29), so a module // that writes operator config names the account and its home rather than a value it cannot know. // Absent when no operator account is known — a headless box a person never logs into. if r.Account != "" { out["account"] = r.Account out["account-home"] = accountHomeOf(r.Account, r.AccountHome) } return out } // accountHomeOf is where an account's home is: what was stored, or the derived default — /root for // root, /home/ otherwise. The one place the default is written, so a fact and the store // cannot disagree about it. func accountHomeOf(account, home string) string { if home != "" { return home } if account == "root" { return "/root" } return "/home/" + account } // machineInto replaces a file's ${machine:…} placeholders with what the mesh knows about the // machine the module was assigned to. // // A key the mesh does not hold is refused, for the same reason a binding's is: left alone, the // literal would be written into a configuration file and read as a value. func machineInto(resource map[string]any, facts map[string]string, module string) error { // Content, and now the path and owner too: a module that writes into a person's home names it // with ${machine:account-home} and ${machine:account}, which it cannot know until assigned // (novox/hq to-be 29), the same reason its content names ${machine:address}. for _, field := range []string{"path", "owner", "content"} { s, ok := resource[field].(string) if !ok { continue } for _, key := range machineUsed(s) { value, has := facts[key] if !has { return fmt.Errorf( "%s has a %s that says ${machine:%s}, and this machine says %s", module, field, key, orNothing(namesOfFacts(facts))) } s = strings.ReplaceAll(s, fmt.Sprintf("${machine:%s}", key), value) resource[field] = s } } return nil } func namesOfFacts(facts map[string]string) []string { out := make([]string, 0, len(facts)) for k := range facts { out = append(out, k) } sort.Strings(out) return out }