mesh/merge-gate head d309bb6cef45 merged into trunk 253f464a8155 — pass: builds mesh-delivery-view → shanks; no bus step; every machine composes with the c…
mesh/repo-check head d309bb6cef45 merged into trunk 253f464a8155 — pass: its merge-check.sh passed
mesh/delivery delivered
249 lines
8.7 KiB
Go
249 lines
8.7 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The controller's manifest reader is mesh-controller internal/catalogue (ParseManifest), an internal package of another
|
|
// repository, which this module cannot import. These tests therefore decode the manifest strictly into the fields the
|
|
// controller's Manifest, Build and Artifact types name (internal/catalogue/manifest.go:475, :602, :659, :667, :737,
|
|
// :809, :821, :892, :1010, :1065-:1090), as its parseManifest does (DisallowUnknownFields, manifest.go:1744), and check
|
|
// the rules the issue names, and two rules of the controller's module check (a module with templates packs a files
|
|
// archive; a module contributing an item is moved). **The merge gate's module check is what proves the controller
|
|
// takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.**
|
|
|
|
type artifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
System string `json:"system"`
|
|
From string `json:"from"`
|
|
Binary string `json:"binary"`
|
|
Context *struct {
|
|
Repository string `json:"repository"`
|
|
} `json:"context"`
|
|
}
|
|
|
|
type item struct {
|
|
Collection string `json:"collection"`
|
|
Template string `json:"template"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Moved bool `json:"moved"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Reads []string `json:"reads"`
|
|
OwnSecrets map[string]string `json:"own-secrets"`
|
|
SecretsOwner string `json:"secrets-owner"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Items []item `json:"items"`
|
|
Build struct {
|
|
Artifacts []artifact `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func load(t *testing.T) (manifest, map[string]json.RawMessage) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("the controller's reader refuses a key this manifest has: %v", err)
|
|
}
|
|
var top map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &top); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, top
|
|
}
|
|
|
|
func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-view" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
want := []string{"mesh-controller.assignments", "mesh-controller.builds", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "node-engine.have"} // 1168/AC1
|
|
got := slices.Clone(m.Reads)
|
|
slices.Sort(got)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("reads = %v, want exactly %v", got, want)
|
|
}
|
|
for _, k := range []string{"state", "emits", "consumes", "provides", "claims"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the view writes nothing", k)
|
|
}
|
|
}
|
|
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 {
|
|
t.Errorf("own-secrets = %v, want exactly broker (or no credential is issued)", m.OwnSecrets)
|
|
}
|
|
if m.SecretsOwner != "mesh-delivery-view" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
}
|
|
|
|
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
|
|
m, _ := load(t)
|
|
if len(m.Build.Artifacts) != 3 {
|
|
t.Fatalf("artifacts = %+v", m.Build.Artifacts)
|
|
}
|
|
for _, a := range m.Build.Artifacts {
|
|
if a.Name == "files" {
|
|
continue
|
|
}
|
|
if a.Context != nil {
|
|
t.Errorf("artifact %s names a context of another repository", a.Name)
|
|
}
|
|
if a.Kind != "bundle" || a.Language != "go" || a.System != "arch" {
|
|
t.Errorf("artifact %s = %+v, want a go bundle for arch", a.Name, a)
|
|
}
|
|
if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
|
|
t.Errorf("artifact %s builds from %q, which is not a directory of this module", a.Name, a.From)
|
|
}
|
|
if a.Binary == "" || !strings.HasSuffix(a.From, a.Binary) {
|
|
t.Errorf("artifact %s binary %q does not match %q", a.Name, a.Binary, a.From)
|
|
}
|
|
}
|
|
names := map[string]string{}
|
|
for _, a := range m.Build.Artifacts {
|
|
names[a.Name] = a.Binary
|
|
}
|
|
if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" {
|
|
t.Errorf("artifacts = %v", names)
|
|
}
|
|
}
|
|
|
|
func TestManifestResources(t *testing.T) { // 1134/AC7
|
|
m, _ := load(t)
|
|
byType := map[string][]map[string]any{}
|
|
for _, r := range m.Resources {
|
|
byType[r["type"].(string)] = append(byType[r["type"].(string)], r)
|
|
}
|
|
for _, ty := range []string{"user", "directory", "process", "archive"} {
|
|
if len(byType[ty]) == 0 {
|
|
t.Fatalf("no %s resource", ty)
|
|
}
|
|
}
|
|
user := byType["user"][0]["name"]
|
|
if user != "mesh-delivery-view" {
|
|
t.Errorf("user = %v", user)
|
|
}
|
|
p := byType["process"][0]
|
|
if p["user"] != user || p["artifact"] != "program" {
|
|
t.Errorf("process = %v", p)
|
|
}
|
|
env, _ := p["env"].(map[string]any)
|
|
if env["MESH_BROKER_FILE"] != "${dir:state}/broker" || m.OwnSecrets["broker"] != "${dir:state}/broker" {
|
|
t.Errorf("the process reads %v, the own-secret is written at %v", env["MESH_BROKER_FILE"], m.OwnSecrets["broker"])
|
|
}
|
|
if h, _ := p["health"].(map[string]any); h["kind"] != "unit" {
|
|
t.Errorf("health = %v", p["health"])
|
|
}
|
|
var state, run bool
|
|
for _, d := range byType["directory"] {
|
|
if d["id"] == "state" && d["mode"] == "0700" && d["owner"] == user {
|
|
state = true
|
|
}
|
|
if d["path"] == "/var/lib/mesh-delivery-view-cli" && d["mode"] == "0755" && d["owner"] == user {
|
|
run = true
|
|
}
|
|
}
|
|
if !state || !run {
|
|
t.Errorf("state directory 0700 owned by the user: %v; run directory for the socket 0755: %v", state, run)
|
|
}
|
|
a := byType["archive"][0]
|
|
if a["artifact"] != "cli" || a["path"] != "/usr/local/lib/mesh-delivery-view" {
|
|
t.Errorf("archive = %v, want the cli unpacked into its own directory", a)
|
|
}
|
|
}
|
|
|
|
func TestModuleCheckRules(t *testing.T) { // 1134/AC7
|
|
m, _ := load(t)
|
|
if len(m.Items) > 0 && !m.Moved {
|
|
t.Error("a module contributing an item is marked moved: true")
|
|
}
|
|
var files bool
|
|
for _, a := range m.Build.Artifacts {
|
|
files = files || (a.Name == "files" && a.Kind == "archive" && a.From == "files")
|
|
}
|
|
if len(m.Items) > 0 && !files {
|
|
t.Error("a module naming templates packs a files archive from files/")
|
|
}
|
|
}
|
|
|
|
func TestAliasItem(t *testing.T) { // 1137/AC1
|
|
m, _ := load(t)
|
|
if !reflect.DeepEqual(m.Items, []item{{Collection: "shell:zsh:normal", Template: "files/zsh-normal"}}) {
|
|
t.Fatalf("items = %+v", m.Items)
|
|
}
|
|
raw, err := os.ReadFile("files/zsh-normal")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(raw) != "alias nox-delivery=mesh-delivery-cli\n" {
|
|
t.Fatalf("files/zsh-normal = %q, want the one line", raw)
|
|
}
|
|
}
|
|
|
|
// The controller refuses two modules declaring one path (internal/catalogue/resolve.go:1160-1168). mesh-cli (mesh-catalog
|
|
// modules/mesh-cli/module.json) declares /usr/local/bin whole, so this module may place only a file below a shared
|
|
// directory, never the directory itself.
|
|
func TestNoSharedDirectoryIsClaimed(t *testing.T) { // 1134/AC7
|
|
m, _ := load(t)
|
|
shared := map[string]bool{"/usr/local/bin": true, "/usr/local/lib": true, "/usr/bin": true, "/usr/local": true, "/usr/lib": true, "/etc": true, "/opt": true}
|
|
var binary bool
|
|
for _, r := range m.Resources {
|
|
p, _ := r["path"].(string)
|
|
if shared[strings.TrimRight(p, "/")] {
|
|
t.Errorf("resource %v declares the shared directory %s, which mesh-cli and others declare too", r["id"], p)
|
|
}
|
|
if p == "/usr/local/bin/mesh-delivery-cli" && r["type"] == "file" && r["template"] == "files/mesh-delivery-cli" {
|
|
binary = true
|
|
}
|
|
}
|
|
if !binary {
|
|
t.Error("the command is placed as the file /usr/local/bin/mesh-delivery-cli")
|
|
}
|
|
raw, err := os.ReadFile("files/mesh-delivery-cli")
|
|
if err != nil || !strings.Contains(string(raw), "exec /usr/local/lib/mesh-delivery-view/mesh-delivery-cli") {
|
|
t.Errorf("files/mesh-delivery-cli = %q, %v", raw, err)
|
|
}
|
|
}
|
|
|
|
// The node-engine never places a module-owned directory below /etc, /usr, /run or /var/run: they are root's
|
|
// (mesh-host internal/apply/placement_guard.go:80, rootsOnly; novox/hq issue 339).
|
|
func TestNoDirectoryBelowRootsTrees(t *testing.T) { // 1134/AC10
|
|
m, _ := load(t)
|
|
for _, r := range m.Resources {
|
|
if r["type"] != "directory" {
|
|
continue
|
|
}
|
|
p, _ := r["path"].(string)
|
|
for _, root := range []string{"/etc", "/usr", "/run", "/var/run"} {
|
|
if p == root || strings.HasPrefix(p, root+"/") {
|
|
t.Errorf("directory %v at %s is below %s, which is root's", r["id"], p, root)
|
|
}
|
|
}
|
|
}
|
|
var socketDir bool
|
|
for _, r := range m.Resources {
|
|
if r["path"] == "/var/lib/mesh-delivery-view-cli" && r["type"] == "directory" && r["mode"] == "0755" && r["owner"] == "mesh-delivery-view" {
|
|
socketDir = true
|
|
}
|
|
}
|
|
if !socketDir {
|
|
t.Error("the socket's directory is /var/lib/mesh-delivery-view-cli, 0755, owned by mesh-delivery-view")
|
|
}
|
|
}
|