package catalogue import ( "encoding/json" "fmt" "reflect" "strings" "testing" "github.com/novox/mesh-host/validate" ) // 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:50020" { t.Errorf("its directory is owned by %v; want its user and own group (ADR 0334 §1)", owner) } if len(composed.Identities) != 1 || !reflect.DeepEqual(composed.Identities[0], IdentityEntry{Module: "radarr", Name: "mesh-radarr", UID: 50020, GID: 50010, Groups: []int{50011}}) { 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 14: a module that neither declares libraries nor uses its identity is composed exactly as before — the whole // declaration: its resources, the filter its port reaches, the contributions it makes — on an engine that keeps // libraries or not; on a machine a library reaches, only that library's group is added. A joining module on an older // engine is left out, naming why. func TestAModuleThatDoesNotJoinIsComposedAsBefore(t *testing.T) { plain := aPlain() plain.Listens = []Listening{{Port: 8080, From: FromEverywhere, Name: "web", Why: "its page"}} plain.Contributes = map[string]map[string]any{"reverse-proxy": {"host": "plain", "port": 8080}} plain.Requires = []string{"reverse-proxy"} modules := []Manifest{plain, proxy(), {Module: "nftables", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}}, theRuntime(t)} base := Rendering{Mesh: []string{"198.51.100.2"}, OutwardLinks: []string{"eth0"}, TunnelInterface: "mesh0"} keeping := func(node string) Rendering { with := librariesRendering() with.Mesh, with.OutwardLinks, with.TunnelInterface = base.Mesh, base.OutwardLinks, base.TunnelInterface return with } for _, node := range []string{"far", "ace"} { r := Resolution{Node: node, Account: "jochen", Modules: modules} before := composeLibraries(t, r, base) after := composeLibraries(t, r, keeping(node)) if node == "far" { if !reflect.DeepEqual(before, after) { t.Fatalf("on a machine no library reaches, a module that does not join composed otherwise:\n %+v\n %+v", before, after) } continue } groups := after.Groups after.Groups, after.Libraries = nil, nil if !reflect.DeepEqual(before, after) { t.Fatalf("on %s, a module that does not join composed otherwise:\n %+v\n %+v", node, before, after) } if len(groups) == 0 || len(after.Identities) > 0 { t.Fatalf("on the library's home: groups %v, identities %v", groups, after.Identities) } filter := fmt.Sprint(byID(before.Resources)["nftables."+FilteringID()]["content"]) if !strings.Contains(filter, "8080") || received(t, before.Resources)[0].From != "plain" { t.Fatalf("the comparison held no filter or contribution of the module: %s", filter) } } 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:} 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") } } // What the node-engine this controller pins (mesh-host #95, ADR 0328's half) makes of a composed declaration with // libraries and a joining module: nothing refused. func TestTheEngineTakesWhatIsComposedForLibraries(t *testing.T) { for _, node := range []string{"ace", "novox"} { r := Resolution{Node: node, Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}} with := librariesRendering() with.Libraries["books"] = Library{Name: "books", Home: "ace", Root: "/storage/books", Reach: []string{"novox"}} composed := composeLibraries(t, r, with) body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": composed.Resources, "groups": composed.Groups, "identities": composed.Identities, "libraries": composed.Libraries}) if err != nil { t.Fatal(err) } if problems := validate.Declaration(body); len(problems) > 0 { t.Fatalf("%s: the pinned node-engine refuses it: %v", node, problems) } } } // novox/hq ADR 0334 §1, §2 (the review of 7ea6d36e, item 2): a joining module's own directories are owned by its user // and its own group, not the library's; and the tool runner's account joins the own group of every joining module whose // tools run on the machine — sent only to an engine that reads it (link.OwnGroupContract). func TestAJoiningModulesOwnDirectoriesAreItsOwnGroups(t *testing.T) { r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aRadarr(t), theRuntime(t)}} got := byID(composeLibraries(t, r, librariesRendering()).Resources) if owner := got["radarr.config"]["owner"]; owner != "50020:50020" { t.Errorf("its directory is owned by %v; want its user and its own group", owner) } if env := got["radarr.app"]["env"].(map[string]any); env["PGID"] != "50010" { t.Errorf("its process's primary group is %v; want the library's, so what it makes there keeps the group", env["PGID"]) } older := composeLibraries(t, r, librariesRendering()) if older.Identities[0].Runner != "" { t.Errorf("an engine that cannot put the runner in a module's own group was told %q", older.Identities[0].Runner) } with := librariesRendering() with.RunnerJoinsOwnGroups = true if got := composeLibraries(t, r, with).Identities[0].Runner; got != "jochen" { t.Errorf("the runner joins %q's own group; want jochen, whose tools it runs", got) } } // novox/hq ADR 0334 §3: a joining module whose tools name a directory whose mode closes it to its group is refused at // the module check, naming the directory. func TestAToolsDirectoryIsOpenToItsGroup(t *testing.T) { closed := map[string]any{ "libraries": []any{map[string]any{"id": "movies", "library": "media", "path": "movies", "use": "read"}}, "resources": []any{ map[string]any{"id": "config", "type": "directory", "mode": "0700"}, map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage, "volumes": []any{"${dir:config}:/config", "${library:movies}:/movies"}}}, "build": map[string]any{"artifacts": []any{map[string]any{"name": "tools", "kind": "bundle", "language": "go", "system": "arch", "from": "cmd/app", "binary": "app", "loads": []any{"app"}, "env": map[string]any{"APP_CONFIG": "${dir:config}/config.xml"}}}}} _, err := ParseManifest(libraryManifest(t, closed)) if err == nil || !strings.Contains(err.Error(), "config") || !strings.Contains(err.Error(), "ADR 0334") { t.Fatalf("a tools' directory closed to its group: %v", err) } open := closed open["resources"] = []any{ map[string]any{"id": "config", "type": "directory", "mode": "0750"}, map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage, "volumes": []any{"${dir:config}:/config", "${library:movies}:/movies"}}} if _, err := ParseManifest(libraryManifest(t, open)); err != nil { t.Fatalf("a tools' directory open to its group was refused: %v", err) } } // The review of 7ea6d36e, item 6: a service's unit takes the identity too — `User=${identity:user}` — at the module // check and in what is composed. func TestAServicesUnitTakesTheIdentity(t *testing.T) { unit := "[Service]\nUser=${identity:user}\nGroup=${identity:group}\nUMask=0002\nExecStart=/usr/bin/importer\n" raw := libraryManifest(t, map[string]any{"resources": []any{ map[string]any{"id": "app", "type": "container", "name": "app", "image": libraryImage, "volumes": []any{"${library:movies}:/movies"}}, map[string]any{"id": "watch", "type": "service", "name": "app-watch", "unit": unit}}}) m, err := ParseManifest(raw) if err != nil { t.Fatalf("a unit naming the identity was refused: %v", err) } with := librariesRendering() with.Identities["app"] = ModuleIdentity{Module: "app", UID: 50030} composed := composeLibraries(t, Resolution{Node: "ace", Modules: []Manifest{m}}, with) got := fmt.Sprint(byID(composed.Resources)["app.watch"]["unit"]) if !strings.Contains(got, "User=mesh-app\nGroup=mesh-media\n") { t.Fatalf("the unit reads %q", got) } }