The service manager is a node seat with the unit verbs, and a user shape names the account (hq ADR 0176, 0177)
node-service-manager joins the mesh's own seats, node-scoped, serving eight
verbs — units, status, start, stop, restart, enable, disable, journal — each
with a schema that takes an optional scope, "system" or the operator
account's "user" manager. Sixteen seats now; the count test says so and names
the record.
${machine:account} resolves in a resource's `name` and `user` as it already
did in path, owner and content: the shell module makes the operator's account
its holder's login shell through the `user` shape, and the desktop's watchers
run as that account through a user-scoped unit (host change alongside).
Neither can name the person. A machine with no account refuses by name.
This commit is contained in:
@@ -102,8 +102,11 @@ func accountHomeOf(account, home string) string {
|
||||
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"} {
|
||||
// (novox/hq to-be 29), the same reason its content names ${machine:address}. And the name a
|
||||
// `user` shape sets the login shell of, and the user a user-scoped unit or a process runs as:
|
||||
// the shell module makes the operator's account its holder's login shell, and the desktop's
|
||||
// watchers run as that account (novox/hq ADR 0176, ADR 0177) — neither can name the person.
|
||||
for _, field := range []string{"path", "owner", "content", "name", "user"} {
|
||||
s, ok := resource[field].(string)
|
||||
if !ok {
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A `user` shape and a user-scoped unit name the operator account the way a home file does
|
||||
// (novox/hq ADR 0176, ADR 0177): with ${machine:account}, resolved when the module is assigned.
|
||||
func TestAUserShapeAndAUserScopedUnitNameTheAccount(t *testing.T) {
|
||||
facts := map[string]string{"account": "ops", "account-home": "/home/ops"}
|
||||
login := map[string]any{"type": "user", "id": "login", "name": "${machine:account}", "shell": "/usr/bin/zsh"}
|
||||
if err := machineInto(login, facts, "zsh"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if login["name"] != "ops" {
|
||||
t.Fatalf("the user shape did not learn the account: %v", login["name"])
|
||||
}
|
||||
watcher := map[string]any{"type": "service", "id": "watcher", "unit": "i3-reload-watcher.service",
|
||||
"scope": "user", "user": "${machine:account}"}
|
||||
if err := machineInto(watcher, facts, "i3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if watcher["user"] != "ops" {
|
||||
t.Fatalf("the user-scoped unit did not learn the account: %v", watcher["user"])
|
||||
}
|
||||
// A machine with no operator account refuses rather than writing the literal.
|
||||
err := machineInto(map[string]any{"type": "user", "id": "login", "name": "${machine:account}"},
|
||||
map[string]string{"address": "10.0.0.1"}, "zsh")
|
||||
if err == nil || !strings.Contains(err.Error(), "${machine:account}") {
|
||||
t.Fatalf("a user shape on a machine with no account was not refused by name: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The service manager is a seat of the mesh's own with the unit verbs as its contract (novox/hq
|
||||
// ADR 0177): every verb described, with a schema, taking a scope.
|
||||
func TestTheServiceManagerSeatServesTheUnitVerbs(t *testing.T) {
|
||||
seat, ok := SeatNamed("node-service-manager")
|
||||
if !ok {
|
||||
t.Fatal("node-service-manager is not a seat the mesh defines")
|
||||
}
|
||||
if seat.Scope != ScopeNode {
|
||||
t.Fatalf("the service manager is a role each machine has once, and the seat is %s-scoped", seat.Scope)
|
||||
}
|
||||
want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal"}
|
||||
var got []string
|
||||
for _, v := range seat.Serves {
|
||||
got = append(got, v.Name)
|
||||
if v.Description == "" || v.Input == nil {
|
||||
t.Fatalf("%s is promised without a description or a schema", v.Name)
|
||||
}
|
||||
props, _ := v.Input["properties"].(map[string]any)
|
||||
if _, has := props["scope"]; !has {
|
||||
t.Fatalf("%s takes no scope, and a user unit could not be asked for", v.Name)
|
||||
}
|
||||
}
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("the seat serves %v, not %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,12 @@ var defaultSeats = []Seat{
|
||||
"active found firewall's chains. An operator's act, by name, never a flush.",
|
||||
Input: schema(map[string]string{"where": "the rule set, as `node show` lists it"}, []string{"where"})},
|
||||
}},
|
||||
// The machine's service manager (novox/hq ADR 0177). The host applies every declared unit,
|
||||
// system or user scope; the holder answers questions and operator acts about them, each verb
|
||||
// taking the unit and an optional scope. The holder runs nothing of its own: its verbs are
|
||||
// served by the node tools runtime (ADR 0175).
|
||||
{Name: "node-service-manager", Scope: ScopeNode, Decision: "novox/hq ADR 0177",
|
||||
Serves: serviceManagerVerbs()},
|
||||
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
|
||||
// model change, not a rename, so it stays until that is built.
|
||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
@@ -392,3 +398,36 @@ func SeatsWithAProtocol() []Seat {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// serviceManagerVerbs is the contract every holder of node-service-manager serves (novox/hq ADR
|
||||
// 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an
|
||||
// optional scope — "system" when absent, "user" for the operator account's own manager — so a
|
||||
// caller asks for a user unit the way it asks for a system one.
|
||||
func serviceManagerVerbs() []Verb {
|
||||
scoped := func(more map[string]string, required []string) map[string]any {
|
||||
props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"}
|
||||
for k, v := range more {
|
||||
props[k] = v
|
||||
}
|
||||
return schema(props, required)
|
||||
}
|
||||
unit := map[string]string{"unit": "the unit's name, as the service manager knows it"}
|
||||
return []Verb{
|
||||
{Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
|
||||
Input: scoped(map[string]string{"pattern": "a glob the unit's name must match (optional)"}, nil)},
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "restart", Description: "Restart one unit.",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).",
|
||||
Input: scoped(unit, []string{"unit"})},
|
||||
{Name: "journal", Description: "The last lines of one unit's journal.",
|
||||
Input: scoped(map[string]string{"unit": unit["unit"], "lines": "how many lines from the end (default 100)"}, []string{"unit"})},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
if len(Seats()) != 15 {
|
||||
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
||||
// Sixteen since node-service-manager (novox/hq ADR 0177).
|
||||
if len(Seats()) != 16 {
|
||||
t.Errorf("the mesh defines %d seats rather than 16; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user