Put the tool runner in the own group of each module whose tools it runs (hq ADR 0334 §2)
A joining module's own directories belong to its own group, and its tools run in the tool runner, which could not read them. An identity may now name the tool runner's account: the engine puts it in mesh-<module> as it puts it in a library's group, says the change as identity:<module> for the runner's unit to restart once, takes it back when no identity names it, and refuses an account that is missing or root's. The engine states contract 7.
This commit is contained in:
@@ -1736,7 +1736,7 @@ func healthAsReported(st liveness.Statement, ns *network.Statement) *link.Health
|
||||
// LibraryContract: this engine reads a resource's declared `health` and judges it (ADR 0240 Phase B), judges a
|
||||
// user's declared `root` (novox/hq ADR 0266), and reads a declaration's identities, groups and libraries
|
||||
// (novox/hq ADR 0328), which is what tells the controller it may be sent any of those fields.
|
||||
h := &link.Health{Contract: link.LibraryContract, At: st.At.UTC(), Resources: []link.ResourceHealth{}}
|
||||
h := &link.Health{Contract: link.OwnGroupContract, At: st.At.UTC(), Resources: []link.ResourceHealth{}}
|
||||
for _, r := range st.Resources {
|
||||
h.Resources = append(h.Resources, link.ResourceHealth{Module: r.Module, Resource: r.ID, Kind: r.Kind,
|
||||
Target: r.Target, State: r.State, Reason: r.Reason, Since: r.Since.UTC(), Streak: r.Streak,
|
||||
|
||||
@@ -87,10 +87,10 @@ func TestTheStatementSaysAnAccountJudgedForRoot(t *testing.T) {
|
||||
Groups: []string{"openrazer"}}, State: accounts.Healthy, Since: at},
|
||||
}}
|
||||
h := withAccounts(healthAsReported(liveness.Statement{At: at}, nil), as)
|
||||
// At RootContract or later: an engine that also reads libraries says LibraryContract (novox/hq ADR 0328), one
|
||||
// above ADR 0325's IdentityContract.
|
||||
if h.Contract < link.RootContract || link.RootContract != 3 || h.Contract != link.LibraryContract ||
|
||||
link.LibraryContract != link.IdentityContract+1 {
|
||||
// At RootContract or later: an engine that also reads an identity's runner says OwnGroupContract (novox/hq ADR
|
||||
// 0334), one above ADR 0328's LibraryContract.
|
||||
if h.Contract < link.RootContract || link.RootContract != 3 || h.Contract != link.OwnGroupContract ||
|
||||
link.OwnGroupContract != link.LibraryContract+1 {
|
||||
t.Fatalf("contract %d", h.Contract)
|
||||
}
|
||||
got := map[string]string{}
|
||||
|
||||
+16
-1
@@ -627,6 +627,21 @@ func ApplyMindingWindows(
|
||||
"was attempted"
|
||||
continue
|
||||
}
|
||||
// The tool runner joins the module's own group, so its tools read what the module keeps (ADR 0334
|
||||
// §2); a change of `identity:<module>` restarts it once, through its unit's restart-on.
|
||||
if id.Runner != "" {
|
||||
moved, said, err := joinGroup(ctx, sys, id.Runner, id.Name, store.Membership{Module: id.Module}, &known, run)
|
||||
if err != nil {
|
||||
identityFailures = append(identityFailures, &Error{Resource: "identity " + id.Name, Err: err})
|
||||
log(fmt.Sprintf(" failed identity %s's runner: %v", id.Name, err))
|
||||
} else if moved {
|
||||
changed[IdentityChange(id.Module)] = true
|
||||
outcome.Detail = joinDetail(outcome.Detail, said)
|
||||
if outcome.Action == "unchanged" {
|
||||
outcome.Action = "updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
report.Outcomes = append(report.Outcomes, outcome)
|
||||
if outcome.Action != "unchanged" {
|
||||
log(fmt.Sprintf(" %s %s: %s", outcome.Action, outcome.ID, outcome.Detail))
|
||||
@@ -1051,7 +1066,7 @@ func ApplyMindingWindows(
|
||||
// Only the mesh says what is assigned: a carried bundle's silence undeclares no identity and no folder.
|
||||
if origin == store.OriginDeclared {
|
||||
// A tool runner no longer put in a library's group leaves it (ADR 0328 §11, ADR 0252 §3).
|
||||
for _, o := range leaveRunners(ctx, sys, d.Libraries, &known, run, wanted) {
|
||||
for _, o := range leaveRunners(ctx, sys, d.Libraries, d.Identities, &known, run, wanted) {
|
||||
report.Outcomes = append(report.Outcomes, o)
|
||||
log(fmt.Sprintf(" %s %s: %s", o.Action, o.ID, o.Detail))
|
||||
}
|
||||
|
||||
+45
-15
@@ -395,43 +395,70 @@ func JudgeLibrary(ctx context.Context, l declaration.Library) LibraryJudgement {
|
||||
return j
|
||||
}
|
||||
|
||||
// joinRunner puts a library's tool runner's account in its group (ADR 0328 §11), as ADR 0252 puts an account in a
|
||||
// group: recorded only when the mesh put it there, so one it was in before is never taken back. Whether it moved.
|
||||
// joinRunner puts a library's tool runner's account in its group (ADR 0328 §11).
|
||||
func joinRunner(ctx context.Context, sys system.System, l declaration.Library, known *store.State,
|
||||
run Runner) (bool, string, error) {
|
||||
if l.Runner == "" {
|
||||
return false, "", nil
|
||||
}
|
||||
in, err := system.GroupsOf(ctx, system.Runner(run), l.Runner)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("the tool runner's account %s could not be read: %w", l.Runner, err)
|
||||
return joinGroup(ctx, sys, l.Runner, l.Group, store.Membership{Library: l.Library}, known, run)
|
||||
}
|
||||
|
||||
// IdentityChange is what a tool runner joining a module's own group changes (ADR 0334 §2): `identity:<module>`,
|
||||
// which the tool runner's unit names under restart-on, as it names `library:<name>`.
|
||||
func IdentityChange(module string) string { return "identity:" + module }
|
||||
|
||||
// joinGroup puts a tool runner's account in a group (ADR 0328 §11, ADR 0334 §2), as ADR 0252 puts an account in a
|
||||
// group: recorded only when the mesh put it there, so one it was in before is never taken back. The account must be
|
||||
// here and never root's: uid 0, or a root group as its primary (§15). Whether it moved, and what was said.
|
||||
func joinGroup(ctx context.Context, sys system.System, account, group string, of store.Membership, known *store.State,
|
||||
run Runner) (bool, string, error) {
|
||||
who, found, err := lookUp(ctx, run, "passwd", account)
|
||||
switch {
|
||||
case err != nil:
|
||||
return false, "", fmt.Errorf("the tool runner's account %s could not be read: %w", account, err)
|
||||
case !found:
|
||||
return false, "", fmt.Errorf("the tool runner's account %s is not on this machine, so it joins no group", account)
|
||||
}
|
||||
if setOf(in)[l.Group] {
|
||||
if err := rootIdentity(who.id, who.prim); err != nil {
|
||||
return false, "", fmt.Errorf("the tool runner's account %s joins no group: %w", account, err)
|
||||
}
|
||||
in, err := system.GroupsOf(ctx, system.Runner(run), account)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("the tool runner's account %s could not be read: %w", account, err)
|
||||
}
|
||||
if setOf(in)[group] {
|
||||
return false, "", nil
|
||||
}
|
||||
if err := sys.AddUserToGroup(ctx, system.Runner(run), l.Runner, l.Group); err != nil {
|
||||
if err := sys.AddUserToGroup(ctx, system.Runner(run), account, group); err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
for _, m := range known.Members {
|
||||
if m.Account == l.Runner && m.Group == l.Group {
|
||||
return true, fmt.Sprintf("put the tool runner's %s back in %s", l.Runner, l.Group), nil
|
||||
if m.Account == account && m.Group == group {
|
||||
return true, fmt.Sprintf("put the tool runner's %s back in %s", account, group), nil
|
||||
}
|
||||
}
|
||||
known.Members = append(known.Members, store.Membership{Account: l.Runner, Group: l.Group, Library: l.Library})
|
||||
return true, fmt.Sprintf("put the tool runner's %s in %s; it holds it once restarted", l.Runner, l.Group), nil
|
||||
of.Account, of.Group = account, group
|
||||
known.Members = append(known.Members, of)
|
||||
return true, fmt.Sprintf("put the tool runner's %s in %s; it holds it once restarted", account, group), nil
|
||||
}
|
||||
|
||||
// leaveRunners takes every tool runner's account the mesh put in a library's group out of it once no declared
|
||||
// library puts it there and no declared user resource asks for it (ADR 0252 §3). Never fatal: what could not be
|
||||
// leaveRunners takes every tool runner's account the mesh put in a library's group, or a module's own group, out of
|
||||
// it once no declared library or identity puts it there and no declared user resource asks for it (ADR 0252 §3). Never fatal: what could not be
|
||||
// left stays recorded, said, for the next apply.
|
||||
func leaveRunners(ctx context.Context, sys system.System, libs []declaration.Library, known *store.State, run Runner,
|
||||
wanted Wanted) []Outcome {
|
||||
func leaveRunners(ctx context.Context, sys system.System, libs []declaration.Library, ids []declaration.Identity,
|
||||
known *store.State, run Runner, wanted Wanted) []Outcome {
|
||||
declared := map[string]bool{}
|
||||
for _, l := range libs {
|
||||
if l.Runner != "" {
|
||||
declared[l.Runner+" "+l.Group] = true
|
||||
}
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id.Runner != "" {
|
||||
declared[id.Runner+" "+id.Name] = true
|
||||
}
|
||||
}
|
||||
var out []Outcome
|
||||
var keep []store.Membership
|
||||
for _, m := range known.Members {
|
||||
@@ -440,6 +467,9 @@ func leaveRunners(ctx context.Context, sys system.System, libs []declaration.Lib
|
||||
continue
|
||||
}
|
||||
o := Outcome{ID: LibraryAccess(m.Library) + " runner", Type: "library", Target: m.Account}
|
||||
if m.Module != "" {
|
||||
o.ID, o.Type = "identity "+declaration.IdentityPrefix+m.Module+" runner", "identity"
|
||||
}
|
||||
if others := wanted.othersAsk(m.Account, m.Group, ""); len(others) > 0 {
|
||||
o.Action, o.Detail = "forgotten", fmt.Sprintf("left %s in %s, which %s asks for", m.Account, m.Group,
|
||||
strings.Join(others, ", "))
|
||||
|
||||
@@ -832,3 +832,92 @@ func TestAnApplyWaitsForALockThatIsNoWalk(t *testing.T) {
|
||||
t.Fatalf("a library whose root is not there was read as kept while its lock was held: %+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0334 §2: the tool runner's account joins a module's own group once, its unit restarted for it (a change of
|
||||
// identity:<module>), and is taken out once no identity names it; an account not here, or root's, is refused.
|
||||
func TestTheToolRunnerJoinsAModulesOwnGroup(t *testing.T) {
|
||||
m := &accountsMachine{users: map[string][2]int{"nox": {1003, 1003}}, groups: map[string][2]int{"nox": {1003, 0}}}
|
||||
d := withGroups(t, ``, `{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001,"runner":"nox"}`, ``)
|
||||
report, known, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(strings.Join(m.ran, "\n"), "usermod --append --groups mesh-radarr nox") {
|
||||
t.Fatalf("the tool runner was not put in the module's own group: %v", m.ran)
|
||||
}
|
||||
if o := outcomeOf(report, "identity mesh-radarr"); !strings.Contains(o.Detail, "tool runner") ||
|
||||
len(known.Members) != 1 || known.Members[0].Module != "radarr" {
|
||||
t.Fatalf("its joining is not said, or not recorded: %+v %+v", o, known.Members)
|
||||
}
|
||||
m.ran = nil
|
||||
if _, known, err = Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, m.run, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(strings.Join(m.ran, "\n"), "usermod") {
|
||||
t.Fatal("the tool runner was put in its group again")
|
||||
}
|
||||
none := withGroups(t, ``, `{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001}`, ``)
|
||||
if _, known, err = Apply(context.Background(), archHost(t), none, known, store.OriginDeclared, m.run, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(strings.Join(m.ran, "\n"), "gpasswd --delete nox mesh-radarr") || len(known.Members) != 0 {
|
||||
t.Fatalf("the tool runner was not taken out of a group no identity puts it in: %v", m.ran)
|
||||
}
|
||||
|
||||
was := RootGroupIDs
|
||||
RootGroupIDs = func() map[int]string { return map[int]string{10: "wheel"} }
|
||||
defer func() { RootGroupIDs = was }()
|
||||
for runner, says := range map[string]string{"ghost": "not on this machine", "root": "never root",
|
||||
"admin": "root group"} {
|
||||
m := &accountsMachine{users: map[string][2]int{"root": {0, 0}, "admin": {1001, 10}}, groups: map[string][2]int{}}
|
||||
d := withGroups(t, ``, `{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001,"runner":"`+
|
||||
runner+`"}`, ``)
|
||||
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), says) {
|
||||
t.Errorf("%s: not refused saying %q: %v", runner, says, err)
|
||||
}
|
||||
if strings.Contains(strings.Join(m.ran, "\n"), "usermod --append") {
|
||||
t.Errorf("%s: put in a group", runner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0334 §2: a service naming identity:<module> under restart-on is restarted when the tool runner joins the
|
||||
// module's own group, and not on an apply where nothing moved.
|
||||
func TestTheToolRunnerIsRestartedOnceForTheOwnGroup(t *testing.T) {
|
||||
m := &accountsMachine{users: map[string][2]int{"nox": {1003, 1003}}, groups: map[string][2]int{"nox": {1003, 0}}}
|
||||
d := withGroups(t, ``, `{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001,"runner":"nox"}`,
|
||||
`{"id":"node-tools.unit","type":"service","unit":"node-tools.service","state":"running",`+
|
||||
`"restart-on":["identity:radarr"]}`)
|
||||
var verbs []string
|
||||
systemd := systemctlStub(t, "loaded", "active", "enabled", &verbs)
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "systemctl" {
|
||||
return systemd(ctx, name, args...)
|
||||
}
|
||||
return m.run(ctx, name, args...)
|
||||
}
|
||||
stops := func() int {
|
||||
n := 0
|
||||
for _, v := range verbs {
|
||||
if v == "stop" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
_, known, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stops() != 1 {
|
||||
t.Fatalf("the tool runner was restarted %d times for joining the own group, and once was wanted", stops())
|
||||
}
|
||||
verbs = nil
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, run, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stops() != 0 {
|
||||
t.Fatalf("the tool runner was restarted %d times with nothing moved", stops())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1564,6 +1564,9 @@ type Identity struct {
|
||||
UID int `json:"uid"`
|
||||
GID int `json:"gid"`
|
||||
Groups []int `json:"groups,omitempty"`
|
||||
// Runner is the tool runner's account, put in the module's own group (novox/hq ADR 0334 §2) when its tools run
|
||||
// on this machine. Absent for a module that declares no tools here, and from a controller older than ADR 0334.
|
||||
Runner string `json:"runner,omitempty"`
|
||||
}
|
||||
|
||||
// Group is one library's group, as the controller allocated it (ADR 0328 §3).
|
||||
@@ -1707,6 +1710,9 @@ func checkIdentities(ids []Identity, groups []Group) []string {
|
||||
"declaration makes", where, g))
|
||||
}
|
||||
}
|
||||
if id.Runner != "" && !accountName.MatchString(id.Runner) {
|
||||
problems = append(problems, fmt.Sprintf("%s: its runner %q is not an account's name", where, id.Runner))
|
||||
}
|
||||
names[id.Name], uids[id.UID] = true, id.Name
|
||||
}
|
||||
return problems
|
||||
|
||||
@@ -274,6 +274,12 @@ const IdentityContract = 5
|
||||
// it, its presence is what tells the controller this host may be sent those fields.
|
||||
const LibraryContract = 6
|
||||
|
||||
// OwnGroupContract is the statement of a host that also reads an identity's `runner` (novox/hq ADR 0334 §2): it puts
|
||||
// the tool runner's account in the module's own group, takes it back when no identity names it, and says the change
|
||||
// as `identity:<module>`, which the tool runner's unit names under restart-on to be restarted once. Like the fields
|
||||
// before it, its presence is what tells the controller this host may be sent `runner` on an identity.
|
||||
const OwnGroupContract = 7
|
||||
|
||||
// Health is one statement of every long-running resource's state on this machine (to-be 48 §4). Said in
|
||||
// every report, as an event on each change, and again every minute while anything is not healthy — so a
|
||||
// lost statement is not a lost fault.
|
||||
|
||||
@@ -267,7 +267,9 @@ type MadeGroup struct {
|
||||
type Membership struct {
|
||||
Account string `json:"account"`
|
||||
Group string `json:"group"`
|
||||
Library string `json:"library"`
|
||||
// Library is the library whose group it is; Module, for a module's own group (ADR 0334 §2), the module.
|
||||
Library string `json:"library,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
}
|
||||
|
||||
// MadeIdentity is one module identity as the engine made it.
|
||||
|
||||
Reference in New Issue
Block a user