mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/418-node-reads delivered: every member is delivered
The real mesh-vault manifest declares its directories without a path, so the list left its state and ledger out, and a place still holding a placeholder vanished from it without a word. The provider's directories are now resolved where the node places them, an unresolved place fails the composition naming the module, and a node-reads holder whose account is put in a group is refused.
137 lines
6.3 KiB
Go
137 lines
6.3 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// novox/hq ADR 0310 §1, §2: node-reads is a node seat, and its verbs say the commands they replace, so
|
|
// mesh_search finds them by `cat`, `grep` and `getent passwd` (ADR 0245 §3).
|
|
func TestNodeReadsIsANodeSeatWhoseVerbsSayWhatTheyReplace(t *testing.T) {
|
|
s, ok := SeatNamed(ReadsSeat)
|
|
if !ok {
|
|
t.Fatalf("the mesh defines no seat %s", ReadsSeat)
|
|
}
|
|
if s.Scope != ScopeNode || s.Delivers != "" || !strings.Contains(s.Decision, "0310") {
|
|
t.Errorf("%s: scope %q, delivers %q, decision %q", ReadsSeat, s.Scope, s.Delivers, s.Decision)
|
|
}
|
|
replaced := map[string]string{}
|
|
for _, v := range s.Serves {
|
|
if v.Optional {
|
|
t.Errorf("%s.%s is optional: a new seat's holder serves every verb", ReadsSeat, v.Name)
|
|
}
|
|
if len(v.Replaces) == 0 {
|
|
t.Errorf("%s.%s says nothing it replaces", ReadsSeat, v.Name)
|
|
}
|
|
for _, c := range v.Replaces {
|
|
replaced[c] = v.Name
|
|
}
|
|
}
|
|
for command, verb := range map[string]string{"cat": "read", "grep": "search", "getent passwd": "account",
|
|
"getent group": "account", "id": "account", "stat": "stat", "readlink": "readlink", "ls": "list"} {
|
|
if replaced[command] != verb {
|
|
t.Errorf("%q is replaced by %q, want %s.%s", command, replaced[command], ReadsSeat, verb)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0310 §3: a holder of node-reads reads as an account of its own; one the machine's runtime would
|
|
// carry — as the operator's account — is refused at parse.
|
|
func TestAHolderOfNodeReadsWithoutAnAccountOfItsOwnIsRefused(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module": "reads", "version": "1",
|
|
"claims": [{"name": "node-reads", "scope": "node"}]}`))
|
|
if err == nil || !strings.Contains(err.Error(), "runs-as") || !strings.Contains(err.Error(), "0310") {
|
|
t.Errorf("a holder of node-reads without runs-as was not refused for it: %v", err)
|
|
}
|
|
if p := ReadsHolderProblems(Manifest{Module: "reads", RunsAs: "mesh-reads",
|
|
Claims: []Claim{{Name: ReadsSeat}}}); len(p) != 0 {
|
|
t.Errorf("a holder running as its own account was refused: %v", p)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0310 §5: the holder is told, by the composition, where its node keeps a secret — the directory
|
|
// of every module's own secret, every credential file and grant directory, the vault's directories and the
|
|
// place the operator-sealed secrets are kept — and no other module is told.
|
|
func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) {
|
|
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
|
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"},
|
|
"reads": {"broker": "own"}, "desk": {"broker": "own"}}}
|
|
goRuntime := Manifest{Module: RuntimeModule, Version: "1",
|
|
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
|
|
Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go",
|
|
System: "arch", From: "cmd/node-tools"}}}}
|
|
goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle,
|
|
Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reads := aToolsModule(t, "reads", "tools/index.js")
|
|
reads.RunsAs, reads.SecretsOwner = "mesh-reads", "mesh-reads"
|
|
reads.Claims = []Claim{{Name: ReadsSeat}}
|
|
reads.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/mesh-reads/broker"}}
|
|
desk := aToolsModule(t, "desk", "tools/index.js")
|
|
desk.RunsAs, desk.SecretsOwner = "desk", "desk"
|
|
desk.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/desk/broker"}}
|
|
// The vault as the catalogue declares it: its directories have no path of their own, and the node places them.
|
|
vault, err := placedManifest(catalogueManifest(t, "mesh-vault"), with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := Manifest{Module: "app", Version: "1", Secrets: map[string]string{"postgres-database": "/etc/app/db.secret"},
|
|
OwnSecrets: OwnSecrets{"token": {Path: "/home/ops/.config/app/token"}}}
|
|
r := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{reads, desk, vault, app, goRuntime}}
|
|
owns, err := r.ownRuntimes(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var given map[string]map[string]string
|
|
for _, p := range owns {
|
|
if p["id"] == "reads."+OwnRuntimeID() {
|
|
if err := json.Unmarshal([]byte(p["env"].(map[string]string)[RuntimeToolEnv]), &given); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if p["id"] == "desk."+OwnRuntimeID() && strings.Contains(p["env"].(map[string]string)[RuntimeToolEnv], ReadsSecretPlaces) {
|
|
t.Error("a module not holding node-reads is told the node's secret places")
|
|
}
|
|
}
|
|
var places []string
|
|
if err := json.Unmarshal([]byte(given["reads"][ReadsSecretPlaces]), &places); err != nil {
|
|
t.Fatalf("the holder is not told its node's secret places: %v (%v)", err, given)
|
|
}
|
|
want := []string{"/etc/app/db.secret", "/home/ops/.config/app", "/var/lib/desk", "/var/lib/mesh-reads",
|
|
"/var/lib/mesh-vault", "/var/lib/mesh/" + RuntimeModule}
|
|
if strings.Join(places, " ") != strings.Join(want, " ") {
|
|
t.Errorf("secret places:\n got %v\nwant %v", places, want)
|
|
}
|
|
}
|
|
|
|
// The review of #257: a secret place the controller cannot resolve fails the composition, naming the module,
|
|
// rather than leaving the list one place shorter.
|
|
func TestASecretPlaceThatCannotBeResolvedFailsTheComposition(t *testing.T) {
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{
|
|
{Module: "app", Version: "1", Keeps: "${dir:keep}/sealed"},
|
|
}}
|
|
if _, err := r.secretPlaces(Rendering{}); err == nil || !strings.Contains(err.Error(), "app") {
|
|
t.Errorf("an unresolved place was dropped: %v", err)
|
|
}
|
|
r.Modules[0].Keeps = "relative/place"
|
|
if _, err := r.secretPlaces(Rendering{}); err == nil {
|
|
t.Error("a relative place was dropped")
|
|
}
|
|
}
|
|
|
|
// ADR 0310 §3: the account that reads is in no group beside its own; one put in a group is refused.
|
|
func TestTheReadsAccountIsInNoGroup(t *testing.T) {
|
|
m := Manifest{Module: "reads", RunsAs: "mesh-reads", Claims: []Claim{{Name: ReadsSeat}},
|
|
Resources: []map[string]any{{"id": "account", "type": "user", "name": "mesh-reads", "groups": []any{"docker"}}}}
|
|
if p := ReadsHolderProblems(m); len(p) != 1 || !strings.Contains(p[0], "docker") {
|
|
t.Errorf("an account in docker was not refused: %v", p)
|
|
}
|
|
m.Resources[0]["groups"] = []any{}
|
|
if p := ReadsHolderProblems(m); len(p) != 0 {
|
|
t.Errorf("an account in no group was refused: %v", p)
|
|
}
|
|
}
|