The mesh owns the operator's ~/.ssh: account fact + home-scoped resources (to-be 29)
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.
This commit is contained in:
@@ -71,32 +71,53 @@ func machineFacts(r Resolution, names map[string]string, meshRange string) map[s
|
||||
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 {
|
||||
if fmt.Sprint(resource["type"]) != "file" {
|
||||
return nil
|
||||
}
|
||||
content, ok := resource["content"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, key := range machineUsed(content) {
|
||||
value, has := facts[key]
|
||||
if !has {
|
||||
return fmt.Errorf(
|
||||
"%s has a file that says ${machine:%s}, and this machine says %s",
|
||||
module, key, orNothing(namesOfFacts(facts)))
|
||||
// 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
|
||||
}
|
||||
resource["content"] = strings.ReplaceAll(
|
||||
content, fmt.Sprintf("${machine:%s}", key), value)
|
||||
content = resource["content"].(string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user