Files
mesh-controller/internal/inventory/identities_test.go
T
jschoubben 63602264ef Give every module an identity and ask once to keep a shared folder (hq ADR 0325, issue 575)
A module's containers that declare identity run as an identity the controller allocates once, mesh-wide,
from 50000-59999; each access names its users and whether its folder is taken; an access wait is excused
for a folder the mesh may not keep yet, and the operator is asked once per folder and machine, the folder
taken only on the warrant. All of it is sent only to an engine at contract 5. A placement for an access a
newer build brings is judged against that build (issue 575). mesh-host pinned to the ADR 0325 branch head.
2026-10-12 19:05:13 +02:00

159 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package inventory
import (
"encoding/json"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// novox/hq ADR 0325 §3, §11: a module gets one allocation, the same whoever asks and however often, from the range
// of its own; no allocation is 0 or a root group's; a number once given is never given again.
func TestAModuleIsGivenOneIdentityOnceAndNeverRoot(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
radarr, err := inv.AllocateIdentity(ctx, "radarr")
if err != nil {
t.Fatal(err)
}
again, err := inv.AllocateIdentity(ctx, "radarr")
if err != nil {
t.Fatal(err)
}
if radarr != again {
t.Fatalf("radarr was given %+v, then %+v: one allocation per module", radarr, again)
}
plex, err := inv.AllocateIdentity(ctx, "plex")
if err != nil {
t.Fatal(err)
}
for _, id := range []catalogue.ModuleIdentity{radarr, plex} {
if id.UID == 0 || id.GID == 0 || !catalogue.AllocatableID(id.UID) || !catalogue.AllocatableID(id.GID) ||
id.UID != id.GID {
t.Errorf("%s was given %d:%d: within %d–%d, never 0, uid and gid one number", id.Module, id.UID, id.GID,
catalogue.IdentityLow, catalogue.IdentityHigh)
}
}
if radarr.UID == plex.UID {
t.Fatalf("radarr and plex share %d", plex.UID)
}
// Read back as composition reads it, from another connection's view: the same numbers (stable across restarts
// and machines, since every machine's composition reads this one row).
all, err := inv.Identities(ctx)
if err != nil {
t.Fatal(err)
}
if all["radarr"].UID != radarr.UID || all["plex"].UID != plex.UID {
t.Fatalf("the identities read back are %+v", all)
}
// A module forgotten keeps its row, and the next module is given a new number: never one given before.
if _, err := inv.store.Pool().Exec(ctx, `delete from module_identity where module = 'plex'`); err != nil {
t.Fatal(err)
}
sonarr, err := inv.AllocateIdentity(ctx, "sonarr")
if err != nil {
t.Fatal(err)
}
if sonarr.UID == plex.UID || sonarr.UID == radarr.UID {
t.Fatalf("sonarr was given %d, a number given before", sonarr.UID)
}
// The store itself refuses a number outside the range, root's above all: no write by hand makes one root.
for _, n := range []int{0, 10, 1000, 49999, 60000} {
if _, err := inv.store.Pool().Exec(ctx,
`insert into module_identity (module, uid, gid) values ($1, $2, $2)`, "byhand", n); err == nil {
t.Errorf("the store took %d as an identity", n)
}
}
}
// novox/hq ADR 0325 §8: a folder taken is recorded once per machine, under a warrant, and read back by its path.
func TestAFolderIsTakenOnceUnderAWarrant(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
if _, err := inv.AddNode(ctx, "ace"); err != nil {
t.Fatal(err)
}
if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies/", By: "the operator"}); err == nil {
t.Fatal("a folder was recorded taken with no warrant")
}
first := TakenFolder{Path: "/storage/media/movies/", By: "the operator", Ask: "c01", Warrant: "c01", Said: "started"}
if err := inv.RecordTaken(ctx, "ace", first); err != nil {
t.Fatal(err)
}
if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies", By: "someone else", Ask: "c02",
Warrant: "c02"}); err != nil {
t.Fatal(err)
}
kept, err := inv.KeptFolders(ctx, "ace")
if err != nil {
t.Fatal(err)
}
got, has := kept["/storage/media/movies"]
if !has || got.Ask != "c01" || got.By != "the operator" {
t.Fatalf("the taken folder reads %+v (%v): the first take, by its clean path", got, has)
}
}
// novox/hq issue 575: a module registered without the access movies, whose newest build on its branch declares it —
// the build the delivery under way brings — takes a proposal placing movies: judged against that build, not refused.
// Without such a build it is refused, as before.
func TestAPlacementIsJudgedAgainstTheBuildThatBringsTheAccess(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
if _, err := inv.AddNode(ctx, "ace"); err != nil {
t.Fatal(err)
}
registered := catalogue.Manifest{Module: "media-importer", Version: "1",
Accesses: []catalogue.Access{{ID: "downloads", Path: "/services/media/downloads", Mode: catalogue.AccessReadWrite}},
Resources: []map[string]any{{"id": "server", "type": "container", "name": "importer", "image": "x",
"volumes": []any{"${access:downloads}:/downloads"}}}}
if err := inv.RegisterModule(ctx, registered, Source{Repository: "https://forge.invalid/media.git",
Ref: "main", BuiltFrom: "c0ffee33"}); err != nil {
t.Fatal(err)
}
placing := map[string]any{catalogue.AccessesSetting: map[string]any{"movies": "/storage/media/movies"}}
err := inv.JudgeSettings(ctx, "ace", "media-importer", placing)
if err == nil || !strings.Contains(err.Error(), `places the access "movies", which it does not declare`) {
t.Fatalf("a placement no build declares: %v", err)
}
// main's manifest — the build of #34 — declares movies.
ahead := registered
ahead.Accesses = append([]catalogue.Access{}, registered.Accesses...)
ahead.Accesses = append(ahead.Accesses, catalogue.Access{ID: "movies", Path: "/services/media/movies",
Mode: catalogue.AccessReadWrite})
raw, err := json.Marshal(ahead)
if err != nil {
t.Fatal(err)
}
b := aBuild("b34", "media-importer", "")
b.Ref, b.Manifest = "main", raw
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
if err := inv.JudgeSettings(ctx, "ace", "media-importer", placing); err != nil {
t.Fatalf("the proposal placing movies, which main's build declares, was refused: %v", err)
}
if err := inv.SetSettings(ctx, "ace", "media-importer", placing); err != nil {
t.Fatalf("the layer placing movies was not stored: %v", err)
}
// Stored ahead, it does not cost the registered module its place: composition passes it by.
layers, err := inv.SettingsFor(ctx, "ace", "media-importer")
if err != nil {
t.Fatal(err)
}
if err := catalogue.ComposesWith(registered, layers, false); err != nil {
t.Fatalf("the registered module no longer composes with the layer stored ahead: %v", err)
}
// A typo is still a typo: an access neither build declares is refused.
typo := map[string]any{catalogue.AccessesSetting: map[string]any{"moveis": "/storage/media/movies"}}
if err := inv.JudgeSettings(ctx, "ace", "media-importer", typo); err == nil {
t.Fatal("a placement no build declares was taken")
}
}