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:
@@ -67,8 +67,14 @@ func nodeCommand(ctx context.Context, args []string) error {
|
||||
// because the damage is already done by the time it prints.
|
||||
return publicDomain(ctx, inv, args[1:])
|
||||
|
||||
case "account":
|
||||
// The operator's login on this machine (novox/hq to-be 29): what a home-scoped file is
|
||||
// owned by and which account `ssh <node>` uses. Reports with no argument; sets with one;
|
||||
// an optional second argument is the home when it is not /home/<account>.
|
||||
return nodeAccount(ctx, inv, args[1:])
|
||||
|
||||
default:
|
||||
return fmt.Errorf("node has no %q; it has add, list, show and public-domain", args[0])
|
||||
return fmt.Errorf("node has no %q; it has add, list, show, public-domain and account", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +112,39 @@ func modeOf(n inventory.Node) string {
|
||||
}
|
||||
|
||||
// publicDomainUsage is the one description of the three forms, so a refusal and the help agree.
|
||||
// nodeAccount reports or sets a node's operator account (novox/hq to-be 29). Read-shaped with no
|
||||
// argument, like public-domain: `node account novox` answers, it does not change anything.
|
||||
func nodeAccount(ctx context.Context, inv *inventory.Inventory, positionals []string) error {
|
||||
if len(positionals) == 0 || len(positionals) > 3 {
|
||||
return errors.New("node account <name> — what it is now; " +
|
||||
"node account <name> <account> [home] — set it (home defaults to /home/<account>)")
|
||||
}
|
||||
node := positionals[0]
|
||||
if len(positionals) == 1 {
|
||||
who, err := inv.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if who.Account == "" {
|
||||
fmt.Printf("%s has no operator account known\n", node)
|
||||
fmt.Printf(" `node account %s <account>` sets it\n", node)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s logs a person in as %s (home %s)\n", node, who.Account, who.Home())
|
||||
return nil
|
||||
}
|
||||
home := ""
|
||||
if len(positionals) == 3 {
|
||||
home = positionals[2]
|
||||
}
|
||||
if err := inv.SetAccount(ctx, node, positionals[1], home); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s logs a person in as %s\n", node, positionals[1])
|
||||
fmt.Printf(" run `push %s` once ssh-client is assigned, to send its operator config\n", node)
|
||||
return nil
|
||||
}
|
||||
|
||||
const publicDomainUsage = "node public-domain <name> — what it is now; " +
|
||||
"<name> <domain> to set it; <name> --clear to take it away"
|
||||
|
||||
|
||||
@@ -83,9 +83,17 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
// The operator account this node logs a person in as, and where its home is (novox/hq to-be
|
||||
// 29) — carried so a home-scoped file's owner and path resolve for this machine.
|
||||
who, err := inv.NodeByName(ctx, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
resolved, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities,
|
||||
At: onNetwork[nodeName], PublicDomain: publicDomain}, world)
|
||||
At: onNetwork[nodeName], PublicDomain: publicDomain,
|
||||
Account: who.Account, AccountHome: who.AccountHome}, world)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
@@ -520,6 +528,19 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
|
||||
// Each machine's operator account, so an ssh Host block can name the login for every node
|
||||
// (novox/hq to-be 29). Keyed by the bare node name, which entriesFrom falls back to.
|
||||
allNodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
accounts := map[string]string{}
|
||||
for _, n := range allNodes {
|
||||
if n.Account != "" {
|
||||
accounts[n.Name] = n.Account
|
||||
}
|
||||
}
|
||||
|
||||
// And every routed name → the node that serves it (novox/hq ADR 0066). Alongside the
|
||||
// `<node>.internal` names above, so a container — or an internal ACME validator — resolves a
|
||||
// routed name to the proxy that serves it, mesh-wide. The mesh publishes the names it was told
|
||||
@@ -604,7 +625,7 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
||||
Machines: machines,
|
||||
Suffix: overlay.Suffix(), MeshRange: meshRange, Foundation: foundation, Kept: kept,
|
||||
Suffix: overlay.Suffix(), MeshRange: meshRange, Accounts: accounts, Foundation: foundation, Kept: kept,
|
||||
Adopted: record.Adopted,
|
||||
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built,
|
||||
}, record, nil
|
||||
|
||||
Reference in New Issue
Block a user