A node carries its operator account (name + home; migration 0036, Node.Account,
SetAccount, 'node account' CLI). The account and its home are offered as
machine facts ${machine:account} / ${machine:account-home}, and machineInto
now resolves placeholders in a resource's path and owner (not just content), so
a module writes into a person's home naming what it cannot know. A RosterFile
gains Home: the file is placed under the account's home and chowned to it, its
template sees each node's Account, and a machine with no account gets none —
this is how the ssh Host blocks for every node reach a person's ~/.ssh. Roster
carries per-node accounts (Rendering.Accounts). Tested, including ssh-client
composed end-to-end. Not deployed.
133 lines
5.5 KiB
Go
133 lines
5.5 KiB
Go
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, `<machine>.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:<key>}.
|
|
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/<account> 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
|
|
}
|