320 lines
16 KiB
Go
320 lines
16 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0328 on the controller's side, row by row of its "How it is checked" table where the row is the
|
|
// controller's: libraries defined (1, 13), entries resolved (2), groups and numbers (3, 4, 15), the identity and library
|
|
// placeholders (5), a read use mounted read-only (10), the tool runner in a library's group (11), a joining module held
|
|
// until its libraries are taken (8), and a module that does not join composed exactly as before (14).
|
|
|
|
const libraryImage = "x@sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
|
|
|
var mediaLibrary = Library{Name: "media", Home: "ace", Root: "/storage/media", Reach: []string{"novox"}}
|
|
|
|
// aRadarr writes movies and reads books, tells its image its identity by PUID and PGID, and has tools on the runner.
|
|
func aRadarr(t *testing.T) Manifest {
|
|
t.Helper()
|
|
m := aToolsModule(t, "radarr", "tools/index.js")
|
|
m.Libraries = []LibraryUse{
|
|
{ID: "movies", Library: "media", Path: "movies", Use: AccessReadWrite},
|
|
{ID: "books", Library: "books", Path: "shelf", Use: AccessRead},
|
|
}
|
|
m.Resources = []map[string]any{
|
|
{"id": "config", "type": "directory", "mode": "0750"},
|
|
{"id": "env", "type": "file", "path": "${dir:config}/identity.env", "mode": "0644",
|
|
"content": "PUID=${identity:uid}\nPGID=${identity:gid}\nUSER=${identity:user}\nGROUP=${identity:group}\nUMASK=002\n"},
|
|
{"id": "app", "type": "container", "name": "radarr", "image": libraryImage,
|
|
"env": map[string]any{"PUID": "${identity:uid}", "PGID": "${identity:gid}"},
|
|
"volumes": []any{"${dir:config}:/config", "${library:movies}:/movies", "${library:books}:/books:z"}},
|
|
}
|
|
return m
|
|
}
|
|
|
|
// aPlain is a module that neither declares libraries nor uses its identity.
|
|
func aPlain() Manifest {
|
|
return Manifest{Module: "plain", Version: "1", Resources: []map[string]any{
|
|
{"id": "data", "type": "directory", "mode": "0700", "owner": "999:999"},
|
|
{"id": "app", "type": "container", "name": "plain", "image": libraryImage, "volumes": []any{"${dir:data}:/data"}},
|
|
}}
|
|
}
|
|
|
|
func librariesRendering() Rendering {
|
|
books := Library{Name: "books", Home: "ace", Root: "/storage/books"}
|
|
return Rendering{KeepsLibraries: true,
|
|
Libraries: map[string]Library{"media": mediaLibrary, "books": books},
|
|
LibraryGroups: map[string]int{"media": 50010, "books": 50011},
|
|
Identities: map[string]ModuleIdentity{"radarr": {Module: "radarr", UID: 50020}},
|
|
TakenLibraries: map[string]bool{"media": true, "books": true},
|
|
LibraryPaths: map[string][]string{"media": {"movies", "series"}},
|
|
}
|
|
}
|
|
|
|
func composeLibraries(t *testing.T, r Resolution, with Rendering) Composed {
|
|
t.Helper()
|
|
with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}
|
|
with.ArtifactStore = "anchor.internal:5101"
|
|
composed, err := r.Compose(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return composed
|
|
}
|
|
|
|
// Rows 2, 4, 5, 10: a joining module's identity is filled everywhere, its library entries resolve to root + path in its
|
|
// volume sources, a read entry mounts read-only, its directories are owned by its user and primary group — the first
|
|
// library it uses — and its other libraries are added groups.
|
|
func TestAJoiningModuleIsComposedWithItsIdentityAndLibraries(t *testing.T) {
|
|
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}}
|
|
composed := composeLibraries(t, r, librariesRendering())
|
|
got := byID(composed.Resources)
|
|
|
|
app := got["radarr.app"]
|
|
if fmt.Sprint(app["env"]) != fmt.Sprint(map[string]any{"PUID": "50020", "PGID": "50010"}) {
|
|
t.Errorf("the container's environment is %v", app["env"])
|
|
}
|
|
want := []any{"/var/lib/radarr/config:/config", "/storage/media/movies:/movies", "/storage/books/shelf:/books:ro,z"}
|
|
if !reflect.DeepEqual(app["volumes"], want) {
|
|
t.Errorf("the volumes are %v; want %v", app["volumes"], want)
|
|
}
|
|
if content := fmt.Sprint(got["radarr.env"]["content"]); content !=
|
|
"PUID=50020\nPGID=50010\nUSER=mesh-radarr\nGROUP=mesh-media\nUMASK=002\n" {
|
|
t.Errorf("the identity file reads %q", content)
|
|
}
|
|
if owner := got["radarr.config"]["owner"]; owner != "50020:50010" {
|
|
t.Errorf("its directory is owned by %v; want its user and primary group", owner)
|
|
}
|
|
if len(composed.Identities) != 1 || !reflect.DeepEqual(composed.Identities[0], IdentityEntry{Module: "radarr",
|
|
Name: "mesh-radarr", UID: 50020, GID: 50020, Groups: []string{"mesh-media", "mesh-books"}}) {
|
|
t.Errorf("the identities are %+v", composed.Identities)
|
|
}
|
|
if w := LibraryWarnings(aRadarr(t)); len(w) != 1 || !strings.Contains(w[0], "reaches only media") {
|
|
t.Errorf("a module of two libraries is warned %q", w)
|
|
}
|
|
}
|
|
|
|
// Rows 3, 7, 11: every library reaching the machine has its group, with its number; each library used here or homed
|
|
// here is named with its root, whether this is its home, whether it is taken, the sub-paths kept on its home, and the
|
|
// tool runner's account where a module whose tools run there uses it.
|
|
func TestTheDeclarationNamesTheLibrariesAndTheirGroups(t *testing.T) {
|
|
with := librariesRendering()
|
|
with.TakenLibraries = map[string]bool{"media": false, "books": true}
|
|
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}}
|
|
composed := composeLibraries(t, r, with)
|
|
raw, _ := json.Marshal(map[string]any{"groups": composed.Groups, "libraries": composed.Libraries})
|
|
want := `{"groups":[{"name":"mesh-books","gid":50011},{"name":"mesh-media","gid":50010}],` +
|
|
`"libraries":[{"library":"books","root":"/storage/books","group":"mesh-books","gid":50011,"home":true,` +
|
|
`"taken":true,"paths":["shelf"],"runner":"jochen"},` +
|
|
`{"library":"media","root":"/storage/media","group":"mesh-media","gid":50010,"home":true,"taken":false,` +
|
|
`"paths":["movies","series"],"runner":"jochen"}]}`
|
|
if string(raw) != want {
|
|
t.Errorf("the declaration says\n %s\nwant\n %s", raw, want)
|
|
}
|
|
// On a machine it reaches but is not home: the group, the library as used there, and no sub-path from elsewhere.
|
|
elsewhere := composeLibraries(t, Resolution{Node: "novox", Account: "jochen",
|
|
Modules: []Manifest{aPlain(), theRuntime(t)}}, with)
|
|
raw, _ = json.Marshal(map[string]any{"groups": elsewhere.Groups, "libraries": elsewhere.Libraries})
|
|
if string(raw) != `{"groups":[{"name":"mesh-media","gid":50010}],"libraries":null}` {
|
|
t.Errorf("on a machine it only reaches: %s", raw)
|
|
}
|
|
}
|
|
|
|
// Row 2: a module assigned to a machine its library does not reach is left out there, naming the library; one whose
|
|
// library is not defined likewise.
|
|
func TestALibraryNotReachingTheMachineLeavesItsModuleOut(t *testing.T) {
|
|
with := librariesRendering()
|
|
with.Identities["radarr"] = ModuleIdentity{Module: "radarr", UID: 50020}
|
|
composed := composeLibraries(t, Resolution{Node: "far", Account: "jochen",
|
|
Modules: []Manifest{aRadarr(t), theRuntime(t)}}, with)
|
|
if why := composed.LeftOut["radarr"]; !strings.Contains(why, "does not reach far") {
|
|
t.Errorf("radarr on a machine media does not reach: %q", why)
|
|
}
|
|
delete(with.Libraries, "books")
|
|
composed = composeLibraries(t, Resolution{Node: "ace", Account: "jochen",
|
|
Modules: []Manifest{aRadarr(t), theRuntime(t)}}, with)
|
|
if why := composed.LeftOut["radarr"]; !strings.Contains(why, "books, which is not defined") {
|
|
t.Errorf("radarr with an undefined library: %q", why)
|
|
}
|
|
}
|
|
|
|
// Row 8: a joining module that ran here before keeps the user it ran as until its libraries are taken — left out, its
|
|
// containers untouched — while its libraries are still named, so the engine states the wait and the take can come.
|
|
func TestAJoiningModuleIsHeldUntilItsLibrariesAreTaken(t *testing.T) {
|
|
with := librariesRendering()
|
|
with.TakenLibraries["media"] = false
|
|
with.Held = map[string]string{"radarr": "media"}
|
|
composed := composeLibraries(t, Resolution{Node: "ace", Account: "jochen",
|
|
Modules: []Manifest{aRadarr(t), theRuntime(t)}}, with)
|
|
if why := composed.LeftOut["radarr"]; !strings.Contains(why, "keeps the user it ran as until the library media is taken") {
|
|
t.Fatalf("radarr while media is not taken: %q", why)
|
|
}
|
|
if byID(composed.Resources)["radarr.app"] != nil {
|
|
t.Fatal("a held module's container was sent")
|
|
}
|
|
found := false
|
|
for _, l := range composed.Libraries {
|
|
found = found || (l.Library == "media" && !l.Taken)
|
|
}
|
|
if !found {
|
|
t.Fatalf("the library a held module waits on is not named: %+v", composed.Libraries)
|
|
}
|
|
}
|
|
|
|
// Row 14: a module that neither declares libraries nor uses its identity is composed exactly as before, on an engine
|
|
// that keeps libraries or not; and a joining module on an older engine is left out, naming why.
|
|
func TestAModuleThatDoesNotJoinIsComposedAsBefore(t *testing.T) {
|
|
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aPlain(), theRuntime(t)}}
|
|
before := composeLibraries(t, r, Rendering{})
|
|
after := composeLibraries(t, r, librariesRendering())
|
|
if !reflect.DeepEqual(before.Resources, after.Resources) {
|
|
t.Fatalf("a module that does not join composed otherwise:\n %v\n %v", before.Resources, after.Resources)
|
|
}
|
|
if Joins(aPlain()) || !Joins(aRadarr(t)) {
|
|
t.Fatal("joining is not read from the libraries and the identity")
|
|
}
|
|
older := librariesRendering()
|
|
older.KeepsLibraries = false
|
|
composed := composeLibraries(t, Resolution{Node: "ace", Account: "jochen",
|
|
Modules: []Manifest{aRadarr(t), aPlain(), theRuntime(t)}}, older)
|
|
if why := composed.LeftOut["radarr"]; !strings.Contains(why, "older than them") {
|
|
t.Fatalf("a joining module on an older engine: %q", why)
|
|
}
|
|
if len(composed.Groups)+len(composed.Identities)+len(composed.Libraries) > 0 {
|
|
t.Fatal("an older engine was sent groups, identities or libraries")
|
|
}
|
|
if byID(composed.Resources)["plain.app"] == nil {
|
|
t.Fatal("a module that does not join was left out beside one that does")
|
|
}
|
|
}
|
|
|
|
// Row 15: no number outside the range is sent, root's above all; the composition refuses, naming why.
|
|
func TestNoNumberTheMeshGivesIsRoot(t *testing.T) {
|
|
for _, n := range []int{0, 4, 10, IdentityLow - 1, IdentityHigh + 1} {
|
|
if AllocatableID(n) {
|
|
t.Errorf("%d is allocatable", n)
|
|
}
|
|
with := librariesRendering()
|
|
with.Identities["radarr"] = ModuleIdentity{Module: "radarr", UID: n}
|
|
with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}
|
|
with.ArtifactStore = "anchor.internal:5101"
|
|
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}}
|
|
if _, err := r.Compose(with); err == nil || !strings.Contains(err.Error(), "§15") {
|
|
t.Errorf("a module numbered %d composed: %v", n, err)
|
|
}
|
|
with = librariesRendering()
|
|
with.LibraryGroups["media"] = n
|
|
with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}
|
|
with.ArtifactStore = "anchor.internal:5101"
|
|
if _, err := r.Compose(with); err == nil || !strings.Contains(err.Error(), "§15") {
|
|
t.Errorf("a library group numbered %d composed: %v", n, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func libraryManifest(t *testing.T, extra map[string]any) []byte {
|
|
t.Helper()
|
|
m := map[string]any{"module": "app", "version": "1", "capabilities": []any{"container-runtime"},
|
|
"libraries": []any{map[string]any{"id": "movies", "library": "media", "path": "movies", "use": "read-write"}},
|
|
"resources": []any{map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage,
|
|
"volumes": []any{"${library:movies}:/movies"}}}}
|
|
for k, v := range extra {
|
|
m[k] = v
|
|
}
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// Rows 2 and 5 at the module check: library entries are accepted by name, sub-path and use, and refused with a host
|
|
// path, an owner, a group, a mode or a number; ${library:<id>} only in a volume source; ${identity:…} only by its four
|
|
// names; a joining module names no directory owner and declares no number setting.
|
|
func TestTheModuleCheckHoldsLibrariesAndIdentityToTheDecision(t *testing.T) {
|
|
if _, err := ParseManifest(libraryManifest(t, nil)); err != nil {
|
|
t.Fatalf("a library entry was refused: %v", err)
|
|
}
|
|
entry := func(extra map[string]any) map[string]any {
|
|
e := map[string]any{"id": "movies", "library": "media", "path": "movies", "use": "read-write"}
|
|
for k, v := range extra {
|
|
e[k] = v
|
|
}
|
|
return map[string]any{"libraries": []any{e}}
|
|
}
|
|
for name, c := range map[string]struct {
|
|
extra map[string]any
|
|
want string
|
|
}{
|
|
"a host path": {entry(map[string]any{"path": "/storage/media/movies"}), "never a host path"},
|
|
"out of it": {entry(map[string]any{"path": "../other"}), "never a host path"},
|
|
"an owner": {entry(map[string]any{"owner": "1000"}), "owner"},
|
|
"a group": {entry(map[string]any{"group": "media"}), "group"},
|
|
"a mode": {entry(map[string]any{"mode": "0775"}), "mode"},
|
|
"a number": {entry(map[string]any{"gid": 1000}), "gid"},
|
|
"a use": {entry(map[string]any{"use": "write"}), "is used \"write\""},
|
|
"in the env": {map[string]any{"resources": []any{map[string]any{"id": "app", "type": "container",
|
|
"name": "app", "image": libraryImage, "env": map[string]any{"MOVIES": "${library:movies}"}}}}, "volume source"},
|
|
"in a file": {map[string]any{"resources": []any{map[string]any{"id": "f", "type": "file", "path": "/etc/app.conf",
|
|
"mode": "0644", "content": "movies=${library:movies}"}}}, "volume source"},
|
|
"on the container side": {map[string]any{"resources": []any{map[string]any{"id": "app", "type": "container",
|
|
"name": "app", "image": libraryImage, "volumes": []any{"/srv:${library:movies}"}}}}, "volume source"},
|
|
"an unknown identity": {map[string]any{"resources": []any{map[string]any{"id": "app", "type": "container",
|
|
"name": "app", "image": libraryImage, "env": map[string]any{"HOME": "${identity:home}"}}}}, "${identity:home}"},
|
|
"a directory owner": {map[string]any{"resources": []any{map[string]any{"id": "d", "type": "directory",
|
|
"mode": "0750", "owner": "1000:1000"}}}, "names its owner"},
|
|
} {
|
|
_, err := ParseManifest(libraryManifest(t, c.extra))
|
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v; want refused saying %q", name, err, c.want)
|
|
}
|
|
}
|
|
// A module that does not join keeps its directory owner and its settings as they are (§14).
|
|
plain := map[string]any{"module": "plain", "version": "1",
|
|
"resources": []any{map[string]any{"id": "d", "type": "directory", "mode": "0750", "owner": "1000:1000"}}}
|
|
raw, _ := json.Marshal(plain)
|
|
if _, err := ParseManifest(raw); err != nil {
|
|
t.Errorf("a module that does not join was refused: %v", err)
|
|
}
|
|
// A setting carrying a number, or a placement's owner, is refused for a joining module, and kept for one that does
|
|
// not.
|
|
if err := JoiningSettingProblem(aRadarr(t), []Layer{{From: "ace", Values: map[string]any{"puid": "1000"}}}); err == nil ||
|
|
!strings.Contains(err.Error(), "carries a user or group number") {
|
|
t.Errorf("a joining module's puid setting: %v", err)
|
|
}
|
|
if err := JoiningSettingProblem(aPlain(), []Layer{{From: "ace", Values: map[string]any{"puid": "1000"}}}); err != nil {
|
|
t.Errorf("a plain module's puid setting: %v", err)
|
|
}
|
|
placed := []Layer{{From: "ace", Values: map[string]any{PlacesSetting: map[string]any{
|
|
"config": map[string]any{"path": "/srv/x", "owner": "1000:1000"}}}}}
|
|
if err := PlacesOwnerProblem(aRadarr(t), placed); err == nil || !strings.Contains(err.Error(), "ADR 0328") {
|
|
t.Errorf("a joining module's placed owner: %v", err)
|
|
}
|
|
if err := PlacesOwnerProblem(aPlain(), placed); err != nil {
|
|
t.Errorf("a plain module's placed owner: %v", err)
|
|
}
|
|
}
|
|
|
|
// Row 1: the libraries are the controller's own mesh-wide setting, the terminal's or the operator's Approve alone.
|
|
func TestTheLibrariesAreTheOperatorsSetting(t *testing.T) {
|
|
controller := Manifest{Module: LibrariesModule, Version: "1"}
|
|
keys := TerminalKeys(controller)
|
|
if !hasName(keys, LibrariesSetting) {
|
|
t.Fatalf("the libraries are not among the terminal's keys: %v", keys)
|
|
}
|
|
if hasName(TerminalKeys(Manifest{Module: "radarr"}), LibrariesSetting) {
|
|
t.Fatal("another module's libraries key is the terminal's")
|
|
}
|
|
if err := ComposesWith(controller, []Layer{{From: "ace", Values: map[string]any{LibrariesSetting: map[string]any{}}}},
|
|
false); err == nil {
|
|
t.Fatal("libraries set on one machine were accepted")
|
|
}
|
|
if _, err := ParseLibraries(map[string]any{"media": map[string]any{"home": "ace", "root": "/etc/media"}}); err == nil {
|
|
t.Fatal("a library in the machine's own tree was accepted")
|
|
}
|
|
}
|