Third review of mesh-catalog #147. A unit's name can now come from a setting (a template's instance), so every systemctl call that names a unit passes `--` before it, and a name can never be read as an option. On removal, a unit is started or enabled again only when it is exactly the stopped and disabled the mesh held it to. A masked unit, or one somebody started or enabled since, is left as it is, and the report says why. A restore that fails (the start refused, or the unit not running after it) puts back what this attempt changed and is reported as waiting, so no apply is made fatal by it and the next one tries again. The tests' fake service managers read systemctl's arguments in the shape they were written for (oldShape); internal/system's own test holds the `--`.
234 lines
7.7 KiB
Go
234 lines
7.7 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0252 and issue 247: a module puts the operator's account in a group by declaring
|
|
// the account with that group alone; the account's other groups are never touched; a new login is said;
|
|
// and the mesh gives back only a group it put the account in, and only when nobody declared still asks.
|
|
|
|
// groupDB is a fake user and group database: the account's groups, the groups the machine has, and every
|
|
// command it was asked.
|
|
type groupDB struct {
|
|
account string
|
|
in map[string]bool
|
|
exists map[string]bool
|
|
asked []string
|
|
}
|
|
|
|
func newGroupDB(in []string, exists ...string) *groupDB {
|
|
g := &groupDB{account: "operator", in: map[string]bool{}, exists: map[string]bool{}}
|
|
for _, x := range in {
|
|
g.in[x], g.exists[x] = true, true
|
|
}
|
|
for _, x := range exists {
|
|
g.exists[x] = true
|
|
}
|
|
return g
|
|
}
|
|
|
|
func (g *groupDB) run(_ context.Context, name string, args ...string) (string, error) {
|
|
|
|
args = oldShape(name, args)
|
|
g.asked = append(g.asked, name+" "+strings.Join(args, " "))
|
|
switch {
|
|
case name == "getent" && args[0] == "passwd":
|
|
if args[1] == g.account {
|
|
return g.account + ":x:1500:1500::/home/" + g.account + ":/bin/bash\n", nil
|
|
}
|
|
return "", errors.New("getent exited 2: ")
|
|
case name == "getent" && args[0] == "group":
|
|
if g.exists[args[1]] {
|
|
return args[1] + ":x:900:\n", nil
|
|
}
|
|
return "", errors.New("getent exited 2: ")
|
|
case name == "id":
|
|
var out []string
|
|
for x := range g.in {
|
|
out = append(out, x)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, " ") + "\n", nil
|
|
case name == "usermod" && args[0] == "--append":
|
|
if !g.exists[args[2]] {
|
|
return "", errors.New("usermod exited 6: group '" + args[2] + "' does not exist")
|
|
}
|
|
g.in[args[2]] = true
|
|
case name == "gpasswd" && args[0] == "--delete":
|
|
delete(g.in, args[2])
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (g *groupDB) groups() string {
|
|
var out []string
|
|
for x := range g.in {
|
|
out = append(out, x)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
func applyGroupsOf(t *testing.T, g *groupDB, known store.State, resources ...string) (Report, store.State, error) {
|
|
t.Helper()
|
|
if len(resources) == 0 {
|
|
resources = []string{`{"id":"other.dir","type":"directory","path":"` + t.TempDir() + `/other"}`}
|
|
}
|
|
return Apply(context.Background(), archHost(t), parse(t, `{"declaration":1,"resources":[`+
|
|
strings.Join(resources, ",")+`]}`), known, store.OriginDeclared, g.run, nil, nil)
|
|
}
|
|
|
|
const (
|
|
razer = `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer"]}`
|
|
docker = `{"id":"docker.account","type":"user","name":"operator","groups":["docker"]}`
|
|
shell = `{"id":"zsh.login","type":"user","name":"operator","groups":[]}`
|
|
)
|
|
|
|
func TestAModulePutsTheAccountInAGroupAndSaysANewLoginIsNeeded(t *testing.T) {
|
|
g := newGroupDB([]string{"wheel", "plugdev"}, "openrazer")
|
|
report, state, err := applyGroupsOf(t, g, store.State{}, shell, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "openrazer plugdev wheel" {
|
|
t.Fatalf("the account's groups are %q", got)
|
|
}
|
|
o := outcomeOf(report, "openrazer.account")
|
|
if o.Action != "updated" || !strings.Contains(o.Detail, "put in openrazer") || !strings.Contains(o.Detail, "new login") {
|
|
t.Errorf("the outcome did not say the group and the new login: %+v", o)
|
|
}
|
|
a, _ := state.Find("openrazer.account")
|
|
if strings.Join(a.Groups, " ") != "openrazer" {
|
|
t.Errorf("the record holds %v, want the one group the mesh added", a.Groups)
|
|
}
|
|
for _, asked := range g.asked {
|
|
if strings.HasPrefix(asked, "usermod") && !strings.HasPrefix(asked, "usermod --append --groups openrazer ") {
|
|
t.Errorf("usermod was asked something other than appending the one group: %q", asked)
|
|
}
|
|
}
|
|
// Applied again: nothing to do, and the record still says the mesh added it.
|
|
report, state, err = applyGroupsOf(t, g, state, shell, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "openrazer.account"); o.Action != "unchanged" {
|
|
t.Errorf("a second apply changed something: %+v", o)
|
|
}
|
|
if a, _ := state.Find("openrazer.account"); strings.Join(a.Groups, " ") != "openrazer" {
|
|
t.Errorf("a second apply lost what the mesh added: %v", a.Groups)
|
|
}
|
|
}
|
|
|
|
func TestAGroupTheAccountWasAlreadyInIsNeverTakenBack(t *testing.T) {
|
|
g := newGroupDB([]string{"wheel", "openrazer"})
|
|
_, state, err := applyGroupsOf(t, g, store.State{}, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a, _ := state.Find("openrazer.account"); len(a.Groups) != 0 {
|
|
t.Fatalf("a group found was recorded as the mesh's: %v", a.Groups)
|
|
}
|
|
if _, _, err := applyGroupsOf(t, g, state); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "openrazer wheel" {
|
|
t.Errorf("undeclaring took a found group: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTheGroupTheMeshAddedIsGivenBackWhenItsModuleGoes(t *testing.T) {
|
|
g := newGroupDB([]string{"wheel"}, "openrazer")
|
|
_, state, err := applyGroupsOf(t, g, store.State{}, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, state, err := applyGroupsOf(t, g, state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "wheel" {
|
|
t.Errorf("the account's groups after its module went: %q, want wheel alone", got)
|
|
}
|
|
o := outcomeOf(report, "openrazer.account")
|
|
if o.Action != "restored" || !strings.Contains(o.Detail, "taken out of openrazer") {
|
|
t.Errorf("the removal did not say the group was given back: %+v", o)
|
|
}
|
|
if _, still := state.Find("openrazer.account"); still {
|
|
t.Error("the record stayed")
|
|
}
|
|
}
|
|
|
|
func TestAGroupAnotherResourceStillAsksForStays(t *testing.T) {
|
|
both := `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer","video"]}`
|
|
video := `{"id":"media.account","type":"user","name":"operator","groups":["video"]}`
|
|
g := newGroupDB(nil, "openrazer", "video")
|
|
_, state, err := applyGroupsOf(t, g, store.State{}, both, video)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, err := applyGroupsOf(t, g, state, video)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "video" {
|
|
t.Errorf("the account's groups: %q, want video kept for media and openrazer given back", got)
|
|
}
|
|
if o := outcomeOf(report, "openrazer.account"); !strings.Contains(o.Detail, "media.account still asks for") {
|
|
t.Errorf("the removal did not say why video stayed: %+v", o)
|
|
}
|
|
}
|
|
|
|
func TestAGroupNoLongerDeclaredIsGivenBackWhileTheAccountStaysDeclared(t *testing.T) {
|
|
g := newGroupDB(nil, "openrazer", "input")
|
|
two := `{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer","input"]}`
|
|
_, state, err := applyGroupsOf(t, g, store.State{}, two)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, state, err = applyGroupsOf(t, g, state, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "openrazer" {
|
|
t.Errorf("the account's groups: %q, want input given back", got)
|
|
}
|
|
if a, _ := state.Find("openrazer.account"); strings.Join(a.Groups, " ") != "openrazer" {
|
|
t.Errorf("the record holds %v", a.Groups)
|
|
}
|
|
}
|
|
|
|
func TestAGroupThatDoesNotExistYetFailsTheResourceSayingSo(t *testing.T) {
|
|
g := newGroupDB([]string{"wheel"})
|
|
_, _, err := applyGroupsOf(t, g, store.State{}, razer)
|
|
if err == nil || !strings.Contains(err.Error(), "no such group yet") {
|
|
t.Fatalf("a missing group was not said: %v", err)
|
|
}
|
|
for _, asked := range g.asked {
|
|
if strings.HasPrefix(asked, "usermod") {
|
|
t.Errorf("usermod was run for a group the machine does not have: %q", asked)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAGroupAPersonTookTheAccountOutOfSinceIsPutBackWhileDeclared(t *testing.T) {
|
|
g := newGroupDB(nil, "openrazer")
|
|
_, state, err := applyGroupsOf(t, g, store.State{}, razer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
delete(g.in, "openrazer")
|
|
if _, _, err := applyGroupsOf(t, g, state, razer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := g.groups(); got != "openrazer" {
|
|
t.Errorf("a declared group was not put back: %q", got)
|
|
}
|
|
}
|