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:
@@ -46,6 +46,12 @@ type RosterFile struct {
|
||||
// whole. A property of the fact, not of the path: the format determines whether the file is
|
||||
// wholly the mesh's, not where a module happened to ask for it.
|
||||
Shared bool `json:"shared,omitempty"`
|
||||
// Home places the file under this node's operator-account home and chowns it to that account,
|
||||
// rather than at an absolute system path (novox/hq to-be 29). Then Path is home-relative
|
||||
// (`.ssh/config.d/mesh`), resolved against the account's home on the node it is composed for; a
|
||||
// node with no operator account gets no such file. This is how the ssh-client config — every
|
||||
// other node's Host block — is written into a person's home rather than into /etc.
|
||||
Home bool `json:"home,omitempty"`
|
||||
}
|
||||
|
||||
// rosterView is what a RosterFile's template sees. A closed shape — a template referencing a field
|
||||
@@ -57,11 +63,14 @@ type rosterView struct {
|
||||
Machines []rosterEntry
|
||||
}
|
||||
|
||||
// rosterEntry is one machine as a template sees it: its bare name, its full mesh name, its address.
|
||||
// rosterEntry is one machine as a template sees it: its bare name, its full mesh name, its address,
|
||||
// and the operator account to log into it as (novox/hq to-be 29) — empty when none is known, so an
|
||||
// ssh Host block template can omit the User line for a machine nobody has an account on.
|
||||
type rosterEntry struct {
|
||||
Name string
|
||||
FQDN string
|
||||
Address string
|
||||
Account string
|
||||
}
|
||||
|
||||
// FactsInto renders the roster files a module asked for, as files it will be given.
|
||||
@@ -70,7 +79,7 @@ type rosterEntry struct {
|
||||
// or a client does with it. This only puts it there. `every` is every name the mesh serves;
|
||||
// `machines` is only the machines — the two must not be confused (novox/hq 04-ISSUES/111), so both
|
||||
// are given and the template chooses.
|
||||
func FactsInto(m Manifest, r Resolution, every, machines map[string]string, suffix string) ([]map[string]any, error) {
|
||||
func FactsInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string) ([]map[string]any, error) {
|
||||
if len(m.Facts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -83,25 +92,39 @@ func FactsInto(m Manifest, r Resolution, every, machines map[string]string, suff
|
||||
view := rosterView{
|
||||
Node: r.Node,
|
||||
Suffix: strings.TrimPrefix(suffixOr(suffix), "."),
|
||||
Names: entriesFrom(every, suffix),
|
||||
Machines: entriesFrom(machines, suffix),
|
||||
Names: entriesFrom(every, accounts, suffix),
|
||||
Machines: entriesFrom(machines, accounts, suffix),
|
||||
}
|
||||
|
||||
out := make([]map[string]any, 0, len(names))
|
||||
for _, name := range names {
|
||||
fact := m.Facts[name]
|
||||
if !strings.HasPrefix(fact.Path, "/") {
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks for %q at %q, which is not an absolute path", m.Module, name, fact.Path)
|
||||
}
|
||||
content, err := renderRoster(fact.Template, view)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s cannot render %q: %w", m.Module, name, err)
|
||||
}
|
||||
// Where the file goes: under the operator's home and chowned to it (a home fact), or at the
|
||||
// absolute system path it names. A home fact on a machine with no operator account cannot be
|
||||
// placed, and is left out rather than written to nowhere (novox/hq to-be 29).
|
||||
path := fact.Path
|
||||
var owner string
|
||||
if fact.Home {
|
||||
if r.Account == "" {
|
||||
continue
|
||||
}
|
||||
path = accountHomeOf(r.Account, r.AccountHome) + "/" + strings.TrimLeft(fact.Path, "/")
|
||||
owner = r.Account
|
||||
} else if !strings.HasPrefix(fact.Path, "/") {
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks for %q at %q, which is not an absolute path", m.Module, name, fact.Path)
|
||||
}
|
||||
file := map[string]any{
|
||||
"id": "fact-" + name, "type": "file", "path": fact.Path, "mode": "0644",
|
||||
"id": "fact-" + name, "type": "file", "path": path, "mode": "0644",
|
||||
"content": content,
|
||||
}
|
||||
if owner != "" {
|
||||
file["owner"] = owner
|
||||
}
|
||||
if fact.Shared {
|
||||
// The host owns only the lines between `# BEGIN mesh <id>` and `# END mesh <id>` and
|
||||
// keeps the rest of the file byte for byte; undeclared, the region goes and nothing else
|
||||
@@ -136,11 +159,17 @@ func renderRoster(tmpl string, view rosterView) (string, error) {
|
||||
// and does not yet know where it is, which is the ordinary state between adding a machine and it
|
||||
// joining. Writing the name anyway would give a name that resolves to nothing, and a connection to
|
||||
// that hangs; leaving it out fails at once and says the name is unknown.
|
||||
func entriesFrom(addresses map[string]string, suffix string) []rosterEntry {
|
||||
func entriesFrom(addresses, accounts map[string]string, suffix string) []rosterEntry {
|
||||
out := make([]rosterEntry, 0, len(addresses))
|
||||
for _, name := range sortedNames(addresses) {
|
||||
internal, bare := meshName(name, suffix)
|
||||
out = append(out, rosterEntry{Name: bare, FQDN: internal, Address: addresses[name]})
|
||||
// The account is looked up by whichever key the caller keys accounts on — the internal name
|
||||
// or the bare one — so a template gets the right login however the maps were built.
|
||||
account := accounts[name]
|
||||
if account == "" {
|
||||
account = accounts[bare]
|
||||
}
|
||||
out = append(out, rosterEntry{Name: bare, FQDN: internal, Address: addresses[name], Account: account})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user