mesh/merge-gate head c85c2804a010 merged into trunk dd9ab7927d96 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c85c2804a010 merged into trunk dd9ab7927d96 — fail: its merge-check.sh failed: --- FAIL: TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWait…
mesh/delivery superseded: a newer head of the same pull request
The engine delivers before the controller half, and today's controller sends no identities, users or taken. Keeping now starts only for an access whose folder has users, or a declaration with identities; every other access is mounted when present and refused when absent as on main, with nothing made, kept, held or judged. A leftover whose walk could not run says why, not always that its disk is away.
135 lines
4.9 KiB
Go
135 lines
4.9 KiB
Go
//go:build linux
|
|
|
|
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// The engine delivers before the controller half (novox/hq ADR 0325): a controller that does not speak
|
|
// link.IdentityContract sends no identities, no users and no taken, and every access of its declarations is applied
|
|
// exactly as before this engine — mounted when present, refused when absent (ADR 0051 §7), nothing kept, nothing
|
|
// made, no container held, nothing judged. These are origin/main's outcomes, word for word.
|
|
func TestAnOlderControllersAccessesAreAppliedExactlyAsBefore(t *testing.T) {
|
|
keeping(t, nil, nil)
|
|
dir := t.TempDir()
|
|
unwritable := filepath.Join(dir, "library") // its group may not write it
|
|
empty := filepath.Join(dir, "spool")
|
|
absent := filepath.Join(dir, "media", "movies")
|
|
for _, d := range []string{unwritable, empty} {
|
|
if err := os.Mkdir(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// A root-owned empty folder, read only.
|
|
rootEmpty := ""
|
|
for _, c := range []string{"/var/empty", "/srv/http", "/srv/ftp"} {
|
|
if entries, err := os.ReadDir(c); err == nil && len(entries) == 0 {
|
|
if uid, _, _ := statOwner(mustLstat(t, c)); uid == 0 {
|
|
rootEmpty = c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
resources := []string{
|
|
`{"id":"radarr.library","type":"access","path":"` + unwritable + `","mode":"read-write"}`,
|
|
`{"id":"sab.spool","type":"access","path":"` + empty + `","mode":"read-write"}`,
|
|
`{"id":"plex.movies","type":"access","path":"` + absent + `"}`,
|
|
`{"id":"plex.app","type":"container","name":"plex","image":"` + pinned + `","volumes":["` + absent + `:/movies"]}`,
|
|
}
|
|
if rootEmpty != "" {
|
|
resources = append(resources, `{"id":"web.root","type":"access","path":"`+rootEmpty+`","mode":"read-write"}`)
|
|
}
|
|
var started bool
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if len(args) > 0 && args[0] == "run" {
|
|
started = true
|
|
}
|
|
if len(args) > 0 && args[0] == "container" {
|
|
if started {
|
|
return "true\t" + containerSpec(declare(t, resources[3]).Resources[0].(*declaration.Container), inputs{}), nil
|
|
}
|
|
return "", errors.New("no such container")
|
|
}
|
|
return "", nil
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), declare(t, strings.Join(resources, ",")), store.State{},
|
|
store.OriginDeclared, run, 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)
|
|
}
|
|
for id, mode := range map[string]string{"radarr.library": "read-write", "sab.spool": "read-write"} {
|
|
o := outcomeOf(report, id)
|
|
if o.Action != "unchanged" || o.Detail != "operator-owned; present, "+mode+", nothing managed" {
|
|
t.Errorf("%s: %+v, and before it was unchanged, %q", id, o, "operator-owned; present, "+mode+", nothing managed")
|
|
}
|
|
}
|
|
if rootEmpty != "" {
|
|
if o := outcomeOf(report, "web.root"); o.Action != "unchanged" {
|
|
t.Errorf("a root-owned folder was judged: %+v", o)
|
|
}
|
|
}
|
|
if !started {
|
|
t.Error("a container mounting an absent access was held, and before it was created")
|
|
}
|
|
if _, err := os.Stat(absent); !errors.Is(err, os.ErrNotExist) {
|
|
t.Error("an absent access was made")
|
|
}
|
|
for _, d := range []string{unwritable, empty} {
|
|
if n, _ := unix.Getxattr(d, aclAccessXattr, nil); n > 0 {
|
|
t.Errorf("an ACL was set on %s", d)
|
|
}
|
|
if info := mustLstat(t, d); info.Mode().Perm() != 0o755 {
|
|
t.Errorf("%s changed mode: %o", d, info.Mode().Perm())
|
|
}
|
|
}
|
|
if all, _ := KeptFolders.All(); len(all) != 0 {
|
|
t.Errorf("folders were recorded as kept: %v", all)
|
|
}
|
|
if len(Leftovers()) != 0 || len(WaitingOnAccess(declare(t, strings.Join(resources, ",")))) != 0 {
|
|
t.Error("something was said or held for an older controller's access")
|
|
}
|
|
}
|
|
|
|
func mustLstat(t *testing.T, path string) os.FileInfo {
|
|
t.Helper()
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return info
|
|
}
|
|
|
|
// Review (low): a folder no access names, whose filesystem could not be named this look, is not said as its disk
|
|
// being away.
|
|
func TestALeftoverSaysWhyItsWalkCouldNotRun(t *testing.T) {
|
|
uuid, fsid, source := "6f1c0a4e-1b2d-4c3e-8f00-aa11bb22cc33", "0000080100000000", "/dev/sdb1"
|
|
_, lib, access := poolNamed(t, &uuid, &fsid, &source)
|
|
takeIt(t, lib, access)
|
|
other := `{"id":"x.dir","type":"directory","path":"` + filepath.Join(filepath.Dir(filepath.Dir(lib)), "x") + `"}`
|
|
if _, _, err := applyAccesses(t, store.State{}, other); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uuid, fsid, source = "", "0000000000000000", "" // nothing names it now
|
|
ReconcileKept(context.Background())
|
|
left := Leftovers()
|
|
if len(left) != 1 || strings.Contains(left[0].Why, "disk") || !strings.Contains(left[0].Why, "could not be named") {
|
|
t.Fatalf("the leftover does not say why its walk could not run: %+v", left)
|
|
}
|
|
}
|