mesh/merge-gate head c7b534a08c98 merged into trunk 6fe82c61bd32 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c7b534a08c98 merged into trunk 6fe82c61bd32 — pass: its merge-check.sh passed
mesh/delivery delivered
A module's own directories need setgid so what is made beneath keeps its own group, and a mode of 2750 refused the whole declaration while every apply dropped the bit: a directory's mode now carries setgid (never setuid or sticky), set, kept and said. A tool runner's account that is missing or root's failed the whole apply and dropped the library's outcome: it is now a warning, said as runner:<module> or runner:library:<name>. And a runner that leaves a group is taken out before the resources apply, so its unit is restarted in that apply to lose it.
1025 lines
42 KiB
Go
1025 lines
42 KiB
Go
//go:build linux
|
|
|
|
package apply
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// The engine's half of ADR 0328's checks (novox/hq ADR 0328, "How it is checked", issue 567). The engine runs as
|
|
// root; a test may not, and a non-root test gives its folders a group it is in itself.
|
|
|
|
// testGID is a group the test may give its folders: any number as root, else one of its own added groups that is
|
|
// no root group's.
|
|
func testGID(t *testing.T) int {
|
|
t.Helper()
|
|
if os.Geteuid() == 0 {
|
|
return 61000
|
|
}
|
|
groups, _ := os.Getgroups()
|
|
for _, g := range groups {
|
|
if g != os.Getgid() && g != 0 && rootGroup(g) == "" {
|
|
return g
|
|
}
|
|
}
|
|
t.Skip("the test runs as no root and in no group of its own beside its primary one, so it can give no " +
|
|
"folder a library's group")
|
|
return 0
|
|
}
|
|
|
|
// libraries has the test keep its own record of libraries and judge mounts as the test says.
|
|
func libraries(t *testing.T, points, targets []string) {
|
|
t.Helper()
|
|
was, wasPoints, wasTargets := Libraries, MountPoints, MountTargets
|
|
Libraries = NewLibraryMemory()
|
|
forgetMountHistory()
|
|
MountPoints = func() []string { return append([]string{"/"}, points...) }
|
|
MountTargets = func(context.Context) []string { return targets }
|
|
t.Cleanup(func() {
|
|
Libraries, MountPoints, MountTargets = was, wasPoints, wasTargets
|
|
forgetMountHistory()
|
|
})
|
|
}
|
|
|
|
func forgetMountHistory() {
|
|
mountHistory.Lock()
|
|
mountHistory.seen = map[string]bool{}
|
|
mountHistory.Unlock()
|
|
}
|
|
|
|
// libraryDecl is a declaration with one library, its group, and resources.
|
|
func libraryDecl(t *testing.T, lib string, resources ...string) *declaration.Declaration {
|
|
t.Helper()
|
|
var l declaration.Library
|
|
if err := jsonInto(lib, &l); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw := fmt.Sprintf(`{"declaration":1,"groups":[{"name":%q,"gid":%d}],"libraries":[%s],"resources":[%s]}`,
|
|
l.Group, l.GID, lib, strings.Join(resources, ","))
|
|
if len(resources) == 0 {
|
|
raw = strings.Replace(raw, `"resources":[]`, `"owns_nothing":true,"resources":[]`, 1)
|
|
}
|
|
d, err := declaration.Parse([]byte(raw))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// libJSON is a library's declaration.
|
|
func libJSON(name, root string, gid int, home, taken bool, paths ...string) string {
|
|
quoted := make([]string, len(paths))
|
|
for i, p := range paths {
|
|
quoted[i] = strconv.Quote(p)
|
|
}
|
|
return fmt.Sprintf(`{"library":%q,"root":%q,"group":"mesh-%s","gid":%d,"home":%v,"taken":%v,"paths":[%s]}`,
|
|
name, root, name, gid, home, taken, strings.Join(quoted, ","))
|
|
}
|
|
|
|
// applyLib applies d with a runner that answers the group database for the library group (already there) and runs
|
|
// nothing else.
|
|
func applyLib(t *testing.T, d *declaration.Declaration, known store.State) (Report, store.State, error) {
|
|
t.Helper()
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if name == "getent" && args[0] == "group" {
|
|
for _, g := range d.Groups {
|
|
if args[1] == g.Name || args[1] == strconv.Itoa(g.GID) {
|
|
return fmt.Sprintf("%s:x:%d:\n", g.Name, g.GID), nil
|
|
}
|
|
}
|
|
return "", errors.New("getent exited 2")
|
|
}
|
|
return "", nil
|
|
}
|
|
return Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, run, nil, nil)
|
|
}
|
|
|
|
func statOf(t *testing.T, path string) unix.Stat_t {
|
|
t.Helper()
|
|
var st unix.Stat_t
|
|
if err := unix.Lstat(path, &st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return st
|
|
}
|
|
|
|
// Row 6: the root and every requested sub-path kept in the library's group with group rwx and setgid on every apply —
|
|
// those folders alone, never what is in them; owner, owner's and others' bits unchanged.
|
|
func TestALibraryIsKeptInItsGroup(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.Mkdir(root, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false, "movies"))
|
|
if _, _, err := applyLib(t, d, store.State{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Kept since it was empty; an app then narrows a requested folder and writes beneath it.
|
|
movies := filepath.Join(root, "movies")
|
|
if err := os.Chown(movies, -1, os.Getgid()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(movies, 0o705); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
film := filepath.Join(movies, "film")
|
|
if err := os.WriteFile(film, []byte("data"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
filmBefore := statOf(t, film)
|
|
before := statOf(t, movies)
|
|
report, _, err := applyLib(t, d, store.State{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "updated" {
|
|
t.Fatalf("a kept library was not put back: %+v", o)
|
|
}
|
|
after := statOf(t, movies)
|
|
if int(after.Gid) != gid || after.Mode&0o7777 != 0o2775 {
|
|
t.Fatalf("a requested path is not kept in the group with group rwx and setgid: gid %d, mode %o", after.Gid,
|
|
after.Mode&0o7777)
|
|
}
|
|
if after.Uid != before.Uid || after.Mode&0o707 != before.Mode&0o707 {
|
|
t.Errorf("the owner or the owner's and others' bits changed: %o to %o", before.Mode&0o7777, after.Mode&0o7777)
|
|
}
|
|
if f := statOf(t, film); f.Mode != filmBefore.Mode || f.Gid != filmBefore.Gid {
|
|
t.Error("a file under a kept path was changed without the take act")
|
|
}
|
|
if rec, _ := libraryRecord("media"); !rec.Kept || rec.How != store.KeptEmpty {
|
|
t.Errorf("not recorded kept as empty: %+v", rec)
|
|
}
|
|
}
|
|
|
|
// Row 9: a missing sub-path is made with the root's owner, the library's group, group rwx and setgid.
|
|
func TestAMissingSubPathIsMadeAsTheRootIs(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.Mkdir(root, 0o751); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(root, 0o751); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false, "tv/shows"))
|
|
if _, _, err := applyLib(t, d, store.State{}); err != nil {
|
|
t.Fatalf("an empty root was not kept: %v", err)
|
|
}
|
|
rst := statOf(t, root)
|
|
for _, p := range []string{filepath.Join(root, "tv"), filepath.Join(root, "tv", "shows")} {
|
|
st := statOf(t, p)
|
|
if st.Uid != rst.Uid || int(st.Gid) != gid || st.Mode&0o7777 != 0o2771 {
|
|
t.Errorf("%s made as uid %d gid %d mode %o, and the root's owner, the group and 2771 were wanted", p,
|
|
st.Uid, st.Gid, st.Mode&0o7777)
|
|
}
|
|
}
|
|
if rec, _ := libraryRecord("media"); rec.How != store.KeptEmpty {
|
|
t.Errorf("an empty root is not kept as empty: %+v", rec)
|
|
}
|
|
}
|
|
|
|
// Row 7 (keep wait) and §8: a root that holds data not in the group waits with reason keep, and nothing is changed.
|
|
func TestARootThatHoldsDataWaitsForTheOperatorsWord(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.MkdirAll(filepath.Join(root, "movies"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := statOf(t, root)
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false, "movies"))
|
|
report, _, err := applyLib(t, d, store.State{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, KeepWords("media")) {
|
|
t.Fatalf("a root holding data did not wait for the operator: %+v", o)
|
|
}
|
|
if after := statOf(t, root); after.Gid != before.Gid || after.Mode != before.Mode {
|
|
t.Fatal("the root was changed without a warrant")
|
|
}
|
|
j := JudgeLibrary(context.Background(), d.Libraries[0])
|
|
if j.Kind != VerdictWaitKeep || j.Placed == "" || strings.Contains(KeepWords("media")+DiskWords("media"), "/") {
|
|
t.Fatalf("the look does not wait for the operator, naming the filesystem with no path: %+v", j)
|
|
}
|
|
}
|
|
|
|
// Row 7, the act: group, group write and setgid set under the root; no link followed; no other filesystem entered;
|
|
// a file with another name elsewhere left; owner, owner's and others' bits unchanged; nothing without a warrant
|
|
// (the act's ask, internal/link); an undeclared library refused; a walk over its bound goes on from its cursor.
|
|
func TestTheTakeActSetsTheGroupUnderTheRoot(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
base := t.TempDir()
|
|
root := filepath.Join(base, "media")
|
|
outside := filepath.Join(base, "outside")
|
|
for _, d := range []string{filepath.Join(root, "movies", "a film"), outside} {
|
|
if err := os.MkdirAll(d, 0o705); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
file := filepath.Join(root, "movies", "a film", "film.mkv")
|
|
if err := os.WriteFile(file, []byte("data"), 0o604); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(file, 0o4604); err != nil { // its own setuid bit, which a group change drops
|
|
t.Fatal(err)
|
|
}
|
|
secret := filepath.Join(outside, "secret")
|
|
if err := os.WriteFile(secret, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Link(secret, filepath.Join(root, "movies", "hardlink")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if os.Geteuid() == 0 { // an account's file, as an *arr app's hardlinked download is
|
|
if err := os.Chown(secret, 65534, -1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.Symlink(outside, filepath.Join(root, "link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false, "movies"))
|
|
if _, _, err := applyLib(t, d, store.State{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := TakeLibrary(context.Background(), d, "library:books", root, "w", "op"); err == nil {
|
|
t.Fatal("an undeclared library was taken")
|
|
}
|
|
// Bounded: three entries a walk, gone on from the cursor until done.
|
|
was := ActLimits
|
|
ActLimits = WalkLimits{Entries: 3, Time: time.Minute}
|
|
defer func() { ActLimits = was }()
|
|
done, said, err := TakeLibrary(context.Background(), d, "library:media", root, "w-1", "the operator")
|
|
if err != nil {
|
|
t.Fatalf("the take act was refused: %v", err)
|
|
}
|
|
<-done
|
|
if !strings.Contains(said, "w-1") {
|
|
t.Errorf("the act does not say its warrant: %s", said)
|
|
}
|
|
rec, _ := libraryRecord("media")
|
|
if rec.Act == nil || rec.Act.State != "stopped" || len(rec.Cursor) == 0 {
|
|
t.Fatalf("a walk over its bound does not say where it stopped: %+v %+v", rec.Act, rec.Cursor)
|
|
}
|
|
for i := 0; i < 10; i++ {
|
|
ReconcileLibraries(context.Background(), d)
|
|
if rec, _ = libraryRecord("media"); rec.Act.State == "done" {
|
|
break
|
|
}
|
|
}
|
|
if rec.Act.State != "done" || rec.Act.Linked != 1 || rec.Act.Stripped != 1 || !rec.Kept || rec.How != store.KeptByWarrant ||
|
|
rec.KeptOn == "" {
|
|
t.Fatalf("the act is not recorded done under its warrant on its filesystem: %+v %+v", rec, rec.Act)
|
|
}
|
|
for _, p := range []string{root, filepath.Join(root, "movies"), filepath.Join(root, "movies", "a film")} {
|
|
if st := statOf(t, p); int(st.Gid) != gid || st.Mode&0o7777 != 0o2775 {
|
|
t.Errorf("%s: gid %d mode %o, and the group with 2775 (705 plus group rwx and setgid) was wanted", p,
|
|
st.Gid, st.Mode&0o7777)
|
|
}
|
|
}
|
|
if st := statOf(t, file); int(st.Gid) != gid || st.Mode&0o7777 != 0o0664 {
|
|
t.Errorf("a file: gid %d mode %o, and the group with 0664 (its setuid bit taken by the group change and "+
|
|
"never put back) was wanted", st.Gid, st.Mode&0o7777)
|
|
}
|
|
if st := statOf(t, secret); int(st.Gid) != gid || st.Mode&0o7777 != 0o660 {
|
|
t.Error("an account's file with another name (a hardlinked download) was not given the group")
|
|
}
|
|
if st := statOf(t, outside); int(st.Gid) == gid {
|
|
t.Error("the act followed a link out of the library")
|
|
}
|
|
}
|
|
|
|
// Row 7, the act: another filesystem is not entered — a folder whose device is not the root's is passed by.
|
|
func TestTheTakeActEntersNoOtherFilesystem(t *testing.T) {
|
|
gid := testGID(t)
|
|
root := t.TempDir()
|
|
mount := filepath.Join(root, "elsewhere")
|
|
if err := os.Mkdir(mount, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stats := walkStats{
|
|
at: func(dirfd int, name string, st *unix.Stat_t) error {
|
|
err := unix.Fstatat(dirfd, name, st, unix.AT_SYMLINK_NOFOLLOW)
|
|
if name == "elsewhere" {
|
|
st.Dev++
|
|
}
|
|
return err
|
|
},
|
|
fd: unix.Fstat,
|
|
}
|
|
w, err := walkRoot(root, ActLimits, stats, nil, groupVisit(gid))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w.counts.Mounts != 1 || int(statOf(t, mount).Gid) == gid {
|
|
t.Fatalf("the walk entered another filesystem: %+v", w.counts)
|
|
}
|
|
}
|
|
|
|
// Row 9: a root that is absent, or whose disk is not mounted, makes nothing, holds the containers that mount it,
|
|
// and waits with reason disk; off its home, a mount that is not there holds them too.
|
|
func TestALibraryWhoseDiskIsNotThereHoldsItsContainers(t *testing.T) {
|
|
gid := testGID(t)
|
|
base := t.TempDir()
|
|
disk := filepath.Join(base, "disk")
|
|
if err := os.Mkdir(disk, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
why, root string
|
|
home bool
|
|
targets []string
|
|
}{
|
|
{"its root absent", filepath.Join(base, "gone", "media"), true, nil},
|
|
{"its disk not mounted", filepath.Join(disk, "media"), true, []string{disk}},
|
|
{"its mount not there", disk, false, nil},
|
|
} {
|
|
libraries(t, nil, tc.targets)
|
|
d := libraryDecl(t, libJSON("media", tc.root, gid, tc.home, false, "movies"),
|
|
`{"id":"radarr.app","type":"container","name":"radarr","image":"`+pinned+`","volumes":["`+
|
|
filepath.Join(tc.root, "movies")+`:/movies"]}`)
|
|
var started bool
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if len(args) > 0 && args[0] == "run" {
|
|
started = true
|
|
}
|
|
if name == "getent" && args[0] == "group" {
|
|
return fmt.Sprintf("mesh-media:x:%d:\n", gid), nil
|
|
}
|
|
return "", nil
|
|
}
|
|
report, _, _ := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, run, nil, nil)
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, DiskWords("media")) {
|
|
t.Errorf("%s: the library does not wait for its disk: %+v", tc.why, o)
|
|
}
|
|
if o := outcomeOf(report, "radarr.app"); o.Action != "waiting" || started {
|
|
t.Errorf("%s: a container that mounts it was not held: %+v", tc.why, o)
|
|
}
|
|
if !WaitingOnLibrary(d)["radarr.app"] {
|
|
t.Errorf("%s: liveness is not told the container waits", tc.why)
|
|
}
|
|
if j := JudgeLibrary(context.Background(), d.Libraries[0]); j.Kind != VerdictWaitDisk {
|
|
t.Errorf("%s: the look judges it %s", tc.why, j.Kind)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(tc.root, "movies")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Errorf("%s: something was made", tc.why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Row 3: the library's group made with its number; a name or number that is another's refused; what was made, and
|
|
// only that, removed when no longer declared.
|
|
func TestALibrarysGroupIsMadeWithItsNumber(t *testing.T) {
|
|
m := &accountsMachine{users: map[string][2]int{}, groups: map[string][2]int{"media": {2000, 0}}}
|
|
d := withGroups(t, `{"name":"mesh-media","gid":62000}`, ``, ``)
|
|
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.Join(m.ran, "\n") != "groupadd --system --gid 62000 mesh-media" {
|
|
t.Fatalf("made with: %v", m.ran)
|
|
}
|
|
if o := outcomeOf(report, "group mesh-media"); o.Action != "created" || len(known.Groups) != 1 {
|
|
t.Fatalf("not recorded as made: %+v %+v", o, known.Groups)
|
|
}
|
|
clash := withGroups(t, `{"name":"mesh-books","gid":2000}`, ``, ``)
|
|
if _, _, err := Apply(context.Background(), archHost(t), clash, known, store.OriginDeclared, m.run, nil, nil); err == nil ||
|
|
!strings.Contains(err.Error(), "media") {
|
|
t.Errorf("a number that is another group's was not refused naming it: %v", err)
|
|
}
|
|
m.ran = nil
|
|
none := withGroups(t, ``, ``, `{"id":"x.d","type":"directory","path":"`+t.TempDir()+`/x"}`)
|
|
if _, known, err = Apply(context.Background(), archHost(t), none, known, store.OriginDeclared, m.run, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, still := m.groups["mesh-media"]; still || len(known.Groups) != 0 {
|
|
t.Fatalf("a library group no longer declared was not removed: %v", m.ran)
|
|
}
|
|
if _, kept := m.groups["media"]; !kept {
|
|
t.Fatal("a group the mesh did not make was removed")
|
|
}
|
|
}
|
|
|
|
// Row 4: a module's user with its own group (its uid's number), its first library's group as primary and its other
|
|
// libraries' as added groups.
|
|
func TestAModulesUserIsInItsLibrariesGroups(t *testing.T) {
|
|
m := &accountsMachine{users: map[string][2]int{}, groups: map[string][2]int{}}
|
|
d := withGroups(t, `{"name":"mesh-media","gid":62000},{"name":"mesh-books","gid":62001}`,
|
|
`{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":62000,"groups":[62001]}`, ``)
|
|
if _, known, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil,
|
|
nil); err != nil {
|
|
t.Fatal(err)
|
|
} else if len(known.Identities) != 1 {
|
|
t.Fatalf("the identity is not recorded: %+v", known.Identities)
|
|
}
|
|
want := []string{
|
|
"groupadd --system --gid 62000 mesh-media",
|
|
"groupadd --system --gid 62001 mesh-books",
|
|
"groupadd --system --gid 61001 mesh-radarr",
|
|
"useradd --system --uid 61001 --gid 62000 --groups mesh-books --no-create-home --home-dir /nonexistent --shell " +
|
|
"/usr/bin/nologin mesh-radarr",
|
|
}
|
|
if strings.Join(m.ran, "\n") != strings.Join(want, "\n") {
|
|
t.Fatalf("made with:\n%s\nwant:\n%s", strings.Join(m.ran, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
}
|
|
|
|
// Row 11: the tool runner's account is put in each library's group once, its unit restarted for it (a change of
|
|
// library:<name>), and taken out once no library puts it there.
|
|
func TestTheToolRunnerIsInItsLibrariesGroups(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, []string{"/srv/media"}, nil)
|
|
m := &accountsMachine{users: map[string][2]int{"nox": {1003, 1003}}, groups: map[string][2]int{"nox": {1003, 0}}}
|
|
lib := `{"library":"media","root":"/srv/media","group":"mesh-media","gid":` + strconv.Itoa(gid) +
|
|
`,"home":false,"runner":"nox"}`
|
|
d := libraryDecl(t, lib)
|
|
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-media nox") {
|
|
t.Fatalf("the tool runner was not put in the library's group: %v", m.ran)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "updated" || len(known.Members) != 1 {
|
|
t.Fatalf("its joining is not a change of library:media, 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, ``, ``, `{"id":"x.d","type":"directory","path":"`+t.TempDir()+`/x"}`)
|
|
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-media") || len(known.Members) != 0 {
|
|
t.Fatalf("the tool runner was not taken out of a group no library puts it in: %v", m.ran)
|
|
}
|
|
}
|
|
|
|
// Row 15: a library group, an identity's added group or an image told a root group's number is refused with its
|
|
// reason, and nothing is made or started.
|
|
func TestNoLibraryGroupIsRoot(t *testing.T) {
|
|
was := RootGroupIDs
|
|
RootGroupIDs = func() map[int]string { return map[int]string{10: "wheel"} }
|
|
defer func() { RootGroupIDs = was }()
|
|
dir := t.TempDir()
|
|
for _, d := range []*declaration.Declaration{
|
|
withGroups(t, `{"name":"mesh-media","gid":10}`, ``, `{"id":"m.d","type":"directory","path":"`+dir+`/d"}`),
|
|
withGroups(t, `{"name":"mesh-media","gid":10}`,
|
|
`{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001,"groups":[10]}`,
|
|
`{"id":"m.d","type":"directory","path":"`+dir+`/d"}`),
|
|
} {
|
|
m := &accountsMachine{users: map[string][2]int{}, groups: map[string][2]int{}}
|
|
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "wheel") {
|
|
t.Errorf("a root group was not refused naming why: %v", err)
|
|
}
|
|
if len(m.ran) != 0 {
|
|
t.Errorf("something was made: %v", m.ran)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "d")); err == nil {
|
|
t.Error("a resource of a declaration naming a root group was applied")
|
|
}
|
|
}
|
|
if _, err := declaration.Parse([]byte(`{"declaration":1,"groups":[{"name":"mesh-media","gid":0}],` +
|
|
`"owns_nothing":true,"resources":[]}`)); err == nil || !strings.Contains(err.Error(), "never root") {
|
|
t.Errorf("a library group of number 0 was not refused on arrival: %v", err)
|
|
}
|
|
}
|
|
|
|
// A library's filesystem that cannot be named this time is unknown, never a move; another filesystem at the root is.
|
|
func TestALibraryNotNamedIsUnknownAndAnotherFilesystemIsAMove(t *testing.T) {
|
|
gid := testGID(t)
|
|
base := t.TempDir()
|
|
pool := filepath.Join(base, "pool")
|
|
root := filepath.Join(pool, "media")
|
|
if err := os.MkdirAll(filepath.Join(root, "old"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
libraries(t, []string{pool}, nil)
|
|
uuid := "6f1c0a4e-1b2d-4c3e-8f00-aa11bb22cc33"
|
|
wasUUID := MountUUID
|
|
MountUUID = func(p string) string {
|
|
if p == pool {
|
|
return uuid
|
|
}
|
|
return "x"
|
|
}
|
|
defer func() { MountUUID = wasUUID }()
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false))
|
|
if _, _, err := applyLib(t, d, store.State{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
done, _, err := TakeLibrary(context.Background(), d, "library:media", root, "w-1", "op")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-done
|
|
taken := libraryDecl(t, libJSON("media", root, gid, true, true))
|
|
before, _ := libraryRecord("media")
|
|
uuid = "" // nothing names it now
|
|
wasFsid := MountFsid
|
|
MountFsid = func(string) string { return "" }
|
|
wasSource := MountSource
|
|
MountSource = func(string) (string, string) { return "", "" }
|
|
report, _, _ := applyLib(t, taken, store.State{})
|
|
MountFsid, MountSource = wasFsid, wasSource
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, "could not be named") {
|
|
t.Fatalf("an unnamed filesystem is not said so: %+v", o)
|
|
}
|
|
if after, _ := libraryRecord("media"); !after.Kept || after.KeptOn != before.KeptOn {
|
|
t.Fatalf("a name not read reset the record: %+v", after)
|
|
}
|
|
uuid = "00000000-1111-2222-3333-444444444444" // another pool at the same point, its root not in the group
|
|
if err := os.Chown(root, -1, os.Getgid()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(root, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, _ = applyLib(t, taken, store.State{})
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, KeepWords("media")) {
|
|
t.Fatalf("another filesystem at the root was kept on the old warrant: %+v", o)
|
|
}
|
|
}
|
|
|
|
// Old-shape parity: a controller older than ADR 0328 sends no groups, identities or libraries, and an access is
|
|
// applied exactly as before ADR 0325 — mounted when present, refused when absent, nothing kept, made or held.
|
|
func TestAnOlderControllersDeclarationIsAppliedAsBefore(t *testing.T) {
|
|
libraries(t, nil, nil)
|
|
dir := t.TempDir()
|
|
present := filepath.Join(dir, "library")
|
|
absent := filepath.Join(dir, "media", "movies")
|
|
if err := os.Mkdir(present, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := statOf(t, present)
|
|
resources := []string{
|
|
`{"id":"radarr.library","type":"access","path":"` + present + `","mode":"read-write","users":[` +
|
|
`{"identity":"mesh-radarr","uid":61001,"use":"read-write","from":"container"}],"taken":true}`,
|
|
`{"id":"plex.movies","type":"access","path":"` + absent + `"}`,
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), declare(t, strings.Join(resources, ",")), store.State{},
|
|
store.OriginDeclared, noServices, nil, nil)
|
|
if err == nil || !strings.Contains(err.Error(), absent+" is not there, and the mesh does not own it — the operator "+
|
|
"must provide it") {
|
|
t.Fatalf("an absent access is not refused as before: %v", err)
|
|
}
|
|
if o := outcomeOf(report, "radarr.library"); o.Action != "unchanged" ||
|
|
o.Detail != "operator-owned; present, read-write, nothing managed" {
|
|
t.Errorf("a present access is not as before: %+v", o)
|
|
}
|
|
if after := statOf(t, present); after.Gid != before.Gid || after.Mode != before.Mode {
|
|
t.Error("a present access was changed")
|
|
}
|
|
if n, _ := unix.Getxattr(present, "system.posix_acl_access", nil); n > 0 {
|
|
t.Error("an ACL was set on an access")
|
|
}
|
|
if _, err := os.Stat(absent); !errors.Is(err, os.ErrNotExist) {
|
|
t.Error("an absent access was made")
|
|
}
|
|
}
|
|
|
|
func jsonInto(raw string, into any) error { return json.Unmarshal([]byte(raw), into) }
|
|
|
|
// Row 4: a module that joins a library later is given its group as primary on the account the mesh made; an account
|
|
// the mesh did not make is never changed, and is refused if it is not as declared.
|
|
func TestAModuleThatJoinsALibraryLaterIsGivenItsGroup(t *testing.T) {
|
|
m := &accountsMachine{users: map[string][2]int{}, groups: map[string][2]int{}}
|
|
own := withGroups(t, `{"name":"mesh-media","gid":62000}`,
|
|
`{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":61001}`, ``)
|
|
_, known, err := Apply(context.Background(), archHost(t), own, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.ran = nil
|
|
joined := withGroups(t, `{"name":"mesh-media","gid":62000}`,
|
|
`{"module":"radarr","name":"mesh-radarr","uid":61001,"gid":62000}`, ``)
|
|
if _, known, err = Apply(context.Background(), archHost(t), joined, known, store.OriginDeclared, m.run, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(m.ran, "\n") != "usermod --gid 62000 --groups mesh-radarr" || m.users["mesh-radarr"][1] != 62000 {
|
|
t.Fatalf("the mesh's account was not given its library's group: %v", m.ran)
|
|
}
|
|
theirs := &accountsMachine{users: map[string][2]int{"mesh-radarr": {61001, 61001}},
|
|
groups: map[string][2]int{"mesh-radarr": {61001, 0}, "mesh-media": {62000, 0}}}
|
|
if _, _, err := Apply(context.Background(), archHost(t), joined, store.State{}, store.OriginDeclared, theirs.run,
|
|
nil, nil); err == nil || !strings.Contains(err.Error(), "was not made by the mesh") {
|
|
t.Fatalf("an account the mesh did not make was changed, or not refused: %v %v", err, theirs.ran)
|
|
}
|
|
}
|
|
|
|
// A library's root is never in the machine's own trees, as an access's never is (novox/hq issue 339).
|
|
func TestALibraryIsNeverTheMachinesOwnTree(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
for _, root := range []string{"/etc", "/usr/share", "/var/lib/mesh-host/x"} {
|
|
d := libraryDecl(t, libJSON("sys", root, gid, true, true))
|
|
if _, _, err := applyLib(t, d, store.State{}); err == nil {
|
|
t.Errorf("a library at %s was not refused", root)
|
|
}
|
|
if _, _, err := TakeLibrary(context.Background(), d, "library:sys", root, "w", "op"); err == nil {
|
|
t.Errorf("a library at %s was taken", root)
|
|
}
|
|
}
|
|
}
|
|
|
|
// §7: a root in shape with old data beneath it not in the group is asked about, never kept without a warrant; a root
|
|
// and sample all in shape are kept; once taken, never asked again on that filesystem.
|
|
func TestARootInShapeWithDataBelowNotInTheGroupWaits(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
movies := filepath.Join(root, "movies")
|
|
if err := os.MkdirAll(movies, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := filepath.Join(movies, "old film")
|
|
if err := os.WriteFile(old, []byte("data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range []string{root, movies} {
|
|
if err := os.Chown(p, -1, gid); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(p, 0o2775); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
d := libraryDecl(t, libJSON("media", root, gid, true, false, "movies"))
|
|
report, _, err := applyLib(t, d, store.State{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, KeepWords("media")) {
|
|
t.Fatalf("a root in shape with an old file below not in the group was kept without asking: %+v", o)
|
|
}
|
|
// The old file in the group too: the sample is in shape, and the library is kept.
|
|
if err := os.Chown(old, -1, gid); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(old, 0o664); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report, _, _ = applyLib(t, d, store.State{}); outcomeOf(report, "library:media").Action == "waiting" {
|
|
t.Fatal("a root and sample in shape were not kept")
|
|
}
|
|
// Once kept, a file an app later leaves out of the group asks nothing again on that filesystem.
|
|
if err := os.WriteFile(filepath.Join(movies, "new"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report, _, _ = applyLib(t, d, store.State{}); outcomeOf(report, "library:media").Action == "waiting" {
|
|
t.Fatal("a kept library asked again")
|
|
}
|
|
}
|
|
|
|
// Review: a file with another name that root owns is left as it is (its other name may be anywhere), and a setgid
|
|
// file never keeps its setgid bit once regrouped.
|
|
func TestRootsLinkedFilesAreLeftAndSetgidIsNeverKept(t *testing.T) {
|
|
gid := testGID(t)
|
|
f, err := os.CreateTemp(t.TempDir(), "file")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
if err := unix.Chmod(f.Name(), 0o2755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := statOf(t, f.Name())
|
|
rootLinked := st
|
|
rootLinked.Uid, rootLinked.Nlink = 0, 2
|
|
if did, err := groupVisit(gid)(int(f.Fd()), rootLinked, false); err != nil || did != visitShared {
|
|
t.Fatalf("a root-owned file with another name was not left: %d %v", did, err)
|
|
}
|
|
if after := statOf(t, f.Name()); after.Gid != st.Gid || after.Mode != st.Mode {
|
|
t.Fatal("a root-owned file with another name was changed")
|
|
}
|
|
did, err := groupVisit(gid)(int(f.Fd()), st, false)
|
|
if err != nil || did&visitStripped == 0 {
|
|
t.Fatalf("a setgid file's bit is not said taken: %d %v", did, err)
|
|
}
|
|
if after := statOf(t, f.Name()); after.Mode&(unix.S_ISGID|unix.S_ISUID) != 0 || int(after.Gid) != gid {
|
|
t.Fatalf("a setgid file kept its bit in the library's group: %o", after.Mode&0o7777)
|
|
}
|
|
}
|
|
|
|
// Review: a sub-path asked for after the library was kept without a warrant, which holds data not in the group,
|
|
// waits for the operator's word as the root would, and nothing of it is changed; an empty one is kept.
|
|
func TestASubPathHoldingOldDataWaitsForTheOperatorsWord(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.Mkdir(root, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false, "movies")), store.State{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Someone puts old data at a sub-path the library is asked for only now.
|
|
music := filepath.Join(root, "music")
|
|
if err := os.MkdirAll(filepath.Join(music, "album"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := statOf(t, music)
|
|
report, _, err := applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false, "movies", "music")), store.State{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, KeepWords("media")) {
|
|
t.Fatalf("a sub-path holding old data was kept without a warrant: %+v", o)
|
|
}
|
|
if after := statOf(t, music); after.Gid != before.Gid || after.Mode != before.Mode {
|
|
t.Fatal("a sub-path holding old data was changed")
|
|
}
|
|
// An empty one asked for is kept.
|
|
if err := os.RemoveAll(filepath.Join(music, "album")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, _ = applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false, "movies", "music")), store.State{})
|
|
if o := outcomeOf(report, "library:media"); o.Action == "waiting" {
|
|
t.Fatalf("an empty sub-path was not kept: %+v", o)
|
|
}
|
|
}
|
|
|
|
// Review: an apply does not wait for a take walk that is running: it leaves the library to it.
|
|
func TestAnApplyDoesNotWaitForATakeWalk(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.Mkdir(root, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lock := libraryLock("media")
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
libraryWalks.Store("media", "fs-test")
|
|
defer libraryWalks.Delete("media")
|
|
done := make(chan Report, 1)
|
|
go func() {
|
|
r, _, _ := applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false)), store.State{})
|
|
done <- r
|
|
}()
|
|
select {
|
|
case r := <-done:
|
|
if o := outcomeOf(r, "library:media"); !strings.Contains(o.Detail, "take act") {
|
|
t.Fatalf("the apply does not say it left the library to the walk: %+v", o)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("the apply waited for the take walk")
|
|
}
|
|
}
|
|
|
|
// Review: a root in shape whose data beneath — in no requested sub-path — is not in the group waits: the sample, not
|
|
// the root alone, says whether the data is the library's already (§7).
|
|
func TestTheSampleBeneathARootInShapeIsJudged(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "media")
|
|
if err := os.MkdirAll(filepath.Join(root, "old"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "old", "film"), []byte("data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chown(root, -1, gid); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Chmod(root, 0o2775); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, err := applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false)), store.State{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" {
|
|
t.Fatalf("a root in shape with data beneath not in the group was kept: %+v", o)
|
|
}
|
|
}
|
|
|
|
// Review: an apply that finds the library's lock held by something that is no take walk (a take act about to be
|
|
// refused) waits for it and judges the library as always: a library whose disk is not there still holds.
|
|
func TestAnApplyWaitsForALockThatIsNoWalk(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, nil, nil)
|
|
root := filepath.Join(t.TempDir(), "gone", "media")
|
|
lock := libraryLock("media")
|
|
lock.Lock()
|
|
go func() { time.Sleep(50 * time.Millisecond); lock.Unlock() }()
|
|
report, _, _ := applyLib(t, libraryDecl(t, libJSON("media", root, gid, true, false)), store.State{})
|
|
if o := outcomeOf(report, "library:media"); o.Action != "waiting" || !strings.Contains(o.Detail, DiskWords("media")) {
|
|
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+`"}`, ``)
|
|
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
|
if err != nil {
|
|
t.Errorf("%s: a refused runner failed the apply: %v", runner, err)
|
|
}
|
|
ws := RunnerWarnings()
|
|
if len(ws) != 1 || ws[0].Resource != "runner:radarr" || ws[0].Module != "radarr" ||
|
|
!strings.Contains(ws[0].Why, says) || absPath.MatchString(ws[0].Why) {
|
|
t.Errorf("%s: not said as runner:radarr saying %q, with no path: %+v", runner, says, ws)
|
|
}
|
|
if o := outcomeOf(report, "identity mesh-radarr"); o.Action != "created" {
|
|
t.Errorf("%s: the identity was not made beside the refused runner: %+v", runner, o)
|
|
}
|
|
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())
|
|
}
|
|
}
|
|
|
|
// Review: a library's runner that is refused keeps the library's outcome, is said as runner:library:<name>, and never
|
|
// fails the apply; a runner that leaves a group restarts the tool runner once, in the apply it leaves.
|
|
func TestARefusedLibraryRunnerIsAWarningAndLeavingRestarts(t *testing.T) {
|
|
gid := testGID(t)
|
|
libraries(t, []string{"/srv/media"}, nil)
|
|
m := &accountsMachine{users: map[string][2]int{"nox": {1003, 1003}}, groups: map[string][2]int{"nox": {1003, 0}}}
|
|
lib := func(runner string) string {
|
|
return `{"library":"media","root":"/srv/media","group":"mesh-media","gid":` + strconv.Itoa(gid) +
|
|
`,"home":false,"runner":"` + runner + `"}`
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), libraryDecl(t, lib("ghost")), store.State{},
|
|
store.OriginDeclared, m.run, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("a refused library runner failed the apply: %v", err)
|
|
}
|
|
if o := outcomeOf(report, "library:media"); o.ID == "" {
|
|
t.Fatal("the library's outcome was dropped")
|
|
}
|
|
if ws := RunnerWarnings(); len(ws) != 1 || ws[0].Resource != "runner:library:media" {
|
|
t.Fatalf("not said as runner:library:media: %+v", ws)
|
|
}
|
|
|
|
// Joins, then leaves: the tool runner's unit, naming library:media, is restarted on each.
|
|
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...)
|
|
}
|
|
unit := `{"id":"node-tools.unit","type":"service","unit":"node-tools.service","state":"running",` +
|
|
`"restart-on":["library:media"]}`
|
|
_, known, err := Apply(context.Background(), archHost(t), libraryDecl(t, lib("nox"), unit), store.State{},
|
|
store.OriginDeclared, run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(RunnerWarnings()) != 0 {
|
|
t.Error("a warning stayed once the runner was right")
|
|
}
|
|
verbs = nil
|
|
gone := `{"library":"media","root":"/srv/media","group":"mesh-media","gid":` + strconv.Itoa(gid) + `,"home":false}`
|
|
if _, _, err := Apply(context.Background(), archHost(t), libraryDecl(t, gone, unit), known, store.OriginDeclared, run,
|
|
nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(m.ran, "\n"), "gpasswd --delete nox mesh-media") ||
|
|
strings.Count(strings.Join(verbs, " "), "stop") != 1 {
|
|
t.Fatalf("leaving the group did not restart the tool runner once: %v %v", verbs, m.ran)
|
|
}
|
|
}
|
|
|
|
// ADR 0334 §1: a module's own directory may carry the setgid bit, so what is made beneath it keeps its own group;
|
|
// it is set, held across applies, put back and said when it is taken off. Setuid and sticky are refused.
|
|
func TestADirectorysSetgidBitIsKept(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "state")
|
|
d := declare(t, `{"id":"radarr.state","type":"directory","path":"`+dir+`","mode":"2750"}`)
|
|
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil,
|
|
nil); err != nil {
|
|
t.Fatalf("a directory with setgid was refused: %v", err)
|
|
}
|
|
if st := statOf(t, dir); st.Mode&0o7777 != 0o2750 {
|
|
t.Fatalf("made at %o, and 2750 was declared", st.Mode&0o7777)
|
|
}
|
|
_, known, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := statOf(t, dir); st.Mode&0o7777 != 0o2750 {
|
|
t.Fatalf("the setgid bit did not hold across applies: %o", st.Mode&0o7777)
|
|
}
|
|
if err := unix.Chmod(dir, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "radarr.state"); o.Action != "updated" || !strings.Contains(o.Detail, "0750 to 2750") {
|
|
t.Fatalf("the setgid bit taken off was not put back and said: %+v", o)
|
|
}
|
|
if st := statOf(t, dir); st.Mode&0o7777 != 0o2750 {
|
|
t.Fatalf("the setgid bit was not put back: %o", st.Mode&0o7777)
|
|
}
|
|
for _, mode := range []string{"4750", "1750", "6750"} {
|
|
if _, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"m.d","type":"directory","path":"` +
|
|
dir + `","mode":"` + mode + `"}]}`)); err == nil {
|
|
t.Errorf("a directory mode %s was accepted", mode)
|
|
}
|
|
}
|
|
}
|