From e58761afcecbd5ebeb9d1e1ca6b95fe86bb4a0d1 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 08:17:05 +0000 Subject: [PATCH 1/9] Capture the catalogue's module.json files and the placement guard at today's mains for the moved catalogue tests (issue 1379) mesh-catalog bb0fe93b, mesh-host cdcba9ca; part 1 of 3 of go/testdata/beside (module.json as module.json.captured). --- go/testdata/beside/CAPTURED | 27 + .../modules/adwaita/module.json.captured | 176 +++++ .../beside/mesh-catalog/predefined.json | 13 + .../apply/placement_guard.go.captured | 626 ++++++++++++++++++ 4 files changed, 842 insertions(+) create mode 100644 go/testdata/beside/CAPTURED create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/predefined.json create mode 100644 go/testdata/beside/mesh-host/internal/apply/placement_guard.go.captured diff --git a/go/testdata/beside/CAPTURED b/go/testdata/beside/CAPTURED new file mode 100644 index 0000000..825bcd1 --- /dev/null +++ b/go/testdata/beside/CAPTURED @@ -0,0 +1,27 @@ +What a test reads of another repository when no merge check has cloned it beside this one (internal/beside, +novox/hq issue 432). Copied from the repositories at the commits below, never written by hand. Each +module.json is kept as module.json.captured: a module.json in this repository is a module of it to the +forge and the planner, and a merge would build and register it. + +The copy carries the catalogue's private details: domains, first names in module names, a node name and +private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is +a second place to clean besides the catalogue itself. + +Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 2026-10-20 from the mains of both +repositories; before that it was mesh-controller's copy, which still held two modules that had left the catalogue. + +mesh-catalog bb0fe93bbdddd869ad808210352461efc506c3d3 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json +mesh-host cdcba9ca73755854d21e4e657d7e46c32b9b5265 internal/apply/placement_guard.go + +To move them, from this repository's go/ directory, with the two repositories checked out beside mesh-sdk: + + rm -rf testdata/beside/mesh-catalog testdata/beside/mesh-host + mkdir -p testdata/beside/mesh-catalog testdata/beside/mesh-host/internal/apply + git -C ../../mesh-catalog archive modules predefined.json | tar -x -C testdata/beside/mesh-catalog --wildcards \ + predefined.json "modules/*/module.json" "modules/*/files/*" "modules/*/templates/*" + find testdata/beside -name module.json -exec mv {} {}.captured \; + git -C ../../mesh-host show :internal/apply/placement_guard.go \ + > testdata/beside/mesh-host/internal/apply/placement_guard.go.captured + +and write the commits here. The placement guard is kept as .captured so no Go tool reads it as this +repository's code. diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/module.json.captured b/go/testdata/beside/mesh-catalog/modules/adwaita/module.json.captured new file mode 100644 index 0000000..bf81c83 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/module.json.captured @@ -0,0 +1,176 @@ +{ + "module": "adwaita", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "tools": [ + "adwaita_appearance", + "adwaita_cursor", + "adwaita_icons", + "adwaita_portal_check" + ], + "items": [ + { + "collection": "environment", + "name": "gtk-theme", + "attributes": { + "kind": "variable", + "name": "GTK_THEME" + }, + "template": "files/env-gtk-theme" + }, + { + "collection": "environment", + "name": "gtk2-rc-files", + "attributes": { + "kind": "variable", + "name": "GTK2_RC_FILES" + }, + "template": "files/env-gtk2-rc-files" + }, + { + "collection": "environment", + "name": "qt-qpa-platformtheme", + "attributes": { + "kind": "variable", + "name": "QT_QPA_PLATFORMTHEME" + }, + "template": "files/env-qt-qpa-platformtheme" + }, + { + "collection": "environment", + "name": "qt-style-override", + "attributes": { + "kind": "variable", + "name": "QT_STYLE_OVERRIDE" + }, + "template": "files/env-qt-style-override" + }, + { + "collection": "environment", + "name": "qt-select", + "attributes": { + "kind": "variable", + "name": "QT_SELECT" + }, + "template": "files/env-qt-select" + }, + { + "collection": "environment", + "name": "xcursor-theme", + "attributes": { + "kind": "variable", + "name": "XCURSOR_THEME" + }, + "template": "files/env-xcursor-theme" + }, + { + "collection": "environment", + "name": "xcursor-size", + "attributes": { + "kind": "variable", + "name": "XCURSOR_SIZE" + }, + "template": "files/env-xcursor-size" + }, + { + "collection": "shell:xresources:normal", + "name": "xresources-normal", + "template": "files/xresources-normal" + }, + { + "collection": "shell:xinitrc:normal", + "name": "xinitrc-normal", + "template": "files/xinitrc-normal" + } + ], + "resources": [ + { + "id": "package-gnome-themes-extra", + "type": "package", + "package": "gnome-themes-extra" + }, + { + "id": "package-adwaita-icon-theme", + "type": "package", + "package": "adwaita-icon-theme" + }, + { + "id": "package-adwaita-cursors", + "type": "package", + "package": "adwaita-cursors" + }, + { + "id": "package-qt6ct", + "type": "package", + "package": "qt6ct" + }, + { + "id": "package-xdg-desktop-portal-gtk", + "type": "package", + "package": "xdg-desktop-portal-gtk" + }, + { + "id": "gtk3", + "type": "file", + "path": "${machine:account-home}/.config/gtk-3.0/settings.ini", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/gtk-settings.ini" + }, + { + "id": "gtk4", + "type": "file", + "path": "${machine:account-home}/.config/gtk-4.0/settings.ini", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/gtk-settings.ini" + }, + { + "id": "qt6ct", + "type": "file", + "path": "${machine:account-home}/.config/qt6ct/qt6ct.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/qt6ct.conf" + }, + { + "id": "portals", + "type": "file", + "path": "${machine:account-home}/.config/xdg-desktop-portal/portals.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/portals.conf" + }, + { + "id": "cursor", + "type": "file", + "path": "${machine:account-home}/.icons/default/index.theme", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/cursor-index.theme" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/adwaita-tools", + "binary": "adwaita-tools", + "loads": [ + "adwaita-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/predefined.json b/go/testdata/beside/mesh-catalog/predefined.json new file mode 100644 index 0000000..9e53676 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/predefined.json @@ -0,0 +1,13 @@ +{ + "agent-managed-settings": { + "type": "object", + "description": "The managed settings every agent session on a node is held to (Claude Code's managed-settings.json), as a JSON object.", + "trusted": true + }, + "font-size": { + "type": "number", + "description": "The size a desktop program shows its text in, in points.", + "minimum": 6, + "maximum": 72 + } +} diff --git a/go/testdata/beside/mesh-host/internal/apply/placement_guard.go.captured b/go/testdata/beside/mesh-host/internal/apply/placement_guard.go.captured new file mode 100644 index 0000000..e23ef45 --- /dev/null +++ b/go/testdata/beside/mesh-host/internal/apply/placement_guard.go.captured @@ -0,0 +1,626 @@ +package apply + +// Where the node-engine places nothing and mounts nothing, whoever asks (novox/hq issue 339). +// +// A directory names its path, and the controller resolves part of that path from what was set for the +// module: its `places` setting moves a directory anywhere, with an owner it names, and its `accesses` setting +// says which of the machine's paths are mounted into its container. The engine runs as root, so a path it +// accepts blindly is a path anyone who could change those settings hands to any account: a directory at /etc +// owned by a caller's account gives it /etc, and an access at / mounts the machine's root into a container. +// The controller refuses both where a setting is made; the engine refuses them again where it applies, +// because a guard in one place is a guard one change away from gone. Whatever the declaration says: +// +// 1. **No directory, access or mount source is one of the machine's own roots, or holds one**: /, /etc, +// /usr, /var, /var/lib, /home, /run and the rest of protectedRoots. Modules place directories BELOW /etc +// or /var/lib, never the root itself; owning one is owning everything in it. +// 2. **Nothing is placed in /proc, /sys, /dev, /boot, /root, /var/spool, /opt or the container runtimes' data +// (/var/lib/docker, /var/lib/containers, /var/lib/containerd, and where the runtimes' configuration moves +// them: runtimeDataRoots), nor in the node-engine's +// own trees** (its state, its identity, its installed builds) but by its own module; nothing is mounted from +// those but /proc, /sys and /dev. A mount of a kernel file, a device or the clock is the plumbing +// systemPath names. +// 2a. **An account's .ssh is never an access or a mount**, and is a directory only below its account's home, +// as that account's (rule 4). +// 3. **A directory below /etc, /usr or /run is root's.** The machine's configuration and programs are read +// as root's word; a directory there owned by another account is that account writing root's word. An +// access is never there at all: the operator's data is not the machine's configuration. +// 4. **Below a person's or an agent's home, a directory is that account's.** Root's or another account's +// directory there is one the account does not control in a tree whose every parent it does. A home +// itself may be a module's directory (a backup repository kept as an account's home is one), and as +// every directory the mesh did not make, it is used as found: never chowned or chmodded (applyDirectory). +// 5. **A mount source that is a refused directory or a refused access is refused with it**: the container +// would otherwise bind the very path the engine would not place, and the runtime creates a missing one +// as root. +// 6. **A file is judged as a directory is** (novox/hq issue 495): never one of the machine's own roots, nor +// in a tree nothing is placed in (rule 2), and never below a directory or an access refused here. A file +// or an archive below a refused path is refused with it, whichever module declares it and in whatever +// order. Rules 3 and 4 are a directory's owner and do not reach a file: a file below /etc is the mesh's +// ordinary configuration, and one below a home is judged by home_links.go and agent_home_guard.go. +// A refused directory also gates the rest of its module, as a step that did not complete does +// (apply.go's `gated`): what a module declares after its directory was written for that directory. +// +// Each is a failed resource with its reason in the node's report; nothing is touched. Paths are judged as +// declared and again with every link in them resolved, so a link at /srv/x pointing at /etc places nothing. + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/novox/mesh-host/internal/declaration" +) + +// protectedRoots are paths no directory, access or mount source may be, nor hold. +var protectedRoots = []string{"/", "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib32", "/lib64", + "/media", "/mnt", "/opt", "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", "/usr/bin", + "/usr/lib", "/usr/lib64", "/usr/local", "/usr/local/bin", "/usr/local/lib", "/usr/local/sbin", "/usr/sbin", + "/usr/share", "/var", "/var/cache", "/var/lib", "/var/lib/mesh", "/var/log", "/var/run", "/var/tmp", + "/var/spool"} + +// forbiddenBelow are trees nothing is placed in or mounted from: the kernel's, the boot loader's and root's +// home. engineTrees are the node-engine's own, which only its own module places in. +var ( + forbiddenBelow = []string{"/proc", "/sys", "/dev", "/boot", "/root", "/var/spool", "/opt", "/var/lib/docker", + "/var/lib/containers", "/var/lib/containerd"} + // forbiddenBelowMount are the trees no container mounts from: a mount of the kernel's files and devices is + // the plumbing a container may need (systemPath); root's home and the boot loader's are no plumbing. + forbiddenBelowMount = []string{"/boot", "/root", "/var/spool", "/opt", "/var/lib/docker", "/var/lib/containers", + "/var/lib/containerd"} + engineTrees = []string{"/var/lib/mesh-host", "/usr/lib/nox-mesh-host"} + // rootsOnly are trees a directory below is root's, and an access is never in. + rootsOnly = []string{"/etc", "/usr", "/run", "/var/run"} +) + +// runtimeFiles are where the container runtimes say where they keep their data: dockerd's daemon.json, its +// unit and the unit's drop-ins (an ExecStart with --data-root, or the older -g/--graph, continued over lines, +// quoted, through Environment= or EnvironmentFile=, or a --config-file naming another daemon.json), and podman's +// storage.conf (graphroot, a basic or a literal string). The running runtimes are asked first. containerd keeps its own under /var/lib/containerd, which forbiddenBelow names; a +// containerd configured elsewhere, and podman's rootless stores under each account's home, are not read: the +// first is no runtime this mesh runs, and the second is below a home, which rule 4 already keeps for its +// account. A variable so a test names its own. +type runtimeFiles struct { + daemonJSON string + units []string + dropInDirs []string + storageConf string +} + +var runtimeConfigs = runtimeFiles{ + daemonJSON: "/etc/docker/daemon.json", + units: []string{"/etc/systemd/system/docker.service", "/usr/lib/systemd/system/docker.service", "/lib/systemd/system/docker.service"}, + dropInDirs: []string{"/etc/systemd/system/docker.service.d", "/run/systemd/system/docker.service.d", "/usr/lib/systemd/system/docker.service.d"}, + storageConf: "/etc/containers/storage.conf", +} + +var ( + dataRootFlag = regexp.MustCompile(`(?:--data-root|--graph|-g)(?:=|\s+)(\S+)`) + configFlag = regexp.MustCompile(`--config-file(?:=|\s+)(\S+)`) + graphRoot = regexp.MustCompile(`(?m)^\s*graphroot\s*=\s*(?:"([^"]+)"|'([^']+)')`) + envVar = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)`) +) + +// runtimeRoots is where the container runtimes keep their data, as the last apply read it (readRuntimeRoots); +// nil until an apply has read it, when the guard reads the files itself. +var ( + runtimeRootsMu sync.Mutex + runtimeRoots []string +) + +// AskRuntimes is how the engine asks the running container runtimes where they keep their data: set by the engine +// to run the commands, nil in a test, which then reads the files alone. Its own, never the apply's runner, whose +// commands a test reads back as what the apply did. +var AskRuntimes Runner + +// refreshRuntimeRoots reads where the runtimes keep their data once, at the start of an apply. +func refreshRuntimeRoots(ctx context.Context) { + roots := readRuntimeRoots(ctx, AskRuntimes) + runtimeRootsMu.Lock() + runtimeRoots = roots + runtimeRootsMu.Unlock() +} + +// runtimeDataRoots is every place the container runtimes keep their data, beyond the default trees forbiddenBelow +// names: every container's filesystem is there. +func runtimeDataRoots() []string { + runtimeRootsMu.Lock() + roots := runtimeRoots + runtimeRootsMu.Unlock() + if roots != nil { + return roots + } + return readRuntimeRoots(context.Background(), nil) +} + +// readRuntimeRoots asks the running runtimes where they keep their data, when run is given (`docker info`, +// `podman info`), and reads their configuration besides: an answer from a runtime that is running is what it +// really does, and the files say what it will do when it starts again. Both count. A runtime that is not running +// or not installed answers nothing, which is no error. +func readRuntimeRoots(ctx context.Context, run Runner) []string { + seen := map[string]bool{} + var out []string + add := func(p string) { + p = strings.Trim(strings.TrimSpace(p), `"'`) + if !filepath.IsAbs(p) { + return + } + p = filepath.Clean(p) + if !seen[p] { + seen[p] = true + out = append(out, p) + } + } + if run != nil { + // Each runtime has its own ten seconds: one that hangs costs the other nothing. + for _, q := range [][]string{{"docker", "info", "--format", "{{.DockerRootDir}}"}, + {"podman", "info", "--format", "{{.Store.GraphRoot}}"}} { + ask, cancel := context.WithTimeout(ctx, 10*time.Second) + if root, err := run(ask, q[0], q[1:]...); err == nil { + add(root) + } + cancel() + } + } + daemonJSON := func(path string) { + raw, err := os.ReadFile(path) + if err != nil { + return + } + var c struct { + DataRoot string `json:"data-root"` + Graph string `json:"graph"` + } + if json.Unmarshal(raw, &c) == nil { + add(c.DataRoot) + add(c.Graph) + } + } + daemonJSON(runtimeConfigs.daemonJSON) + units := append([]string(nil), runtimeConfigs.units...) + for _, dir := range runtimeConfigs.dropInDirs { + matches, _ := filepath.Glob(filepath.Join(dir, "*.conf")) + units = append(units, matches...) + } + // The unit and its drop-ins are one unit to systemd: a variable set in one is seen by an ExecStart in another. + env := map[string]string{} + var execs []string + for _, u := range units { + raw, err := os.ReadFile(u) + if err != nil { + continue + } + e, x := unitLines(string(raw)) + for k, v := range e { + env[k] = v + } + execs = append(execs, x...) + } + for _, line := range execs { + line = envVar.ReplaceAllStringFunc(line, func(ref string) string { + m := envVar.FindStringSubmatch(ref) + if v, ok := env[m[1]+m[2]]; ok { + return v + } + return ref + }) + for _, m := range dataRootFlag.FindAllStringSubmatch(line, -1) { + add(m[1]) + } + for _, m := range configFlag.FindAllStringSubmatch(line, -1) { + daemonJSON(strings.Trim(m[1], `"'`)) + } + } + if raw, err := os.ReadFile(runtimeConfigs.storageConf); err == nil { + for _, m := range graphRoot.FindAllStringSubmatch(string(raw), -1) { + add(m[1] + m[2]) + } + } + return out +} + +// unitLines reads a unit file as systemd does for what matters here: a line ending in a backslash continues on the +// next, Environment= sets variables (quoted or not, several to a line), EnvironmentFile= (a leading - says it may +// be missing) reads KEY=value lines, and every ExecStart line is returned whole. +func unitLines(text string) (map[string]string, []string) { + var joined []string + var cur strings.Builder + for _, line := range strings.Split(text, "\n") { + trimmed := strings.TrimRight(line, " \t") + if strings.HasSuffix(trimmed, "\\") { + cur.WriteString(strings.TrimSuffix(trimmed, "\\") + " ") + continue + } + cur.WriteString(line) + joined = append(joined, cur.String()) + cur.Reset() + } + if cur.Len() > 0 { + joined = append(joined, cur.String()) + } + env := map[string]string{} + setPairs := func(s string) { + for _, f := range splitQuoted(s) { + if k, v, ok := strings.Cut(f, "="); ok { + env[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), `"'`) + } + } + } + var execs []string + for _, line := range joined { + l := strings.TrimSpace(line) + switch { + case strings.HasPrefix(l, "Environment="): + setPairs(strings.TrimPrefix(l, "Environment=")) + case strings.HasPrefix(l, "EnvironmentFile="): + path := strings.TrimPrefix(strings.TrimSpace(strings.TrimPrefix(l, "EnvironmentFile=")), "-") + if raw, err := os.ReadFile(path); err == nil { + for _, kv := range strings.Split(string(raw), "\n") { + kv = strings.TrimSpace(kv) + if kv == "" || strings.HasPrefix(kv, "#") { + continue + } + setPairs(kv) + } + } + case strings.HasPrefix(l, "ExecStart"): + execs = append(execs, l) + } + } + return env, execs +} + +// splitQuoted splits on blanks outside double or single quotes, keeping the quotes' contents whole. +func splitQuoted(s string) []string { + var out []string + var cur strings.Builder + var quote rune + for _, r := range s { + switch { + case quote != 0 && r == quote: + quote = 0 + case quote == 0 && (r == '"' || r == '\''): + quote = r + case quote == 0 && (r == ' ' || r == '\t'): + if cur.Len() > 0 { + out = append(out, cur.String()) + cur.Reset() + } + default: + cur.WriteRune(r) + } + } + if cur.Len() > 0 { + out = append(out, cur.String()) + } + return out +} + +// engineModule is the module whose resources may place in the engine's own trees. +const engineModule = "mesh-host" + +// passwdFile is the user database homes are read from. A variable so a test names its own. +var passwdFile = "/etc/passwd" + +// PlacementRefusedError is a resource the engine will not place, or mount, where it says. +type PlacementRefusedError struct { + Path, Why string +} + +func (e *PlacementRefusedError) Error() string { + return fmt.Sprintf("%s is not placed: %s (novox/hq issue 339); nothing was touched", e.Path, e.Why) +} + +// homeAccount is a person's or an agent's account and its home. +type homeAccount struct { + Name string + UID int +} + +// accountsOfHomes is each person's or agent's home and the account it belongs to, from the user database: an +// account with a uid of 1000 or more, or a home under /home. A variable so a test names its own. +var accountsOfHomes = func() map[string]homeAccount { + f, err := os.Open(passwdFile) + if err != nil { + return nil + } + defer f.Close() + out := map[string]homeAccount{} + sc := bufio.NewScanner(f) + for sc.Scan() { + fields := strings.Split(sc.Text(), ":") + if len(fields) < 6 { + continue + } + uid, err := strconv.Atoi(fields[2]) + if err != nil { + continue + } + home := filepath.Clean(fields[5]) + if home == "/" || home == "." || home == "" || home == "/nonexistent" { + continue + } + if (uid >= 1000 && uid != 65534) || strings.HasPrefix(home, "/home/") { + out[home] = homeAccount{Name: fields[0], UID: uid} + } + } + return out +} + +// below says whether path is strictly below dir. +func below(path, dir string) bool { + if dir == "/" { + return path != "/" + } + return strings.HasPrefix(path, dir+"/") +} + +// atOrBelow says whether path is dir or below it. +func atOrBelow(path, dir string) bool { return path == dir || below(path, dir) } + +// resolved is a path with every link in it followed, as far as the path exists, and the rest as declared. +func resolved(path string) string { + rest := "" + for p := path; ; p = filepath.Dir(p) { + if real, err := filepath.EvalSymlinks(p); err == nil { + return filepath.Clean(filepath.Join(real, rest)) + } + if filepath.Dir(p) == p { + return path + } + rest = filepath.Join(filepath.Base(p), rest) + } +} + +// ownedByAccount says whether a declared owner is that account: by name, or by its uid ("1001", "1001:1001"). +func ownedByAccount(owner string, a homeAccount) bool { + if owner == a.Name { + return true + } + user, _, _ := strings.Cut(owner, ":") + if uid, err := strconv.Atoi(user); err == nil { + return uid == a.UID + } + return false +} + +// rootOwner says whether a declared owner is root: none, "root", or uid 0. +func rootOwner(owner string) bool { + if owner == "" || owner == "root" { + return true + } + user, _, _ := strings.Cut(owner, ":") + return user == "0" +} + +// what a guarded path is, for the words of a refusal. +type placing int + +const ( + placingDirectory placing = iota + placingAccess + placingMount + placingFile +) + +// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module, +// owner a directory's declared owner. +func refusePath(path string, kind placing, module, owner string) error { + clean := filepath.Clean(path) + if !filepath.IsAbs(clean) { + return nil // the declaration refuses a relative path already; a named volume is not a path + } + for _, p := range []string{clean, resolved(clean)} { + if err := refuseOne(p, kind, module, owner); err != nil { + if p != clean { + err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why) + err.Path = clean + } + return err + } + } + return nil +} + +func refuseOne(path string, kind placing, module, owner string) *PlacementRefusedError { + for _, root := range protectedRoots { + if path == root || below(root, path) { + return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " + + "and owning or mounting it would be owning or mounting everything in it"} + } + } + trees := append([]string(nil), forbiddenBelow...) + if kind == placingMount { + // A mount is the machine's plumbing as often as a module's data — the clock, a kernel file, /dev/null + // (systemPath) — and what a setting can mount at all is a directory or an access, refused above it. + trees = append([]string(nil), forbiddenBelowMount...) + } + // The container runtimes' data, wherever the machine keeps it: every container's filesystem is there. + trees = append(trees, runtimeDataRoots()...) + for _, tree := range trees { + if atOrBelow(path, tree) { + return &PlacementRefusedError{Path: path, Why: "nothing is placed in or mounted from " + tree} + } + } + if module != engineModule { + for _, tree := range engineTrees { + if atOrBelow(path, tree) { + return &PlacementRefusedError{Path: path, Why: tree + " is the node-engine's own, placed in by " + + "its own module alone"} + } + } + } + for _, tree := range rootsOnly { + if !below(path, tree) { + continue + } + switch { + case kind == placingAccess: + return &PlacementRefusedError{Path: path, Why: "an access is the operator's data, and " + tree + + " is the machine's own"} + case kind == placingDirectory && !rootOwner(owner): + return &PlacementRefusedError{Path: path, Why: fmt.Sprintf("a directory below %s is root's, and this "+ + "one is declared %s's", tree, owner)} + } + } + ssh := false + for _, part := range strings.Split(path, "/") { + ssh = ssh || part == ".ssh" + } + if ssh && (kind == placingAccess || kind == placingMount) { + return &PlacementRefusedError{Path: path, Why: "it is an account's .ssh, which holds its keys and who may " + + "log in as it, and is never an access or a mount"} + } + if kind != placingDirectory { + return nil + } + homes := accountsOfHomes() + var deepest string + for home := range homes { + if below(path, home) && len(home) > len(deepest) { + deepest = home + } + } + if ssh && deepest == "" { + return &PlacementRefusedError{Path: path, Why: "a .ssh directory is placed only below its account's home, " + + "as that account's"} + } + if deepest != "" { + if a := homes[deepest]; !ownedByAccount(owner, a) { + if owner == "" { + owner = "root" + } + return &PlacementRefusedError{Path: path, Why: fmt.Sprintf("it is below %s's home and declared %s's; "+ + "below a home only that account's directories are placed", a.Name, owner)} + } + } + return nil +} + +// moduleOfID is the module a resource id names, or "". +func moduleOfID(id string) string { + module, _ := moduleOf(id) + return module +} + +// refusedPlaces judges every directory, access, file and archive of a declaration before anything is applied, +// and answers each refusal by path: what is refused is refused again as a container's mount source. +// +// Directories and accesses first, so a file or an archive below one of them is refused with it whatever order +// they are declared in (novox/hq issue 495): the engine refused Docker's data root as a directory and then wrote +// a file into it, because nothing judged the file's path. +func refusedPlaces(resources []declaration.Resource) map[string]error { + out := map[string]error{} + for _, r := range resources { + var err error + switch res := r.(type) { + case *declaration.Directory: + err = refusePath(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner) + // And outside every home, the walk's judgement before anything is applied (novox/hq issue 358): a + // link another account could make in its path refuses it, and with it what is below it, whatever + // order they are declared in. The apply walks again at the moment of use. A directory handed over + // keeps its own, stricter walk (issue 356), which refuses any link at all. + if err == nil && meshDirsByDescriptor && homeAbove(res.Path) == "" && + !HandedOver()[handOverKey(res.ID, res.Path)] { + if judged := judgeMeshDir(res.Path); judged != nil { + err = judged + var link *LinkInPathError + if errors.As(judged, &link) { + err = &PlacementRefusedError{Path: res.Path, Why: "it " + link.why()} + } + } + } + case *declaration.Access: + err = refusePath(res.Path, placingAccess, moduleOfID(res.ID), "") + default: + continue + } + if err != nil { + out[filepath.Clean(r.Target())] = err + } + } + trees := make(map[string]error, len(out)) + for path, err := range out { + trees[path] = err + } + for _, r := range resources { + var err error + switch res := r.(type) { + case *declaration.File: + if err = belowRefused(res.Path, trees); err == nil { + err = refusePath(res.Path, placingFile, moduleOfID(res.ID), "") + } + case *declaration.Archive: + // Only below a refused path: where an archive may unpack is otherwise the archive's own judgement + // (archive.go), and the engine's own module unpacks its versions into its own tree. + err = belowRefused(res.Path, trees) + default: + continue + } + if err != nil { + out[filepath.Clean(r.Target())] = err + } + } + return out +} + +// belowRefused says why a path below a refused directory or access is refused with it; nil when it is below +// none. Judged as declared and with its links resolved, as refusePath judges. +func belowRefused(path string, refused map[string]error) error { + clean := filepath.Clean(path) + if !filepath.IsAbs(clean) { + return nil + } + for _, p := range []string{clean, resolved(clean)} { + // The deepest refused tree it is below, so the same declaration always gives the same reason. + var tree string + for t := range refused { + if below(p, t) && len(t) > len(tree) { + tree = t + } + } + if tree == "" { + continue + } + why := refused[tree] + reason := why.Error() + var refusal *PlacementRefusedError + if errors.As(why, &refusal) { + reason = refusal.Why + } + return &PlacementRefusedError{Path: clean, Why: "it is below " + tree + ", which is refused: " + reason} + } + return nil +} + +// refuseMounts says why a container's mounts are not made; nil when they may be. +func refuseMounts(c *declaration.Container, refused map[string]error) error { + for _, v := range c.Volumes { + src := mountSource(v) + if !strings.HasPrefix(src, "/") { + continue // a named volume, which the runtime keeps in its own tree + } + src = filepath.Clean(src) + for path, why := range refused { + if atOrBelow(src, path) { + var refusal *PlacementRefusedError + if errors.As(why, &refusal) { + return &PlacementRefusedError{Path: src, Why: "it is mounted from " + path + + ", which is refused: " + refusal.Why} + } + return why + } + } + if err := refusePath(src, placingMount, moduleOfID(c.ID), ""); err != nil { + return err + } + } + return nil +} From a706310d345b63920a570b48b8e0e32d15dd7980 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 08:18:33 +0000 Subject: [PATCH 2/9] Capture the catalogue's module.json files at mesh-catalog bb0fe93b for the moved catalogue tests (issue 1379) Part 2 of 4 of go/testdata/beside; each kept as module.json.captured. --- .../artifact-store-tools/module.json.captured | 65 ++ .../asus-zephyrus-g14/module.json.captured | 229 ++++++ .../modules/audit-logger/module.json.captured | 69 ++ .../modules/avahi/module.json.captured | 43 + .../modules/baserow/module.json.captured | 158 ++++ .../modules/betterbird/module.json.captured | 253 ++++++ .../modules/blueman/module.json.captured | 37 + .../modules/bluetooth/module.json.captured | 53 ++ .../modules/ca-trust/module.json.captured | 66 ++ .../modules/claude-code/module.json.captured | 247 ++++++ .../module.json.captured | 184 +++++ .../modules/clipmenu/module.json.captured | 135 +++ .../modules/confluence/module.json.captured | 76 ++ .../modules/cups/module.json.captured | 58 ++ .../modules/dbus/module.json.captured | 67 ++ .../modules/desk-channel/module.json.captured | 106 +++ .../modules/dev-tools/module.json.captured | 49 ++ .../modules/disk-load/module.json.captured | 26 + .../modules/distribution/module.json.captured | 96 +++ .../modules/dmenu/module.json.captured | 33 + .../modules/dnsmasq/module.json.captured | 162 ++++ .../docker-compose/module.json.captured | 40 + .../modules/docker/module.json.captured | 179 ++++ .../modules/dunst/module.json.captured | 123 +++ .../modules/fail2ban/module.json.captured | 195 +++++ .../modules/feh/module.json.captured | 75 ++ .../modules/flatpak/module.json.captured | 42 + .../modules/fonts/module.json.captured | 70 ++ .../modules/forticlient/module.json.captured | 57 ++ .../modules/gitea/module.json.captured | 545 +++++++++++++ .../modules/gitlab/module.json.captured | 65 ++ .../gnome-keyring/module.json.captured | 82 ++ .../modules/grafana/module.json.captured | 225 +++++ .../modules/hello-web/module.json.captured | 82 ++ .../home-assistant/module.json.captured | 209 +++++ .../modules/hostname/module.json.captured | 61 ++ .../modules/i3/module.json.captured | 124 +++ .../i3status-rust/module.json.captured | 126 +++ .../modules/icecast/module.json.captured | 152 ++++ .../modules/influxdb/module.json.captured | 198 +++++ .../modules/invoicing/module.json.captured | 128 +++ .../modules/jira/module.json.captured | 75 ++ .../modules/kernel/module.json.captured | 78 ++ .../modules/keycloak/module.json.captured | 245 ++++++ .../modules/lab/module.json.captured | 129 +++ .../modules/lemurs/module.json.captured | 84 ++ .../modules/letta/module.json.captured | 156 ++++ .../local-model-consumer/module.json.captured | 42 + .../modules/localization/module.json.captured | 61 ++ .../modules/logrotate/module.json.captured | 58 ++ .../modules/mailu/module.json.captured | 772 ++++++++++++++++++ .../modules/marrytts/module.json.captured | 37 + .../modules/matrix/module.json.captured | 257 ++++++ .../memory-pressure/module.json.captured | 130 +++ .../modules/mesh-catalog/module.json.captured | 98 +++ .../modules/mesh-cli/module.json.captured | 37 + .../modules/mesh-config/module.json.captured | 74 ++ .../mesh-delivery-gate/module.json.captured | 110 +++ .../mesh-delivery/module.json.captured | 95 +++ .../modules/mesh-files/module.json.captured | 57 ++ .../modules/mesh-issues/module.json.captured | 330 ++++++++ .../mesh-knowledge/module.json.captured | 218 +++++ .../mesh-mail-notifier/module.json.captured | 68 ++ .../mesh-mail-web/module.json.captured | 125 +++ .../modules/mesh-mail/module.json.captured | 312 +++++++ .../module.json.captured | 60 ++ .../mesh-provisioner/module.json.captured | 56 ++ .../modules/mesh-reads/module.json.captured | 55 ++ .../modules/mesh-vault/module.json.captured | 173 ++++ .../modules/mesh-watcher/module.json.captured | 71 ++ .../modules/messenger/module.json.captured | 188 +++++ .../modules/minio/module.json.captured | 188 +++++ .../modules/model-usage/module.json.captured | 107 +++ .../modules/mongodb/module.json.captured | 170 ++++ .../modules/mosquitto/module.json.captured | 194 +++++ .../modules/mounts/module.json.captured | 160 ++++ .../modules/mssql/module.json.captured | 174 ++++ .../modules/n8n/module.json.captured | 173 ++++ .../modules/nats/module.json.captured | 171 ++++ .../modules/netcheck/module.json.captured | 57 ++ .../networkmanager/module.json.captured | 71 ++ .../nextcloud-client/module.json.captured | 38 + .../modules/nextcloud/module.json.captured | 168 ++++ .../modules/nfs-server/module.json.captured | 191 +++++ .../modules/nftables/module.json.captured | 91 +++ .../modules/nm-applet/module.json.captured | 37 + .../modules/node-env/module.json.captured | 55 ++ .../modules/nodered/module.json.captured | 297 +++++++ .../modules/ollama/module.json.captured | 62 ++ .../modules/only-office/module.json.captured | 173 ++++ .../openai-consumer/module.json.captured | 61 ++ .../modules/openrazer/module.json.captured | 52 ++ .../modules/pacman/module.json.captured | 96 +++ .../modules/photos-eef/module.json.captured | 56 ++ .../modules/photos-filip/module.json.captured | 56 ++ .../modules/picom/module.json.captured | 90 ++ .../polychromatic/module.json.captured | 40 + .../modules/postgres/module.json.captured | 191 +++++ .../modules/power/module.json.captured | 408 +++++++++ .../powerlevel10k/module.json.captured | 49 ++ .../modules/records/module.json.captured | 103 +++ .../modules/redis/module.json.captured | 154 ++++ .../modules/restic/module.json.captured | 196 +++++ .../modules/rofi/module.json.captured | 124 +++ .../route-adapter/module.json.captured | 134 +++ .../modules/screen-lock/module.json.captured | 78 ++ .../modules/searxng/module.json.captured | 229 ++++++ .../modules/sensors/module.json.captured | 69 ++ .../modules/showcase/module.json.captured | 207 +++++ .../modules/slack/module.json.captured | 41 + .../modules/snapd/module.json.captured | 31 + .../modules/ssh-client/module.json.captured | 112 +++ .../modules/sshd/module.json.captured | 54 ++ .../modules/step-ca/module.json.captured | 106 +++ .../modules/sudo/module.json.captured | 47 ++ .../modules/supabase/module.json.captured | 659 +++++++++++++++ .../systemd-networkd/module.json.captured | 65 ++ .../systemd-resolved/module.json.captured | 110 +++ .../modules/systemd/module.json.captured | 63 ++ .../modules/telegram/module.json.captured | 79 ++ .../modules/time-sync/module.json.captured | 81 ++ .../modules/triggerhappy/module.json.captured | 81 ++ .../modules/umami/module.json.captured | 142 ++++ .../modules/unifi/module.json.captured | 203 +++++ .../modules/website/module.json.captured | 60 ++ .../modules/xclip/module.json.captured | 35 + .../modules/xdg/module.json.captured | 97 +++ .../modules/xorg/module.json.captured | 134 +++ .../modules/xterm/module.json.captured | 67 ++ .../modules/zfs/module.json.captured | 235 ++++++ .../zsh-autosuggestions/module.json.captured | 30 + .../module.json.captured | 30 + .../modules/zsh/module.json.captured | 160 ++++ 133 files changed, 16933 insertions(+) create mode 100644 go/testdata/beside/mesh-catalog/modules/artifact-store-tools/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/audit-logger/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/avahi/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/baserow/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/blueman/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/bluetooth/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/ca-trust/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-licence-manager/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/confluence/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/cups/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/dbus/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/desk-channel/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/disk-load/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/distribution/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/dmenu/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/dnsmasq/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/docker-compose/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/docker/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/dunst/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/feh/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/flatpak/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/fonts/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/forticlient/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/gitlab/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/gnome-keyring/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/hello-web/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/home-assistant/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/hostname/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/icecast/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/influxdb/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/invoicing/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/jira/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/kernel/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/lab/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/lemurs/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/letta/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/local-model-consumer/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/localization/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/logrotate/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/marrytts/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/matrix/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-catalog/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-cli/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-config/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-delivery-gate/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-files/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-knowledge/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail-notifier/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-provisioner-client/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-provisioner/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-reads/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-vault/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-watcher/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/minio/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/model-usage/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mongodb/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mosquitto/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mounts/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mssql/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/n8n/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nats/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/netcheck/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/networkmanager/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nextcloud-client/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nextcloud/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nfs-server/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nftables/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nm-applet/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/node-env/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nodered/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/ollama/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/only-office/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/openai-consumer/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/openrazer/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/photos-eef/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/photos-filip/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/picom/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/polychromatic/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/postgres/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/power/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/powerlevel10k/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/records/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/redis/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/restic/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/route-adapter/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/screen-lock/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/searxng/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/sensors/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/showcase/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/slack/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/snapd/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/ssh-client/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/sshd/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/step-ca/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/sudo/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/systemd-networkd/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/systemd-resolved/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/systemd/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/telegram/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/time-sync/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/triggerhappy/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/umami/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/unifi/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/website/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/xclip/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/xdg/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/xorg/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/xterm/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/zfs/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh-autosuggestions/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh-syntax-highlighting/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/module.json.captured diff --git a/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/module.json.captured b/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/module.json.captured new file mode 100644 index 0000000..208329c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/module.json.captured @@ -0,0 +1,65 @@ +{ + "module": "artifact-store-tools", + "version": "1", + "invokes": [ + "seat:mesh-controller.artifacts", + "seat:mesh-controller.collect", + "mesh-catalog.catalog_modules", + "mesh-catalog.catalog_module" + ], + "tools": [ + "artifact_store_repositories", + "artifact_store_usage", + "artifact_store_references", + "artifact_store_collect" + ], + "config": { + "older-store-names": { + "type": "string", + "description": "The names the artifact store had before, as host names or host:port separated by spaces, or none when it had no other. artifact_store_references reads every definition under these as well as under the store's current name, so an image a definition pins under one is said to be named by a definition (novox/hq issue 323).", + "pattern": "none|[A-Za-z0-9.:-]+( [A-Za-z0-9.:-]+)*", + "default": "none" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "store-names", + "type": "file", + "path": "${dir:state}/store-names", + "mode": "0600", + "trusted": false, + "template": "files/store-names" + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/artifact-store-tools", + "binary": "artifact-store-tools", + "loads": [ + "artifact-store-tools" + ], + "env": { + "MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry", + "MESH_ARTIFACT_STORE_NAMES_FILE": "${dir:state}/store-names" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/module.json.captured b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/module.json.captured new file mode 100644 index 0000000..d1ee8d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/module.json.captured @@ -0,0 +1,229 @@ +{ + "module": "asus-zephyrus-g14", + "version": "1", + "moved": true, + "templates": "templates", + "capabilities": [ + "package-manager", + "service-manager" + ], + "requires": [ + "x11-display" + ], + "emits": [ + "profile.switched" + ], + "tools": [ + "zephyrus_brightness", + "zephyrus_battery", + "zephyrus_charge_limit", + "zephyrus_gpu_mode", + "zephyrus_profile", + "zephyrus_thermals", + "zephyrus_power_draw", + "zephyrus_profile_policy", + "zephyrus_fan_curves", + "zephyrus_keys", + "zephyrus_check" + ], + "resources": [ + { + "id": "asusctl", + "type": "package", + "package": "asusctl" + }, + { + "id": "playerctl", + "type": "package", + "package": "playerctl" + }, + { + "id": "asusd", + "type": "service", + "unit": "asusd.service", + "state": "running" + }, + { + "id": "supergfxd", + "type": "service", + "unit": "supergfxd.service", + "state": "running", + "boot": "enabled" + }, + { + "id": "scripts", + "type": "archive", + "path": "/usr/local/lib/asus-zephyrus-g14", + "artifact": "scripts", + "unknown": "program" + }, + { + "id": "nvidia-options", + "type": "file", + "path": "/etc/modprobe.d/g14-nvidia-power.conf", + "mode": "0644", + "template": "templates/modprobe.d/g14-nvidia-power.conf" + }, + { + "id": "video-options", + "type": "file", + "path": "/etc/modprobe.d/video-brightness-switch.conf", + "mode": "0644", + "template": "templates/modprobe.d/video-brightness-switch.conf" + }, + { + "id": "suspend-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/systemd-suspend.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "nvidia-on-suspend", + "type": "file", + "path": "/etc/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf", + "mode": "0644", + "template": "templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf" + }, + { + "id": "hibernate-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/systemd-hibernate.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "nvidia-on-hibernate", + "type": "file", + "path": "/etc/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf", + "mode": "0644", + "template": "templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf" + }, + { + "id": "suspend-then-hibernate-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "nvidia-on-suspend-then-hibernate", + "type": "file", + "path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf", + "mode": "0644", + "template": "templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf" + }, + { + "id": "powerd-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/nvidia-powerd.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "powerd-opt-in", + "type": "file", + "path": "/etc/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf", + "mode": "0644", + "template": "templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf" + }, + { + "id": "backlight-rule", + "type": "file", + "path": "/etc/udev/rules.d/90-backlight.rules", + "mode": "0644", + "template": "templates/udev/rules.d/90-backlight.rules" + }, + { + "id": "udev", + "type": "service", + "unit": "systemd-udevd.service", + "reload-on": [ + "backlight-rule" + ] + }, + { + "id": "upower-package", + "type": "package", + "package": "upower" + }, + { + "id": "upower-drop-ins", + "type": "directory", + "path": "/etc/UPower/UPower.conf.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "low-battery", + "type": "file", + "path": "/etc/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf", + "mode": "0644", + "template": "templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf" + }, + { + "id": "upower", + "type": "service", + "unit": "upower.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "low-battery" + ] + }, + { + "id": "xorg-drop-ins", + "type": "directory", + "path": "/etc/X11/xorg.conf.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "touchpad", + "type": "file", + "path": "/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf", + "mode": "0644", + "template": "templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf" + } + ], + "items": [ + { + "collection": "shell:after-wake:normal", + "template": "templates/after-wake" + }, + { + "collection": "contribution:node-hotkeys:trigger", + "template": "templates/hotkeys-triggers" + }, + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "templates/display-session" + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/zephyrus", + "binary": "zephyrus", + "loads": [ + "zephyrus" + ] + }, + { + "name": "scripts", + "kind": "archive", + "from": "files" + }, + { + "name": "templates", + "kind": "archive", + "from": "templates" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/audit-logger/module.json.captured b/go/testdata/beside/mesh-catalog/modules/audit-logger/module.json.captured new file mode 100644 index 0000000..122b94e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/audit-logger/module.json.captured @@ -0,0 +1,69 @@ +{ + "module": "audit-logger", + "version": "1", + "slug": "audit", + "consumes": [ + "**" + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "AUDIT_LOG": "${dir:trail}/audit.log", + "AUDIT_SPOOL": "${dir:spool}" + } + } + ] + }, + "data": { + "own": [ + { + "id": "trail", + "path": "${dir:trail}", + "class": "valuable", + "why": "the audit trail, written nowhere else" + }, + { + "id": "spool", + "path": "${dir:spool}", + "class": "valuable", + "why": "events the trail could not take yet; after the bus gives one up, the only copy" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "trail", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "spool", + "type": "directory", + "mode": "0700", + "unknown": "program" + } + ], + "capabilities": [ + "container-runtime" + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/avahi/module.json.captured b/go/testdata/beside/mesh-catalog/modules/avahi/module.json.captured new file mode 100644 index 0000000..de99c7c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/avahi/module.json.captured @@ -0,0 +1,43 @@ +{ + "module": "avahi", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "avahi_status", + "avahi_browse", + "avahi_resolve", + "avahi_services" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "avahi" + }, + { + "id": "daemon", + "type": "service", + "unit": "avahi-daemon.service", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/avahi-tools", + "binary": "avahi-tools", + "loads": [ + "avahi-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/baserow/module.json.captured b/go/testdata/beside/mesh-catalog/modules/baserow/module.json.captured new file mode 100644 index 0000000..8119621 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/baserow/module.json.captured @@ -0,0 +1,158 @@ +{ + "module": "baserow", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database", + "route" + ], + "contributes": { + "postgres-database": { + "name": "baserow" + }, + "route": { + "label": "baserow", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "own-secrets": { + "admin": "${dir:state}/admin.secret" + }, + "listens": [ + { + "name": "web", + "port": 80, + "protocol": "tcp", + "from": "mesh", + "why": "the Baserow web UI and REST API, served by the image's own Caddy; a public name is the route's" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "uploaded files and media; the tables are in the database" + } + ] + }, + "config": { + "host": { + "type": "string", + "description": "The Host header the module's tools send to Baserow, a host name; the file's default is the name the module's route was given.", + "trusted": true, + "default": "${bound:route:name}" + }, + "email": { + "type": "string", + "description": "The email address of the Baserow account the module's tools sign in as; unset, the code falls back to its environment.", + "trusted": true, + "default": "" + }, + "url": { + "type": "string", + "description": "The address the module's tools reach Baserow at, as a URL; unset, the code falls back to its environment, else this machine's Baserow on port 80.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0755", + "owner": "9999:9999", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "net", + "type": "network", + "name": "baserow" + }, + { + "id": "server", + "type": "container", + "name": "baserow", + "image": "baserow/baserow@sha256:263ea6c4b72c9eccabcd975ffe9fdebf23913a293a514bec6a3897a5e0a5a080", + "health": { + "kind": "runtime" + }, + "network": "baserow", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "80" + ], + "volumes": [ + "${dir:data}:/baserow/data", + "${dir:state}/database.secret:/run/secrets/database:ro" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["host", "email", "url"], + "protected": ["password"] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_BASEROW_URL": "http://127.0.0.1:${port:80}", + "MESH_BASEROW_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/module.json.captured b/go/testdata/beside/mesh-catalog/modules/betterbird/module.json.captured new file mode 100644 index 0000000..891f883 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/module.json.captured @@ -0,0 +1,253 @@ +{ + "module": "betterbird", + "version": "1", + "upgrade": { + "policy": "record", + "why": "a new build may remove the community package betterbird-bin and replaces the installation in /opt/betterbird, which a running Betterbird has open: the switch on each node and every upgrade wait for the operator, who quits Betterbird first and then pushes (novox/hq ADR 0316 §4, §5)" + }, + "requires": [ + "x11-display" + ], + "invokes": [ + "seat:mesh-controller.settings", + "seat:mesh-controller.nodes", + "seat:mesh-controller.node", + "betterbird.betterbird_derive" + ], + "tools": [ + "betterbird_status", + "betterbird_prefs", + "betterbird_config", + "betterbird_config_apply", + "betterbird_user_js", + "betterbird_log", + "betterbird_restart", + "betterbird_check", + "betterbird_accounts", + "betterbird_identities", + "betterbird_calendars", + "betterbird_address_books", + "betterbird_filters", + "betterbird_tags", + "betterbird_derive", + "betterbird_drift", + "betterbird_reminder_test" + ], + "config": { + "prefs": { + "type": "object", + "description": "Betterbird's preferences the operator sets, by their own names (mailnews.*, calendar.* and the like), laid into prefs.json; an account's, a server's and a tag's are refused, as accounts, the four kinds and tags own them (novox/hq ADR 0329 §6).", + "trusted": true, + "default": {} + }, + "accounts": { + "type": "object", + "description": "The mail accounts Betterbird is set up with, each by a name of the operator's: its address, its sender's name and its incoming and outgoing servers; no password, token or cookie (novox/hq ADR 0329 §2).", + "trusted": true, + "default": {} + }, + "identities": { + "type": "object", + "description": "The identities beside an account's own, each by a name of the operator's: the address and name it sends as, its signature as a list of lines, and the profile's key to adopt on a node.", + "trusted": true, + "default": {} + }, + "calendars": { + "type": "object", + "description": "The calendars Betterbird shows, each by a name of the operator's: where it is and how it is reached, and the profile's key to adopt on a node.", + "trusted": true, + "default": {} + }, + "address-books": { + "type": "object", + "description": "The address books Betterbird uses, each by a name of the operator's: where it is and how it is reached, and the profile's key to adopt on a node.", + "trusted": true, + "default": {} + }, + "filters": { + "type": "object", + "description": "The message filters of each account, by account: what each matches and what it does with the mail.", + "trusted": true, + "default": {} + }, + "tags": { + "type": "object", + "description": "The tags Betterbird offers, each by its own key: its name, colour and order; trusted since issue 751, so a change to it is the operator's to approve.", + "trusted": true, + "default": {} + } + }, + "resources": [ + { + "id": "configuration-dir", + "type": "directory", + "path": "${machine:account-home}/.config/betterbird", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out", + "knows": ["accounts-record.json", "configuration-record.json", "kept/"] + }, + { + "id": "prefs", + "type": "file", + "path": "${machine:account-home}/.config/betterbird/prefs.json", + "owner": "${machine:account}", + "mode": "0644", + "merge": "json", + "takes-config": [ + "prefs" + ], + "template": "files/prefs.json", + "trusted": true + }, + { + "id": "accounts", + "type": "file", + "path": "${machine:account-home}/.config/betterbird/accounts.json", + "owner": "${machine:account}", + "mode": "0600", + "merge": "json", + "takes-config": [ + "accounts" + ], + "template": "files/accounts.json", + "trusted": true + }, + { + "id": "configuration", + "type": "file", + "path": "${machine:account-home}/.config/betterbird/configuration.json", + "owner": "${machine:account}", + "mode": "0600", + "merge": "json", + "takes-config": [ + "identities", + "calendars", + "address-books", + "filters" + ], + "template": "files/configuration.json", + "trusted": true + }, + { + "id": "tags", + "type": "file", + "path": "${machine:account-home}/.config/betterbird/tags.json", + "owner": "${machine:account}", + "mode": "0644", + "merge": "json", + "takes-config": [ + "tags" + ], + "template": "files/tags.json", + "trusted": true + }, + { + "id": "community-package", + "type": "package", + "package": "betterbird-bin", + "absent": true + }, + { + "id": "installation", + "type": "archive", + "path": "/opt/betterbird", + "artifact": "archive", + "unknown": "program" + }, + { + "id": "owl", + "type": "archive", + "path": "/opt/betterbird/owl", + "artifact": "owl", + "unknown": "program" + }, + { + "id": "policies", + "type": "archive", + "path": "/opt/betterbird/distribution", + "artifact": "distribution", + "unknown": "program" + }, + { + "id": "spelling", + "type": "archive", + "path": "/opt/betterbird/defaults/pref", + "artifact": "defaults", + "unknown": "program" + }, + { + "id": "preparation", + "type": "archive", + "path": "/usr/local/lib/mesh-betterbird", + "artifact": "tools", + "unknown": "program" + }, + { + "id": "start", + "type": "file", + "path": "/usr/local/bin/betterbird", + "mode": "0755", + "template": "files/betterbird" + }, + { + "id": "launcher", + "type": "file", + "path": "/usr/local/share/applications/eu.betterbird.Betterbird.desktop", + "mode": "0644", + "template": "files/eu.betterbird.Betterbird.desktop" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/betterbird-tools", + "binary": "betterbird-tools", + "loads": [ + "betterbird-tools" + ] + }, + { + "name": "archive", + "kind": "archive", + "url": "https://www.betterbird.eu/downloads/LinuxArchive/betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz", + "sha256": "38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2", + "version": "153.4.0esr-bb10", + "within": "betterbird", + "names-on-purpose": { + "www.betterbird.eu": "Betterbird's project publishes its Linux release there; the build seat fetches it once, by its pinned sha256 (novox/hq ADR 0316 §2)" + } + }, + { + "name": "owl", + "kind": "archive", + "url": "https://www.beonex.com/owl/owl.xpi", + "sha256": "a54cc3d52113b34f97942ee19190947dee23561e2217f18db3d80ac023991221", + "version": "1.5.6.1", + "file": "owl.xpi", + "names-on-purpose": { + "www.beonex.com": "Owl's vendor publishes its xpi there; the build seat fetches it once, by its pinned sha256 (novox/hq ADR 0316 §6)" + } + }, + { + "name": "distribution", + "kind": "archive", + "from": "install/distribution" + }, + { + "name": "defaults", + "kind": "archive", + "from": "install/defaults-pref" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/blueman/module.json.captured b/go/testdata/beside/mesh-catalog/modules/blueman/module.json.captured new file mode 100644 index 0000000..73a7640 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/blueman/module.json.captured @@ -0,0 +1,37 @@ +{ + "module": "blueman", + "version": "1", + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "tools": [ + "blueman_status", + "blueman_restart", + "blueman_check" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "blueman" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/blueman-tools", + "binary": "blueman-tools", + "loads": [ + "blueman-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/bluetooth/module.json.captured b/go/testdata/beside/mesh-catalog/modules/bluetooth/module.json.captured new file mode 100644 index 0000000..dfaabf1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/bluetooth/module.json.captured @@ -0,0 +1,53 @@ +{ + "module": "bluetooth", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "bluetooth_controller", + "bluetooth_power", + "bluetooth_devices", + "bluetooth_scan", + "bluetooth_connect", + "bluetooth_disconnect", + "bluetooth_trust", + "bluetooth_pair", + "bluetooth_remove" + ], + "resources": [ + { + "id": "stack", + "type": "package", + "package": "bluez" + }, + { + "id": "utilities", + "type": "package", + "package": "bluez-utils" + }, + { + "id": "daemon", + "type": "service", + "unit": "bluetooth.service", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/bluetooth-tools", + "binary": "bluetooth-tools", + "loads": [ + "bluetooth-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/ca-trust/module.json.captured b/go/testdata/beside/mesh-catalog/modules/ca-trust/module.json.captured new file mode 100644 index 0000000..b3c9dc9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ca-trust/module.json.captured @@ -0,0 +1,66 @@ +{ + "module": "ca-trust", + "version": "1", + "slug": "catrust", + "capabilities": [ + "service-manager" + ], + "requires": [ + "internal-acme-ca" + ], + "seats": [ + { + "name": "the-mesh-trust-anchor", + "scope": "node" + } + ], + "claims": [ + { + "name": "the-mesh-trust-anchor", + "scope": "node" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "anchor", + "type": "file", + "path": "${dir:state}/anchor", + "mode": "0755", + "template": "files/anchor" + }, + { + "id": "unit", + "type": "file", + "path": "/etc/systemd/system/mesh-ca-trust.service", + "mode": "0644", + "template": "files/mesh-ca-trust.service" + }, + { + "id": "trust", + "type": "service", + "unit": "mesh-ca-trust.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "anchor", + "unit" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/module.json.captured b/go/testdata/beside/mesh-catalog/modules/claude-code/module.json.captured new file mode 100644 index 0000000..b869d01 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/module.json.captured @@ -0,0 +1,247 @@ +{ + "module": "claude-code", + "version": "1", + "moved": true, + "slug": "agent", + "capabilities": [ + "package-manager" + ], + "requires": [ + "mcp-endpoint", + { + "name": "gitea-read", + "optional": true + } + ], + "binds": { + "mcp-endpoint": "${dir:state}/mcp-endpoint.json" + }, + "secrets": { + "gitea-read": "${dir:state}/gitea-read.secret" + }, + "uses": [ + "operator-channel" + ], + "state": [ + "servers", + "holdings", + "config", + "proposals", + "questions", + "memory" + ], + "reads": [ + "claude-licence-manager.bindings" + ], + "tools": [ + "claude_code_status", + "claude_code_render", + "claude_code_guard", + "claude_code_pull", + "claude_code_grant", + "claude_code_add_api_key", + "claude_code_registrations", + "claude_code_memory", + "claude_code_mcp_list", + "claude_code_mcp_register", + "claude_code_mcp_unregister", + "claude_code_skill_list", + "claude_code_skill_register", + "claude_code_skill_unregister", + "claude_code_agent_list", + "claude_code_agent_register", + "claude_code_agent_unregister", + "claude_code_command_list", + "claude_code_command_register", + "claude_code_command_unregister", + "claude_code_hook_list", + "claude_code_hook_register", + "claude_code_hook_unregister", + "claude_code_output_style_list", + "claude_code_output_style_register", + "claude_code_output_style_unregister", + "claude_code_instructions_list", + "claude_code_instructions_register", + "claude_code_instructions_unregister", + "claude_code_instructions_propose", + "claude_code_proposals", + "claude_code_ask", + "claude_code_answer", + "claude_code_settings_get", + "claude_code_settings_set", + "claude_code_settings_clear", + "claude_code_permission_add", + "claude_code_permission_remove", + "claude_code_config_list", + "claude_code_config_show", + "claude_code_config_status", + "claude_code_config_import", + "claude_code_home_show", + "claude_code_home_remove", + "claude_code_home_removed", + "claude_code_home_restore", + "claude_code_judge", + "claude_code_judged" + ], + "data": { + "own": [ + { + "id": "agent-home", + "path": "${dir:agent-home}", + "class": "valuable", + "why": "the operator's agent's own files: its memory, history and projects" + } + ] + }, + "config": { + "role": { + "type": "string", + "description": "What this machine is for, in a sentence, written into the instructions every Claude Code session on it reads; empty says nothing.", + "trusted": true, + "default": "" + }, + "mcp_servers": { + "type": "object", + "description": "The tool servers every session on this machine is given beside the mesh's own, by name, each as Claude Code's managed MCP configuration takes it; empty adds none.", + "trusted": true, + "default": {} + }, + "managed_settings": { + "type": "object", + "description": "Keys of Claude Code's managed settings every session on this machine obeys, such as spinnerTipsEnabled, laid over the mesh's own; empty adds none. Falls back to agent-managed-settings, the mesh's or this node's.", + "trusted": true, + "predefined": "agent-managed-settings", + "default": {} + }, + "instructions": { + "type": "object", + "description": "The instructions every session on this machine reads, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "output_styles": { + "type": "object", + "description": "The output styles offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "skills": { + "type": "object", + "description": "The skills offered to every session on this machine, by name, each its files by path, a text as one line or a list of its lines; a skill without its SKILL.md is left out; empty adds none.", + "trusted": true, + "default": {} + }, + "commands": { + "type": "object", + "description": "The slash commands offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "agents": { + "type": "object", + "description": "The agents offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "claude-code" + }, + { + "id": "managed", + "type": "directory", + "path": "/etc/claude-code", + "mode": "0755", + "unknown": "program" + }, + { + "id": "start", + "type": "file", + "path": "/usr/local/bin/claude-agent", + "mode": "0755", + "template": "files/claude-agent" + }, + { + "id": "agent-account-name", + "type": "file", + "path": "/etc/claude-code/agent-account", + "mode": "0644", + "template": "files/agent-account" + }, + { + "id": "agent-account", + "type": "user", + "name": "${machine:agent-account}", + "home": "${machine:agent-home}", + "root": "${machine:agent-root}" + }, + { + "id": "agent-home", + "type": "directory", + "path": "${machine:agent-home}/.claude", + "mode": "0700", + "owner": "${machine:agent-account}", + "unknown": "module" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "${machine:account}", + "place": ".", + "unknown": "program" + }, + { + "id": "facts", + "type": "file", + "path": "${dir:state}/facts.json", + "mode": "0600", + "owner": "${machine:account}", + "template": "files/facts.json" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "owner": "${machine:account}", + "takes-config": ["role", "mcp_servers", "managed_settings", "instructions", "output_styles", "skills", "commands", "agents"], + "merge": "json", + "template": "files/settings.json" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/claude-code", + "binary": "claude-code", + "loads": [ + "claude-code" + ], + "env": { + "MESH_CLAUDE_CODE_STATE": "${dir:state}", + "MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json", + "MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/claude-licence-manager/module.json.captured b/go/testdata/beside/mesh-catalog/modules/claude-licence-manager/module.json.captured new file mode 100644 index 0000000..a55da31 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-licence-manager/module.json.captured @@ -0,0 +1,184 @@ +{ + "module": "claude-licence-manager", + "version": "1", + "slug": "licmgr", + "requires": [ + "postgres-database", + "secret" + ], + "contributes": { + "postgres-database": { + "name": "claude_licences", + "readable": [ + "audit", + "binding" + ] + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "secret": { + "grant-key": "${dir:state}/grant.key" + } + }, + "seats": [ + { + "name": "anthropic-licence-manager", + "scope": "mesh", + "serves": [ + "licences", + "bindings", + "bind", + "switch", + "release", + "refresh", + "usage", + "adopt", + "current", + "public-key" + ] + } + ], + "claims": [ + { + "name": "anthropic-licence-manager", + "scope": "mesh", + "serves": [ + "licences", + "bindings", + "bind", + "switch", + "release", + "refresh", + "usage", + "adopt", + "current", + "public-key" + ] + } + ], + "emits": [ + "licence.adopted", + "licence.refused", + "licence.bound", + "licence.switched", + "licence.released", + "licence.rotated", + "licence.failing", + "usage.read" + ], + "state": [ + "bindings" + ], + "reads": [ + "claude-code.holdings" + ], + "config": { + "cadence_minutes": { + "type": "number", + "description": "How old a licence's grant may grow before it is rotated, in minutes; 0 keeps 240.", + "minimum": 0, + "trusted": true, + "default": 240 + }, + "floor_minutes": { + "type": "number", + "description": "How little time a grant may have left before it is refreshed in any case, in minutes; 0 keeps 60.", + "minimum": 0, + "trusted": true, + "default": 60 + }, + "failures_to_notify": { + "type": "number", + "description": "How many refreshes of a licence fail in a row before the operator is told, a count; 0 keeps 3.", + "minimum": 0, + "trusted": true, + "default": 3 + }, + "cooldown_hours": { + "type": "number", + "description": "How long after telling the operator about a licence nothing more is said about it, in hours; 0 keeps 24.", + "minimum": 0, + "trusted": true, + "default": 24 + }, + "refresh_warn_days": { + "type": "number", + "description": "How long before a licence's refresh token itself expires the operator is warned, in days; 0 keeps 3.", + "minimum": 0, + "trusted": true, + "default": 3 + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "template": "files/database.url" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "takes-config": ["cadence_minutes", "floor_minutes", "failures_to_notify", "cooldown_hours", "refresh_warn_days"], + "merge": "json", + "template": "files/settings.json" + }, + { + "id": "prepare", + "type": "process", + "name": "claude-licence-manager-prepare", + "artifact": "code", + "run": [ + "./claude-licence-manager", + "prepare" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/claude-licence-manager", + "binary": "claude-licence-manager", + "loads": [ + "claude-licence-manager" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_LICENCE_STATE": "${dir:state}", + "MESH_LICENCE_KEY_FILE": "${dir:state}/grant.key", + "MESH_LICENCE_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/module.json.captured b/go/testdata/beside/mesh-catalog/modules/clipmenu/module.json.captured new file mode 100644 index 0000000..30ec9c0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/module.json.captured @@ -0,0 +1,135 @@ +{ + "module": "clipmenu", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-clipboard", + "scope": "node", + "serves": [ + "history", + "copy" + ] + } + ], + "tools": [ + "clipmenu_paste", + "clipmenu_clear", + "clipmenu_delete" + ], + "items": [ + { + "collection": "environment", + "name": "cm-selections", + "attributes": { + "kind": "variable", + "name": "CM_SELECTIONS" + }, + "template": "files/env-cm-selections" + }, + { + "collection": "environment", + "name": "cm-max-clips", + "attributes": { + "kind": "variable", + "name": "CM_MAX_CLIPS" + }, + "template": "files/env-cm-max-clips" + }, + { + "collection": "environment", + "name": "cm-histlength", + "attributes": { + "kind": "variable", + "name": "CM_HISTLENGTH" + }, + "template": "files/env-cm-histlength" + }, + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/i3/50-clipmenu.conf" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "clipmenu" + }, + { + "id": "greenclip", + "type": "package", + "package": "rofi-greenclip", + "absent": true + }, + { + "id": "text-only", + "type": "file", + "path": "/usr/local/lib/mesh-clipmenu/xsel", + "mode": "0755", + "template": "files/bin/xsel" + }, + { + "id": "daemon-dropins", + "type": "directory", + "path": "${machine:account-home}/.config/systemd/user/clipmenud.service.d", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "daemon-dropin", + "type": "file", + "path": "${machine:account-home}/.config/systemd/user/clipmenud.service.d/mesh.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/systemd/clipmenud-mesh.conf" + }, + { + "id": "launcher", + "type": "file", + "path": "/usr/local/lib/mesh-clipmenu/clipmenud-session", + "mode": "0755", + "template": "files/bin/clipmenud-session" + }, + { + "id": "daemon", + "type": "service", + "unit": "clipmenud.service", + "scope": "user", + "user": "${machine:account}", + "state": "running", + "boot": "enabled", + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/clipmenu-tools", + "binary": "clipmenu-tools", + "loads": [ + "clipmenu-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/confluence/module.json.captured b/go/testdata/beside/mesh-catalog/modules/confluence/module.json.captured new file mode 100644 index 0000000..caa0d1b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/confluence/module.json.captured @@ -0,0 +1,76 @@ +{ + "module": "confluence", + "version": "1", + "slug": "confl", + "own-secrets": { + "token": "${dir:state}/token" + }, + "config": { + "atlassian-url": { + "type": "string", + "description": "The address of the operator's Atlassian site the confluence tools call, as a URL such as https://example.atlassian.net, laid into the file as ATLASSIAN_URL; unset, the code falls back to its environment and answers no calls until it is given.", + "trusted": true, + "default": "" + }, + "atlassian-email": { + "type": "string", + "description": "The email address of the Atlassian account the confluence tools sign in as with its API token, laid into the file as ATLASSIAN_EMAIL; unset, the code falls back to its environment and answers no calls until it is given.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:state}/config.json", + "merge": "json", + "template": "files/config.json", + "mode": "0600", + "takes-config": [ + { + "key": "atlassian-url", + "as": "ATLASSIAN_URL" + }, + { + "key": "atlassian-email", + "as": "ATLASSIAN_EMAIL" + } + ] + } + ], + "capabilities": [ + "container-runtime" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_CONFLUENCE_TOKEN_FILE": "${dir:state}/token", + "MESH_CONFLUENCE_CONFIG_FILE": "${dir:state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/cups/module.json.captured b/go/testdata/beside/mesh-catalog/modules/cups/module.json.captured new file mode 100644 index 0000000..01a4822 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/cups/module.json.captured @@ -0,0 +1,58 @@ +{ + "module": "cups", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "cups_printers", + "cups_queue", + "cups_cancel", + "cups_print", + "cups_default", + "cups_resume", + "cups_drivers" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "cups" + }, + { + "id": "driverless", + "type": "package", + "package": "cups-filters" + }, + { + "id": "socket", + "type": "service", + "unit": "cups.socket", + "state": "running", + "boot": "enabled" + }, + { + "id": "scheduler", + "type": "service", + "unit": "cups.service", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/cups-tools", + "binary": "cups-tools", + "loads": [ + "cups-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/dbus/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dbus/module.json.captured new file mode 100644 index 0000000..c0e28b3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dbus/module.json.captured @@ -0,0 +1,67 @@ +{ + "module": "dbus", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "claims": [ + { + "name": "node-message-bus", + "scope": "node" + } + ], + "emits": [ + "bus.stalled", + "bus.recovered", + "bus.restarted", + "service.appeared", + "service.left", + "policy.denied" + ], + "tools": [ + "dbus_names", + "dbus_introspect", + "dbus_monitor", + "dbus_check", + "dbus_health" + ], + "resources": [ + { + "id": "dbus", + "type": "package", + "package": "dbus" + }, + { + "id": "broker", + "type": "package", + "package": "dbus-broker" + }, + { + "id": "broker-units", + "type": "package", + "package": "dbus-broker-units" + }, + { + "id": "system-bus", + "type": "service", + "unit": "dbus-broker.service", + "state": "running" + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/dbus-tools", + "binary": "dbus-tools", + "loads": [ + "dbus-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/desk-channel/module.json.captured b/go/testdata/beside/mesh-catalog/modules/desk-channel/module.json.captured new file mode 100644 index 0000000..115ce62 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/desk-channel/module.json.captured @@ -0,0 +1,106 @@ +{ + "module": "desk-channel", + "version": "1", + "slug": "desk", + "runs-as": "desk-channel", + "claims": [ + { + "name": "channel", + "scope": "mesh", + "kind": "desktop", + "capabilities": [ + "deliver", + "silent", + "loud", + "edit", + "private", + "choice", + "exact-render" + ] + }, + { + "name": "intake", + "scope": "mesh", + "kind": "desktop" + } + ], + "consumes": [ + "dunst.action-chosen", + "dunst.started", + "dunst.paused" + ], + "invokes": [ + "seat:node-notifier.send" + ], + "state": [ + { + "name": "shown", + "ttl-seconds": 2592000 + } + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "desk-channel", + "tools": [ + "desk_channel_status" + ], + "config": { + "desktop-machines": { + "type": "list", + "description": "The machines, by node name, whose desktop shows the operator's questions and notifications, in the order they are tried; empty shows them on none.", + "trusted": true, + "default": [] + } + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "desk-channel", + "shell": "/usr/bin/nologin", + "home": "/var/lib/desk-channel" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "desk-channel", + "place": ".", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "owner": "desk-channel", + "takes-config": ["desktop-machines"], + "merge": "json", + "template": "files/settings.json" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/desk-channel", + "binary": "desk-channel", + "loads": [ + "desk-channel" + ], + "env": { + "MESH_DESK_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured new file mode 100644 index 0000000..8ee6fd1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured @@ -0,0 +1,49 @@ +{ + "module": "dev-tools", + "version": "1", + "capabilities": [ + "package-manager" + ], + "resources": [ + { + "id": "go", + "type": "package", + "package": "go" + }, + { + "id": "c-compiler", + "type": "package", + "package": "gcc" + }, + { + "id": "make", + "type": "package", + "package": "make" + }, + { + "id": "nodejs", + "type": "package", + "package": "nodejs" + }, + { + "id": "npm", + "type": "package", + "package": "npm" + }, + { + "id": "python", + "type": "package", + "package": "python" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "jq", + "type": "package", + "package": "jq" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/disk-load/module.json.captured b/go/testdata/beside/mesh-catalog/modules/disk-load/module.json.captured new file mode 100644 index 0000000..e50253e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/disk-load/module.json.captured @@ -0,0 +1,26 @@ +{ + "module": "disk-load", + "version": "1", + "tools": [ + "disk_load", + "disk_top", + "disk_filesystems", + "disk_devices", + "cpu_top" + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/disk-load", + "binary": "disk-load", + "loads": [ + "disk-load" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/distribution/module.json.captured b/go/testdata/beside/mesh-catalog/modules/distribution/module.json.captured new file mode 100644 index 0000000..eb9f7c2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/distribution/module.json.captured @@ -0,0 +1,96 @@ +{ + "module": "distribution", + "version": "1", + "provides": [ + { + "name": "artifact-store", + "scope": "mesh" + } + ], + "claims": [ + { + "name": "mesh-artifact-store", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "broker": "/var/lib/mesh/registry/broker" + }, + "serves": { + "artifact-store": { + "port": 5000 + } + }, + "listens": [ + { + "name": "registry", + "port": 5000, + "protocol": "tcp", + "from": "mesh", + "why": "every machine pulls images and artifacts from here" + } + ], + "data": { + "own": [ + { + "id": "registry", + "path": "${dir:registry-data}", + "class": "rebuildable", + "backup": "none", + "measure": "shallow", + "why": "the artifact store: every image is built again from its source, and too large to copy every night (ADR 0214 left it out)" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/mesh/registry", + "mode": "0700", + "unknown": "program" + }, + { + "id": "registry-data", + "type": "directory", + "path": "/var/lib/mesh-registry", + "mode": "0700", + "unknown": "program" + }, + { + "id": "store", + "type": "container", + "name": "mesh-registry", + "image": "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373", + "ports": [ + "5000:5000" + ], + "volumes": [ + "/var/lib/mesh-registry:/var/lib/registry" + ], + "env": { + "REGISTRY_STORAGE_DELETE_ENABLED": "true" + } + }, + { + "id": "collect", + "type": "container", + "name": "mesh-registry-collect", + "image": "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373", + "volumes": [ + "/var/lib/mesh-registry:/var/lib/registry" + ], + "args": [ + "garbage-collect", + "/etc/docker/registry/config.yml" + ], + "schedule": "30 3 * * *", + "while-stopped": [ + "store" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/dmenu/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dmenu/module.json.captured new file mode 100644 index 0000000..aeeb3ce --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dmenu/module.json.captured @@ -0,0 +1,33 @@ +{ + "module": "dmenu", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "dmenu_menu", + "dmenu_session" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "dmenu" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/dmenu-tools", + "binary": "dmenu-tools", + "loads": [ + "dmenu-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/dnsmasq/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dnsmasq/module.json.captured new file mode 100644 index 0000000..58cc0da --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dnsmasq/module.json.captured @@ -0,0 +1,162 @@ +{ + "module": "dnsmasq", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the mesh's resolver: a build that breaks it can stop a machine resolving the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236, issue 260)" + }, + "provides": [ + { + "name": "wildcard-resolution", + "scope": "mesh", + "identity": false + } + ], + "requires": [ + "mesh-addressing" + ], + "emits": [ + "name.added", + "name.removed", + "restarted", + "restart-judged" + ], + "own-secrets": { + "broker": "${dir:mesh-state}/broker" + }, + "claims": [ + { + "name": "mesh-dns-resolver", + "scope": "mesh" + } + ], + "tools": [ + "dnsmasq_health", + "dnsmasq_names", + "dnsmasq_resolve" + ], + "listens": [ + { + "name": "dns-udp", + "port": 53, + "protocol": "udp", + "from": "mesh", + "why": "one of the mesh's resolvers (ADR 0194, 0223): every node's and every container's resolver, beside any other holder of the seat — the mesh's own names answered here, a module's zone forwarded to it, the rest forwarded upstream", + "fixed": true + }, + { + "name": "dns-tcp", + "port": 53, + "protocol": "tcp", + "from": "mesh", + "why": "the same names over tcp, which a resolver answers on as well and is asked for whenever an answer will not fit in a datagram. Declared because the daemon serves it: a declaration that covers one of the two protocols its own service listens on leaves the other closed while everything reports success", + "fixed": true + } + ], + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "package", + "type": "package", + "package": "dnsmasq" + }, + { + "id": "config", + "type": "file", + "path": "/etc/dnsmasq.conf", + "mode": "0644", + "template": "files/dnsmasq.conf" + }, + { + "id": "drop-ins", + "type": "directory", + "path": "/etc/systemd/system/dnsmasq.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "wait-for-address", + "type": "file", + "path": "/etc/systemd/system/dnsmasq.service.d/mesh.conf", + "mode": "0644", + "template": "files/mesh.conf" + }, + { + "id": "service", + "type": "service", + "unit": "dnsmasq.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "config", + "wait-for-address", + "dnsmasq.fact-node-zones", + "dnsmasq.fact-zones" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "watch-dir", + "type": "directory", + "path": "/var/lib/dnsmasq-watch", + "mode": "0755", + "unknown": "program" + }, + { + "id": "watch", + "type": "process", + "name": "dnsmasq-watch", + "artifact": "tools", + "run": [ + "./dnsmasq-tools", + "watch" + ], + "health": { + "kind": "tool", + "tool": "dnsmasq_health", + "interval": "20s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + } + } + ], + "facts": { + "node-zones": { + "path": "/etc/mesh-resolver/nodes.conf", + "template": "# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n# joins or leaves, and an edit would survive until then and vanish.\n\nlocal=/{{.Suffix}}/\n# A machine's own name is a host record as well as a wildcard: asked for an IPv6 address, the\n# resolver then says the name exists and has none, where the wildcard alone said there is no such\n# name — and a resolver that reads that as final (musl, so every Alpine container) failed to find\n# the machine at all (novox/hq issue 262).\n{{range .Machines}}address=/{{.FQDN}}/{{.Address}}\nhost-record={{.FQDN}},{{.Address}}\n{{end}}" + }, + "zones": { + "path": "/etc/mesh-resolver/zones.conf", + "template": "# Generated by the mesh. Do not edit — this file is replaced whenever a module declares a zone or\n# its machine moves (novox/hq ADR 0199). One forwarding line per zone, to the module answering it.\n\n{{range .Zones}}server=/{{.Zone}}/{{.Address}}#{{.Port}}\n{{end}}" + } + }, + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/dnsmasq-tools", + "binary": "dnsmasq-tools", + "loads": [ + "dnsmasq-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/docker-compose/module.json.captured b/go/testdata/beside/mesh-catalog/modules/docker-compose/module.json.captured new file mode 100644 index 0000000..2f576b4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker-compose/module.json.captured @@ -0,0 +1,40 @@ +{ + "module": "docker-compose", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "docker_compose_projects", + "docker_compose_ps", + "docker_compose_logs", + "docker_compose_config", + "docker_compose_up", + "docker_compose_down", + "docker_compose_restart", + "docker_compose_pull", + "docker_compose_job" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "docker-compose" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/docker-compose-tools", + "binary": "docker-compose-tools", + "loads": [ + "docker-compose-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/docker/module.json.captured b/go/testdata/beside/mesh-catalog/modules/docker/module.json.captured new file mode 100644 index 0000000..a3a75e0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker/module.json.captured @@ -0,0 +1,179 @@ +{ + "module": "docker", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager", + "privileged" + ], + "invokes": [ + "seat:mesh-controller.images" + ], + "claims": [ + { + "name": "node-container-runtime", + "scope": "node" + } + ], + "tools": [ + "docker_list", + "docker_inspect", + "docker_resolv_conf", + "docker_logs", + "docker_secrets_in_logs", + "docker_secrets_in_events", + "docker_stats", + "docker_start", + "docker_stop", + "docker_restart", + "docker_top", + "docker_images", + "docker_prune", + "docker_prune_images", + "docker_disk_usage", + "docker_networks", + "docker_volumes", + "docker_events", + "docker_daemon_config", + "docker_unlabelled", + "docker_problems", + "docker_ports" + ], + "replaces": { + "docker_resolv_conf": [ + "docker exec cat /etc/resolv.conf" + ] + }, + "config": { + "log-max-size": { + "type": "string", + "description": "The size each container's log is cut at, a whole number from 1 with k, m or g after it; the runtime keeps log-max-file of them, so 100m and 10 hold about 1 GB per container (novox/hq issue 452). The runtime reads it only at its next start, which the mesh never forces, and a container created before that keeps the options it was created with until it is created again.", + "pattern": "[1-9][0-9]*[kmg]", + "trusted": true, + "default": "100m" + }, + "log-max-file": { + "type": "number", + "description": "How many of a container's log files of log-max-size the runtime keeps, a count from 1 to 99, the oldest dropped first; taken at the runtime's next start and by containers created after it, like log-max-size (novox/hq issue 452).", + "minimum": 1, + "maximum": 99, + "trusted": true, + "default": 10 + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "docker" + }, + { + "id": "buildx", + "type": "package", + "package": "docker-buildx" + }, + { + "id": "socket", + "type": "service", + "unit": "docker.socket", + "state": "running", + "boot": "enabled" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "log-rotation", + "type": "file", + "path": "${dir:state}/log-rotation.conf", + "mode": "0600", + "trusted": true, + "template": "files/log-rotation.conf" + }, + { + "id": "log-rotation-check", + "type": "process", + "name": "docker-log-rotation-check", + "artifact": "tools", + "run": [ + "./docker-tools", + "check-log-rotation" + ], + "run-once": true, + "env": { + "DOCKER_LOG_ROTATION": "${dir:state}/log-rotation.conf" + }, + "restart-on": [ + "log-rotation" + ] + }, + { + "id": "daemon", + "type": "file", + "path": "/etc/docker/daemon.json", + "mode": "0644", + "into": "json", + "trusted": true, + "template": "files/daemon.json" + }, + { + "id": "runtime", + "type": "service", + "unit": "docker.service", + "state": "running", + "boot": "enabled", + "reload-on": [ + "daemon" + ] + }, + { + "id": "prune-service", + "type": "file", + "path": "/etc/systemd/system/docker-prune.service", + "mode": "0644", + "template": "files/docker-prune.service" + }, + { + "id": "prune-timer", + "type": "file", + "path": "/etc/systemd/system/docker-prune.timer", + "mode": "0644", + "template": "files/docker-prune.timer" + }, + { + "id": "prune", + "type": "service", + "unit": "docker-prune.timer", + "state": "running", + "boot": "enabled", + "restart-on": [ + "prune-service", + "prune-timer" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/docker-tools", + "binary": "docker-tools", + "loads": [ + "docker-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/dunst/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dunst/module.json.captured new file mode 100644 index 0000000..0c6983b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dunst/module.json.captured @@ -0,0 +1,123 @@ +{ + "module": "dunst", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "claims": [ + { + "name": "node-notifier", + "scope": "node", + "serves": [ + "send", + "history" + ] + } + ], + "tools": [ + "dunst_pause", + "dunst_resume", + "dunst_close_all", + "dunst_rules", + "dunst_count", + "dunst_reload" + ], + "config": { + "font-size": { + "type": "number", + "description": "The size of a notification's text, in points of Inter: 10 reads well on a screen of about 100 DPI, and a denser screen needs a larger size.", + "minimum": 6, + "maximum": 72, + "predefined": "font-size", + "default": 10 + }, + "width": { + "type": "number", + "description": "The width of a notification, in pixels: 250 fits a title of about forty characters at 10 points, and a larger font needs a wider notification.", + "minimum": 50, + "maximum": 4000, + "default": 250 + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "dunst" + }, + { + "id": "client", + "type": "package", + "package": "libnotify" + }, + { + "id": "configuration-dir", + "type": "directory", + "path": "${machine:account-home}/.config/dunst", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "dropins", + "type": "directory", + "path": "${machine:account-home}/.config/dunst/dunstrc.d", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out", + "knows": ["50-slack.conf"] + }, + { + "id": "configuration", + "trusted": false, + "type": "file", + "path": "${machine:account-home}/.config/dunst/dunstrc", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/dunstrc" + }, + { + "id": "notifier", + "type": "service", + "unit": "dunst.service", + "scope": "user", + "user": "${machine:account}", + "reload-on": [ + "configuration" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/dunst-tools", + "binary": "dunst-tools", + "loads": [ + "dunst-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "emits": [ + "action-chosen", + "started", + "paused" + ], + "items": [ + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/i3/50-dunst.conf" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/module.json.captured b/go/testdata/beside/mesh-catalog/modules/fail2ban/module.json.captured new file mode 100644 index 0000000..446395f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/module.json.captured @@ -0,0 +1,195 @@ +{ + "module": "fail2ban", + "version": "1", + "capabilities": [ + "firewall" + ], + "claims": [ + { + "name": "node-intrusion-prevention", + "scope": "node", + "serves": [ + "status", + "banned", + "ban", + "unban" + ] + } + ], + "tools": [ + "fail2ban_settings", + "fail2ban_health" + ], + "jailing": { + "into": "/etc/fail2ban/jail.d/mesh.conf", + "filter-into": "/etc/fail2ban/filter.d" + }, + "config": { + "own-addresses": { + "type": "list", + "description": "The addresses and networks outside the mesh that are the operator's own, each an IP address or a CIDR network: every jail ignores them beside the private ranges, the ban verb refuses them, and a failure a jail counted from one is said as a warning; empty is none.", + "trusted": true, + "default": [] + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "fail2ban" + }, + { + "id": "state-dir", + "type": "directory", + "path": "/var/lib/mesh-fail2ban", + "mode": "0755", + "unknown": "program" + }, + { + "id": "own-addresses-list", + "type": "file", + "path": "/var/lib/mesh-fail2ban/own-addresses", + "mode": "0644", + "create-once": true, + "template": "files/own-addresses" + }, + { + "id": "jail-d", + "type": "directory", + "path": "/etc/fail2ban/jail.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "action-d", + "type": "directory", + "path": "/etc/fail2ban/action.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "filter-d", + "type": "directory", + "path": "/etc/fail2ban/filter.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "run-dir", + "type": "directory", + "path": "/var/run/fail2ban", + "mode": "0755", + "unknown": "program" + }, + { + "id": "jail-local", + "type": "file", + "path": "/etc/fail2ban/jail.local", + "mode": "0644", + "template": "files/jail.local" + }, + { + "id": "jail-sshd", + "type": "file", + "path": "/etc/fail2ban/jail.d/sshd.conf", + "mode": "0644", + "template": "files/sshd.conf" + }, + { + "id": "own-addresses", + "type": "file", + "path": "/etc/fail2ban/mesh-own-addresses.json", + "mode": "0644", + "merge": "json", + "takes-config": [ + "own-addresses" + ], + "trusted": true, + "template": "files/mesh-own-addresses.json" + }, + { + "id": "watch", + "type": "process", + "name": "fail2ban-watch", + "artifact": "tools", + "run": [ + "./fail2ban-tools", + "watch" + ], + "health": { + "kind": "tool", + "tool": "fail2ban_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "90s" + } + }, + { + "id": "log", + "type": "file", + "path": "/var/log/fail2ban.log", + "mode": "0640", + "create-once": true, + "template": "files/own-addresses" + }, + { + "id": "jail-recidive", + "type": "file", + "path": "/etc/fail2ban/jail.d/recidive.conf", + "mode": "0644", + "template": "files/recidive.conf" + }, + { + "id": "action-dualchain", + "type": "file", + "path": "/etc/fail2ban/action.d/iptables-allports-dualchain.conf", + "mode": "0644", + "template": "files/iptables-allports-dualchain.conf" + }, + { + "id": "logrotate", + "type": "file", + "path": "/etc/logrotate.d/fail2ban", + "mode": "0644", + "template": "files/fail2ban" + }, + { + "id": "run", + "type": "service", + "unit": "fail2ban.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "jail-local", + "jail-sshd", + "jail-recidive", + "action-dualchain", + "composed-jails" + ], + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/fail2ban-tools", + "binary": "fail2ban-tools", + "loads": [ + "fail2ban-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/feh/module.json.captured b/go/testdata/beside/mesh-catalog/modules/feh/module.json.captured new file mode 100644 index 0000000..d6e4407 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/feh/module.json.captured @@ -0,0 +1,75 @@ +{ + "module": "feh", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "tools": [ + "feh_set", + "feh_current" + ], + "items": [ + { + "collection": "shell:xinitrc:normal", + "name": "xinitrc-normal", + "template": "files/xinitrc-normal" + }, + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/i3/50-feh.conf" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "feh" + }, + { + "id": "wallpapers", + "type": "archive", + "path": "${machine:account-home}/.local/share/feh/wallpapers", + "owner": "${machine:account}", + "artifact": "wallpapers", + "unknown": "move-out" + }, + { + "id": "fehbg", + "type": "file", + "path": "${machine:account-home}/.fehbg", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/fehbg" + } + ], + "build": { + "artifacts": [ + { + "name": "wallpapers", + "kind": "archive", + "from": "wallpaper" + }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/feh-tools", + "binary": "feh-tools", + "loads": [ + "feh-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/flatpak/module.json.captured b/go/testdata/beside/mesh-catalog/modules/flatpak/module.json.captured new file mode 100644 index 0000000..76d4721 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/flatpak/module.json.captured @@ -0,0 +1,42 @@ +{ + "module": "flatpak", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "flatpak_list", + "flatpak_runtimes", + "flatpak_remotes", + "flatpak_updates", + "flatpak_unused", + "flatpak_disk_usage", + "flatpak_install", + "flatpak_remove", + "flatpak_update", + "flatpak_remove_unused", + "flatpak_job" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "flatpak" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/flatpak-tools", + "binary": "flatpak-tools", + "loads": [ + "flatpak-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/fonts/module.json.captured b/go/testdata/beside/mesh-catalog/modules/fonts/module.json.captured new file mode 100644 index 0000000..b2958da --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fonts/module.json.captured @@ -0,0 +1,70 @@ +{ + "module": "fonts", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "fonts_families", + "fonts_match", + "fonts_glyph", + "fonts_sources", + "fonts_config", + "fonts_cache_rebuild" + ], + "resources": [ + { + "id": "monospace", + "type": "package", + "package": "ttf-jetbrains-mono-nerd" + }, + { + "id": "interface", + "type": "package", + "package": "inter-font" + }, + { + "id": "symbols", + "type": "package", + "package": "ttf-nerd-fonts-symbols" + }, + { + "id": "noto", + "type": "package", + "package": "noto-fonts" + }, + { + "id": "emoji", + "type": "package", + "package": "noto-fonts-emoji" + }, + { + "id": "defaults", + "type": "file", + "path": "${machine:account-home}/.config/fontconfig/conf.d/50-mesh-fonts.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/50-mesh-fonts.conf" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/fonts-tools", + "binary": "fonts-tools", + "loads": [ + "fonts-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/forticlient/module.json.captured b/go/testdata/beside/mesh-catalog/modules/forticlient/module.json.captured new file mode 100644 index 0000000..631d3fa --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/forticlient/module.json.captured @@ -0,0 +1,57 @@ +{ + "module": "forticlient", + "version": "1", + "upgrade": { + "policy": "record", + "why": "its adapter routes the company's domains on the machine a person works on: a build that breaks it cuts the person off from work names until a person pushes the next (hq ADR 0236, ADR 0247)" + }, + "capabilities": [ + "service-manager" + ], + "requires": [ + "x11-display", + "split-dns" + ], + "tools": [ + "forticlient_status", + "forticlient_restart", + "forticlient_check" + ], + "resources": [ + { + "id": "scheduler", + "type": "service", + "unit": "forticlient.service", + "state": "running", + "boot": "enabled" + }, + { + "id": "split-dns", + "type": "process", + "name": "forticlient-split-dns", + "artifact": "tools", + "run": [ + "./forticlient-tools", + "split-dns" + ], + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/forticlient-tools", + "binary": "forticlient-tools", + "loads": [ + "forticlient-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured b/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured new file mode 100644 index 0000000..2de9730 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured @@ -0,0 +1,545 @@ +{ + "module": "gitea", + "version": "1", + "moved": true, + "requires": [ + "postgres-database", + "route", + "secret" + ], + "contributes": { + "postgres-database": { + "name": "gitea" + }, + "route": { + "web": { + "label": "git", + "endpoint": "web" + }, + "internal-api-refused": { + "label": "git", + "path": "/api/internal", + "deny": true, + "priority": 100000 + } + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "secret": { + "internal-token": "${dir:state}/internal-token.secret", + "admin": "${dir:state}/admin.secret" + } + }, + "capabilities": [ + "container-runtime" + ], + "emits": [ + "repo.created", + "issue.opened", + "pull.merged", + "pull.updated", + "pull.closed", + "forge.push", + "forge.create", + "forge.delete", + "forge.fork", + "forge.status", + "forge.pull-request.opened", + "forge.pull-request.reopened", + "forge.pull-request.synchronized", + "forge.pull-request.edited", + "forge.pull-request.closed", + "forge.pull-request.merged", + "forge.pull-request.labeled", + "forge.pull-request.unlabeled", + "forge.pull-request.assigned", + "forge.pull-request.unassigned", + "forge.pull-request.reviewed", + "forge.pull-request.review-requested", + "forge.pull-request.commented", + "forge.pull-request.changed", + "forge.issue-comment.created", + "forge.issue-comment.edited", + "forge.issue-comment.deleted", + "forge.issues.opened", + "forge.issues.reopened", + "forge.issues.closed", + "forge.issues.edited", + "forge.issues.labeled", + "forge.issues.unlabeled", + "forge.issues.assigned", + "forge.issues.unassigned", + "forge.issues.changed", + "forge.release.published", + "forge.release.updated", + "forge.release.deleted", + "forge.release.changed", + "forge.repository.created", + "forge.repository.deleted", + "forge.repository.changed", + "forge.wiki.changed", + "forge.other" + ], + "consumes": [ + "mesh-controller.checked", + "node-build-agent.started" + ], + "listens": [ + { + "name": "web", + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the forge, over http" + }, + { + "name": "ssh", + "port": 22, + "protocol": "tcp", + "from": "mesh", + "why": "git over ssh, gitea's own unmodified sshd. Published on the machine's own side at 222, the mesh's fixed public convention — not 22, which the machine's own daemon holds and a module does not take" + }, + { + "name": "forge-events", + "port": 3047, + "protocol": "tcp", + "from": "machine", + "why": "the forge's system webhook, every event signed with a secret the module keeps, from the forge's container on the docker bridge to the module's receiver at the bridge's gateway 172.17.0.1 (novox/hq issue 729)" + } + ], + "serves": { + "npm-package-registry": { + "scheme": "http", + "port": 3000, + "npm-path": "/api/packages/novox/npm/", + "public-root": "https://${bound:route:name-web}/api/packages/novox/npm/" + }, + "git": { + "scheme": "http", + "port": 3000 + } + }, + "receives": { + "npm-package-registry": "${dir:grants}/npm.json" + }, + "grants": { + "npm-package-registry": "${dir:grants}" + }, + "claims": [ + { + "name": "npm-package-registry", + "scope": "mesh" + }, + { + "name": "git", + "scope": "mesh" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "every repository, its issues and attachments, and the package registry" + } + ], + "consumers": { + "npm-package-registry": { + "class": "rebuildable", + "in": "data", + "why": "a consumer's packages are published again from its source" + } + } + }, + "config": { + "url": { + "type": "string", + "description": "The address the module's tools reach the forge at, as a URL; unset, the code falls back to its environment, else this machine's gitea on its port.", + "trusted": true, + "default": "" + }, + "agent-read-teams": { + "type": "object", + "description": "The read teams of the agents' forge accounts (ADR 0365 clause 3): each team by name, {\"\": {\"repos\": [\"/\", ...], \"members\": [\"\", ...]}}. The forge gets a team agent-read- that reads only, with exactly those repositories and the accounts agent-; a team taken out of the value is emptied. Nothing else on the forge is touched; an agent account is in no team until it is listed here.", + "trusted": true, + "default": {} + }, + "repositories": { + "type": "object", + "description": "The repositories of the organisation novox the mesh holds (ADR 0428): each by name, {\"\": {\"private\": true, \"description\": \"...\"}}. One that does not exist is made by the module's setup login under its own account, its main protected and given a first commit (a README naming the description and a merge-check.sh), its rule narrowed to no push and the tools' merge requiring mesh/repo-check and mesh/merge-gate, and only then moved into the organisation; one that exists only has main's rule put right, its statuses kept. A repository taken out of the value is never touched.", + "trusted": true, + "default": {} + } + }, + "facts": { + "forge-host": { + "path": "/etc/gitea/forge-host", + "template": "{{range .Machines}}{{if eq .Name $.Node}}{{.FQDN}}{{end}}{{end}}\n" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "runtime-state", + "type": "directory", + "path": "${dir:mesh-state}/state", + "mode": "0700", + "unknown": "program" + }, + { + "id": "forge-patches", + "type": "directory", + "path": "/var/lib/mesh/forge-patches", + "mode": "0733", + "owner": "${machine:account}", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/api/healthz", + "status": 200, + "needs": "postgres-database" + }, + "name": "gitea", + "image": "gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef", + "env": { + "DB_TYPE": "postgres", + "USER_UID": "1000", + "USER_GID": "1000" + }, + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "3000", + "222:22" + ], + "volumes": [ + "${dir:data}:/data" + ], + "secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it", + "logging": "journald" + }, + { + "id": "admin-bootstrap", + "type": "container", + "name": "mesh-gitea-admin", + "image": "gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef", + "run-once": true, + "env": { + "USER_UID": "1000", + "USER_GID": "1000", + "MESH_GITEA_ADMIN_USER": "mesh-admin" + }, + "env-file": [ + "${dir:state}/server.env" + ], + "volumes": [ + "${dir:data}:/data", + "${dir:state}/admin.secret:/run/secrets/admin:ro" + ], + "args": [ + "/bin/sh", + "-c", + "su-exec git gitea admin user create --admin --username \"$MESH_GITEA_ADMIN_USER\" --email mesh-admin@localhost --password \"$(cat /run/secrets/admin)\" --must-change-password=false || true" + ], + "secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it" + }, + { + "id": "origins", + "type": "file", + "path": "${dir:mesh-state}/origins", + "mode": "0644", + "template": "files/origins" + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": [ + "url" + ] + }, + { + "id": "agent-teams-file", + "type": "file", + "path": "${dir:mesh-state}/agent-teams.json", + "mode": "0600", + "template": "files/agent-teams.json", + "merge": "json", + "takes-config": [ + "agent-read-teams" + ] + }, + { + "id": "repositories-file", + "type": "file", + "path": "${dir:mesh-state}/repositories.json", + "mode": "0600", + "template": "files/repositories.json" + }, + { + "id": "consumers", + "type": "file", + "path": "${dir:state}/consumers.json", + "mode": "0600", + "template": "files/consumers.json" + }, + { + "id": "agent-teams", + "type": "process", + "name": "gitea-agent-teams", + "artifact": "agent-teams", + "run": [ + "./agent-teams" + ], + "run-once": true, + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_AGENT_TEAMS_FILE": "${dir:mesh-state}/agent-teams.json" + }, + "restart-on": [ + "agent-teams-file" + ] + }, + { + "id": "repositories", + "type": "process", + "name": "gitea-repositories", + "artifact": "repositories", + "run": [ + "./repositories", + "sync" + ], + "run-once": true, + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_GITEA_TOOLS_USER": "mesh-tools", + "MESH_REPOSITORIES_FILE": "${dir:mesh-state}/repositories.json", + "MESH_REPOSITORIES_OUTCOME": "${dir:runtime-state}/repositories-outcome.json" + }, + "restart-on": [ + "repositories-file" + ] + } + ], + "provides": [ + { + "name": "npm-package-registry", + "scope": "mesh", + "identity": { + "max": 40, + "in": "a Gitea user name" + } + }, + { + "name": "git", + "scope": "mesh" + }, + { + "name": "gitea-read", + "scope": "mesh", + "credential": { + "issued-by": "software", + "tool": "gitea_agent_token" + } + }, + { + "name": "forge-account", + "scope": "mesh" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_GITEA_TOOLS_USER": "mesh-tools", + "MESH_GITEA_STATE_DIR": "${dir:runtime-state}", + "MESH_RECEIVES": "${dir:grants}/npm.json", + "MESH_GITEA_HOOK_URL": "http://172.17.0.1:3047", + "MESH_GITEA_HOOK_PORT": "3047", + "MESH_GITEA_HOOK_HOST": "172.17.0.1" + } + }, + { + "name": "npm-registry", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/npm-registry", + "binary": "npm-registry", + "loads": [ + "npm-registry" + ], + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_NPM_OWNER": "novox" + } + }, + { + "name": "forge", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/forge", + "binary": "forge", + "loads": [ + "forge" + ], + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_TOOLS_USER": "mesh-tools", + "MESH_GITEA_TOKEN_FILE": "${dir:runtime-state}/token", + "MESH_GITEA_PATCH_DIR": "${dir:forge-patches}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + }, + { + "name": "agent-token", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/agent-token", + "binary": "agent-token", + "loads": [ + "agent-token" + ], + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_GITEA_ORIGINS_FILE": "${dir:mesh-state}/origins" + } + }, + { + "name": "agent-teams", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/agent-teams", + "binary": "agent-teams", + "loads": [ + "agent-teams" + ] + }, + { + "name": "repositories", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/repositories", + "binary": "repositories", + "loads": [ + "repositories" + ] + }, + { + "name": "forge-account-provider", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/forge-account-provider", + "binary": "forge-account-provider", + "loads": [ + "forge-account-provider" + ], + "env": { + "MESH_GITEA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GITEA_ADMIN_USER": "mesh-admin", + "MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_GITEA_ORIGINS_FILE": "${dir:mesh-state}/origins", + "MESH_GITEA_SSH_PORT": "222", + "MESH_FORGE_HOST_FILE": "/etc/gitea/forge-host" + } + } + ] + }, + "jails": [ + { + "name": "gitea", + "failregex": "^.*Failed authentication attempt for .* from (?::\\d+)?\\s*$\n ^.*Invalid user .* from port \\d+\\s*$\n ^.*User \\S+ from not allowed because .*$", + "jail": "backend = systemd\njournalmatch = CONTAINER_NAME=gitea\nport = http,https,222\nmaxretry = 3\nfindtime = 1d\nbantime = 1d" + } + ], + "state": [ + "bound", + "announced" + ], + "reads": [ + "mesh-controller.assignments", + "ssh-client.keys" + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/gitlab/module.json.captured b/go/testdata/beside/mesh-catalog/modules/gitlab/module.json.captured new file mode 100644 index 0000000..3cb33fb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitlab/module.json.captured @@ -0,0 +1,65 @@ +{ + "module": "gitlab", + "version": "1", + "own-secrets": { + "token": "${dir:state}/token" + }, + "config": { + "gitlab-url": { + "type": "string", + "description": "The address of the GitLab instance the gitlab tools call, as a URL such as https://gitlab.com, laid into the file as GITLAB_URL; unset, the code falls back to its environment and answers no calls until it is given.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:state}/config.json", + "merge": "json", + "template": "files/config.json", + "mode": "0600", + "takes-config": [ + { + "key": "gitlab-url", + "as": "GITLAB_URL" + } + ] + } + ], + "capabilities": [ + "container-runtime" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_GITLAB_TOKEN_FILE": "${dir:state}/token", + "MESH_GITLAB_CONFIG_FILE": "${dir:state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/gnome-keyring/module.json.captured b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/module.json.captured new file mode 100644 index 0000000..9f75b9f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/module.json.captured @@ -0,0 +1,82 @@ +{ + "module": "gnome-keyring", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "claims": [ + { + "name": "node-secret-service", + "scope": "node" + } + ], + "tools": [ + "gnome_keyring_unlocked", + "gnome_keyring_lock", + "gnome_keyring_collections", + "gnome_keyring_ssh_keys" + ], + "items": [ + { + "collection": "shell:xinitrc:first", + "name": "xinitrc-first", + "template": "files/xinitrc-first" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "gnome-keyring" + }, + { + "id": "library", + "type": "package", + "package": "libsecret" + }, + { + "id": "manager", + "type": "package", + "package": "seahorse" + }, + { + "id": "pam-login", + "type": "file", + "path": "/etc/pam.d/login", + "mode": "0644", + "into": "block", + "at": "end", + "template": "files/pam/login" + }, + { + "id": "pam-passwd", + "type": "file", + "path": "/etc/pam.d/passwd", + "mode": "0644", + "into": "block", + "at": "end", + "template": "files/pam/passwd" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/gnome-keyring-tools", + "binary": "gnome-keyring-tools", + "loads": [ + "gnome-keyring-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/module.json.captured b/go/testdata/beside/mesh-catalog/modules/grafana/module.json.captured new file mode 100644 index 0000000..1cbf995 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/module.json.captured @@ -0,0 +1,225 @@ +{ + "module": "grafana", + "version": "1", + "emits": [ + "alert.firing" + ], + "own-secrets": { + "admin": "${dir:mesh-state}/admin" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "web", + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the dashboards. Also 3000 inside, like the forge - which is the mesh's port assignment earning its keep" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "dashboards, users and alert rules" + } + ] + }, + "config": { + "user": { + "type": "string", + "description": "The Grafana account the module's tools sign in as with the vault's password; the file's default is admin.", + "trusted": true, + "default": "admin" + }, + "url": { + "type": "string", + "description": "The address the module's tools reach Grafana at, as a URL; unset, the code falls back to its environment, else this machine's Grafana on its port.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "472:472", + "unknown": "program" + }, + { + "id": "admin-secret", + "type": "file", + "path": "${dir:state}/admin.secret", + "mode": "0400", + "owner": "472:472", + "template": "files/admin.secret" + }, + { + "id": "oidc-secret", + "type": "file", + "path": "${dir:state}/oidc-client.secret", + "mode": "0400", + "owner": "472:472", + "template": "files/oidc-client.secret" + }, + { + "id": "oidc-env", + "type": "file", + "path": "${dir:state}/oidc.env", + "mode": "0644", + "template": "files/oidc.env" + }, + { + "id": "influxdb-datasources", + "type": "directory", + "path": "${dir:state}/datasources", + "mode": "0755", + "owner": "472:472", + "unknown": "program" + }, + { + "id": "influxdb-credential", + "type": "directory", + "path": "${dir:state}/influxdb", + "mode": "0700", + "owner": "472:472", + "unknown": "program" + }, + { + "id": "influxdb-secret", + "type": "file", + "path": "${dir:influxdb-credential}/influxdb-api", + "mode": "0400", + "owner": "472:472", + "template": "files/influxdb-api", + "only-when-bound": "influxdb-api" + }, + { + "id": "influxdb-datasource", + "type": "file", + "path": "${dir:influxdb-datasources}/mesh-influxdb.yaml", + "mode": "0644", + "template": "files/mesh-influxdb.yaml", + "only-when-bound": "influxdb-api" + }, + { + "id": "server", + "type": "container", + "name": "grafana", + "image": "grafana/grafana@sha256:ac461fb352abc50da10a51c7d02462e9c05488f11f53f14b3ad79a8145f638a0", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/" + }, + "ports": [ + "3000" + ], + "volumes": [ + "${dir:data}:/var/lib/grafana", + "${dir:state}/admin.secret:/run/secrets/admin:ro", + "${dir:state}/oidc-client.secret:/run/secrets/oidc-client:ro", + "${dir:influxdb-credential}:/run/secrets/influxdb:ro", + "${dir:influxdb-datasources}:/etc/grafana/provisioning/datasources:ro" + ], + "env": { + "GF_SECURITY_ADMIN_PASSWORD__FILE": "/run/secrets/admin" + }, + "env-file": [ + "${dir:state}/oidc.env" + ], + "restart-on": [ + "oidc-env", + "oidc-secret", + "influxdb-datasource", + "influxdb-secret" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["user", "url"], + "protected": ["password", "token"] + } + ], + "requires": [ + "route", + "oidc-client", + { + "name": "influxdb-api", + "optional": true + } + ], + "contributes": { + "route": { + "label": "grafana", + "endpoint": "web" + }, + "oidc-client": { + "label": "grafana", + "endpoint": "web", + "callback": "/login/generic_oauth" + }, + "influxdb-api": { + "access": "read" + } + }, + "binds": { + "route": "${dir:state}/route.json", + "oidc-client": "${dir:state}/oidc.json", + "influxdb-api": "${dir:state}/influxdb.json" + }, + "secrets": { + "oidc-client": "${dir:mesh-state}/oidc-client", + "influxdb-api": "${dir:mesh-state}/influxdb-api" + }, + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "MESH_GRAFANA_URL": "http://127.0.0.1:${port:3000}", + "MESH_GRAFANA_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/hello-web/module.json.captured b/go/testdata/beside/mesh-catalog/modules/hello-web/module.json.captured new file mode 100644 index 0000000..005578c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/hello-web/module.json.captured @@ -0,0 +1,82 @@ +{ + "module": "hello-web", + "slug": "hello", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "hello", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "listens": [ + { + "name": "web", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the demo web page; only the route-proxy reaches it, and the public name is a route grant" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "page", + "type": "file", + "path": "${dir:state}/index.html", + "mode": "0644", + "template": "files/index.html" + }, + { + "id": "net", + "type": "network", + "name": "hello-web" + }, + { + "id": "server", + "type": "container", + "name": "hello-web", + "network": "hello-web", + "ports": [ + "8080" + ], + "volumes": [ + "${dir:state}/index.html:/www/index.html:ro" + ], + "args": [ + "sh", + "-c", + "while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\n'; cat /www/index.html; } | nc -l -p 8080; done" + ], + "artifact": "server" + } + ], + "build": { + "artifacts": [ + { + "name": "server", + "kind": "upstream", + "from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/home-assistant/module.json.captured b/go/testdata/beside/mesh-catalog/modules/home-assistant/module.json.captured new file mode 100644 index 0000000..9c4f99d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/home-assistant/module.json.captured @@ -0,0 +1,209 @@ +{ + "module": "home-assistant", + "version": "1", + "slug": "hass", + "capabilities": [ + "container-runtime" + ], + "emits": [ + "state.changed" + ], + "own-secrets": { + "token": "${dir:mesh-state}/token" + }, + "listens": [ + { + "name": "web", + "port": 8123, + "protocol": "tcp", + "from": "mesh", + "why": "the dashboard, the API and the companion apps" + }, + { + "name": "sonos-events", + "port": 1400, + "protocol": "tcp", + "from": "mesh", + "why": "the Sonos integration's event callback: speakers push their state changes here" + }, + { + "name": "webrtc", + "port": 18555, + "protocol": "tcp", + "from": "mesh", + "why": "the bundled go2rtc's WebRTC port, which camera streams to a browser use" + } + ], + "data": { + "own": [ + { + "id": "config", + "path": "${dir:config}", + "class": "valuable", + "active": "1d", + "why": "the house's configuration, automations and history; written all the time" + } + ] + }, + "config": { + "url": { + "type": "string", + "description": "The address the module's tools reach Home Assistant at, as a URL; unset, the code falls back to its environment, else this machine's Home Assistant on its port.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "written", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "home-assistant", + "image": "ghcr.io/home-assistant/home-assistant@sha256:d8922685169707fd91e8b9729902d975f06157d005e422874d201e0261dda196", + "network": "host", + "env": { + "TZ": "Etc/UTC" + }, + "volumes": [ + "${dir:config}:/config" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["url"] + }, + { + "id": "provisions-env", + "type": "file", + "path": "${dir:state}/provisions.env", + "mode": "0600", + "template": "files/provisions.env" + }, + { + "id": "provisions", + "type": "process", + "name": "home-assistant-provisions", + "artifact": "code", + "run": [ + "node", + "provisions/index.js" + ], + "run-once": true, + "env-file": [ + "${dir:state}/provisions.env" + ], + "restart-on": [ + "provisions-env", + "bound-mqtt-topic", + "secret-mqtt-topic", + "bound-sonarr-api", + "secret-sonarr-api", + "bound-radarr-api", + "secret-radarr-api", + "bound-lidarr-api", + "secret-lidarr-api" + ] + } + ], + "requires": [ + { + "name": "lidarr-api", + "optional": true + }, + { + "name": "mqtt-topic", + "optional": true + }, + { + "name": "radarr-api", + "optional": true + }, + "route", + { + "name": "sonarr-api", + "optional": true + } + ], + "contributes": { + "mqtt-topic": { + "topics": [ + "#" + ] + }, + "route": { + "label": "home-assistant", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json", + "mqtt-topic": "${dir:state}/mqtt-topic.json", + "sonarr-api": "${dir:state}/sonarr-api.json", + "radarr-api": "${dir:state}/radarr-api.json", + "lidarr-api": "${dir:state}/lidarr-api.json" + }, + "secrets": { + "mqtt-topic": "${dir:state}/mqtt-topic.secret", + "sonarr-api": "${dir:state}/sonarr-api.secret", + "radarr-api": "${dir:state}/radarr-api.secret", + "lidarr-api": "${dir:state}/lidarr-api.secret" + }, + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisions/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "MESH_HOMEASSISTANT_URL": "http://127.0.0.1:${port:8123}", + "MESH_HOMEASSISTANT_TOKEN_FILE": "${dir:mesh-state}/token", + "MESH_HOMEASSISTANT_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/hostname/module.json.captured b/go/testdata/beside/mesh-catalog/modules/hostname/module.json.captured new file mode 100644 index 0000000..e84e7be --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/hostname/module.json.captured @@ -0,0 +1,61 @@ +{ + "module": "hostname", + "version": "1", + "claims": [ + { + "name": "node-hostname", + "scope": "node", + "serves": [ + "entries", + "add", + "remove" + ] + } + ], + "config": { + "hostname": { + "type": "string", + "description": "The machine's host name, written to /etc/hostname: the operator's name for this machine, with no default (novox/hq ADR 0112).", + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "own", + "type": "file", + "path": "/etc/hosts", + "mode": "0644", + "into": "block", + "at": "start", + "template": "files/hosts" + }, + { + "id": "name", + "type": "file", + "path": "/etc/hostname", + "mode": "0644", + "template": "files/hostname" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/hostname-tools", + "binary": "hostname-tools", + "loads": [ + "hostname-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/i3/module.json.captured b/go/testdata/beside/mesh-catalog/modules/i3/module.json.captured new file mode 100644 index 0000000..1ec9242 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/module.json.captured @@ -0,0 +1,124 @@ +{ + "module": "i3", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-display-session", + "scope": "node", + "serves": [ + "reload", + "workspaces", + "windows" + ] + } + ], + "tools": [ + "i3_focus", + "i3_move", + "i3_layout_save", + "i3_layout_restore", + "i3_exec", + "i3_kill", + "i3_bindings", + "i3_config_check", + "i3_marks", + "i3_scratchpad" + ], + "items": [ + { + "collection": "environment", + "name": "xdg-current-desktop", + "attributes": { + "kind": "variable", + "name": "XDG_CURRENT_DESKTOP" + }, + "template": "files/env-xdg-current-desktop" + }, + { + "collection": "environment", + "name": "xdg-session-desktop", + "attributes": { + "kind": "variable", + "name": "XDG_SESSION_DESKTOP" + }, + "template": "files/env-xdg-session-desktop" + }, + { + "collection": "shell:xinitrc:last", + "name": "xinitrc-last", + "template": "files/xinitrc-last" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "i3-wm" + }, + { + "id": "package-dex", + "type": "package", + "package": "dex" + }, + { + "id": "config-dir", + "type": "directory", + "path": "${machine:account-home}/.config/i3", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out", + "knows": ["scripts/"] + }, + { + "id": "drop-ins", + "type": "directory", + "path": "${machine:account-home}/.config/i3/config.d", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out", + "knows": ["10-asus.conf", "20-g14.conf"] + }, + { + "id": "config", + "type": "file", + "path": "${machine:account-home}/.config/i3/config", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/config" + }, + { + "id": "session", + "type": "file", + "path": "/etc/lemurs/wms/i3", + "mode": "0755", + "template": "files/wms/i3" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/i3-tools", + "binary": "i3-tools", + "loads": [ + "i3-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/module.json.captured b/go/testdata/beside/mesh-catalog/modules/i3status-rust/module.json.captured new file mode 100644 index 0000000..1e07556 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/module.json.captured @@ -0,0 +1,126 @@ +{ + "module": "i3status-rust", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-bar", + "scope": "node" + } + ], + "tools": [ + "i3status_rust_reload", + "i3status_rust_blocks", + "i3status_rust_block_run", + "i3status_rust_themes" + ], + "items": [ + { + "collection": "shell:xinitrc:normal", + "name": "xinitrc-normal", + "template": "files/xinitrc-normal" + }, + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/i3/60-i3status-rust.conf" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "i3status-rust" + }, + { + "id": "configuration-dir", + "type": "directory", + "path": "${machine:account-home}/.config/i3status-rust", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "icons-dir", + "type": "directory", + "path": "${machine:account-home}/.config/i3status-rust/icons", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "top-bar", + "type": "file", + "path": "${machine:account-home}/.config/i3status-rust/top-bar.toml", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/top-bar.toml" + }, + { + "id": "bottom-bar", + "type": "file", + "path": "${machine:account-home}/.config/i3status-rust/bottom-bar.toml", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/bottom-bar.toml" + }, + { + "id": "icons", + "type": "file", + "path": "${machine:account-home}/.config/i3status-rust/icons/custom-icons.toml", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/icons/custom-icons.toml" + }, + { + "id": "updates", + "type": "file", + "path": "${machine:account-home}/.local/bin/i3status-updates", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/i3status-updates" + }, + { + "id": "watchdog", + "type": "file", + "path": "${machine:account-home}/.local/bin/i3bar-watchdog", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/i3bar-watchdog" + }, + { + "id": "value", + "type": "file", + "path": "${machine:account-home}/.local/bin/i3status-value", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/i3status-value" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/i3status-rust-tools", + "binary": "i3status-rust-tools", + "loads": [ + "i3status-rust-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/icecast/module.json.captured b/go/testdata/beside/mesh-catalog/modules/icecast/module.json.captured new file mode 100644 index 0000000..6f9ab54 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/icecast/module.json.captured @@ -0,0 +1,152 @@ +{ + "module": "icecast", + "version": "1", + "requires": [ + "route", + "secret" + ], + "contributes": { + "route": { + "label": "icecast", + "endpoint": "stream" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "secrets": { + "secret": { + "source": "${dir:state}/source.secret", + "admin": "${dir:state}/admin.secret", + "relay": "${dir:state}/relay.secret" + } + }, + "capabilities": [ + "container-runtime" + ], + "emits": [ + "stream.started", + "stream.stopped" + ], + "listens": [ + { + "name": "stream", + "port": 8000, + "protocol": "tcp", + "from": "mesh", + "why": "streams in from sources (HTTP PUT) and out to listeners, plus the status and admin pages; a public name is its route" + } + ], + "data": { + "own": [ + { + "id": "logs", + "path": "${dir:logs}", + "class": "cache", + "why": "the streaming server's logs" + } + ] + }, + "config": { + "url": { + "type": "string", + "description": "The address the module's tools reach Icecast at, as a URL; unset, the code falls back to its environment, else this machine's Icecast on its port.", + "trusted": true, + "default": "" + }, + "user": { + "type": "string", + "description": "The Icecast admin account the module's tools sign in as; unset, the code falls back to its environment, else admin.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "logs", + "type": "directory", + "mode": "0700", + "owner": "100:101", + "unknown": "program" + }, + { + "id": "server-conf", + "type": "file", + "path": "${dir:state}/icecast.xml", + "mode": "0600", + "template": "files/icecast.xml" + }, + { + "id": "net", + "type": "network", + "name": "icecast" + }, + { + "id": "server", + "type": "container", + "name": "icecast", + "image": "infiniteproject/icecast@sha256:cd506cf3dfe31ce05fd37d7e672dbd1213e7255cc93d28ecf5a3b547af4e162c", + "network": "icecast", + "ports": [ + "8000" + ], + "volumes": [ + "${dir:state}/icecast.xml:/etc/icecast.xml:ro", + "${dir:logs}:/var/log/icecast" + ], + "restart-on": [ + "server-conf" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["url", "user"] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "MESH_ICECAST_URL": "http://127.0.0.1:${port:8000}", + "MESH_ICECAST_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_ICECAST_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/influxdb/module.json.captured b/go/testdata/beside/mesh-catalog/modules/influxdb/module.json.captured new file mode 100644 index 0000000..53389a5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/influxdb/module.json.captured @@ -0,0 +1,198 @@ +{ + "module": "influxdb", + "version": "1", + "provides": [ + { + "name": "influxdb-api", + "scope": "mesh", + "identity": { + "in": "an InfluxDB v1 authorization" + } + } + ], + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "admin": "${dir:state}/admin.secret", + "admin-token": "${dir:state}/admin-token.secret" + }, + "listens": [ + { + "name": "api", + "port": 8086, + "protocol": "tcp", + "from": "mesh", + "why": "queries, writes and the web UI, over http; consumers granted influxdb-api sign in with the mesh's credential, and a name is a route grant" + } + ], + "config": { + "org": { + "type": "string", + "description": "The organisation influxdb-api names to its consumers and the module's tools write to; mesh is the one influxdb makes at its first start (server.env), and a machine whose server was set up with another sets its own (novox/hq issue 696, ADR 0348 clause 11).", + "trusted": true, + "default": "mesh" + }, + "bucket": { + "type": "string", + "description": "The bucket influxdb-api names to its consumers and the module's tools write to; default is the one influxdb makes at its first start (server.env), and a machine whose server was set up with another sets its own (novox/hq issue 696, ADR 0348 clause 11).", + "trusted": true, + "default": "default" + }, + "url": { + "type": "string", + "description": "The address the module's tools reach influxdb at, as a URL; empty is this machine's influxdb on its port.", + "trusted": true, + "default": "" + } + }, + "serves": { + "influxdb-api": { + "scheme": "http", + "port": 8086, + "org": "${config:org}", + "bucket": "${config:bucket}" + } + }, + "receives": { + "influxdb-api": "${dir:grants}/mesh.json" + }, + "grants": { + "influxdb-api": "${dir:grants}" + }, + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "every consumer's time series" + }, + { + "id": "config", + "path": "${dir:config}", + "class": "valuable", + "why": "the server's own configuration and its operator token, made at its first start" + } + ], + "consumers": { + "influxdb-api": { + "class": "valuable", + "in": "data", + "why": "a consumer's measurements are the only copy" + } + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "config", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "server", + "type": "container", + "name": "influxdb", + "image": "influxdb@sha256:f75e48af0598e8aec7986e991a848d19a119101a7d563a2e5db1dfaac9c45daa", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "8086" + ], + "volumes": [ + "${dir:data}:/var/lib/influxdb2", + "${dir:config}:/etc/influxdb2", + "${dir:state}/admin.secret:/run/secrets/admin:ro", + "${dir:state}/admin-token.secret:/run/secrets/admin-token:ro" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": [ + "bucket", + "org", + "url" + ], + "trusted": true + } + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "influxdb", + "endpoint": "api" + } + }, + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_INFLUXDB_URL": "http://127.0.0.1:${port:8086}", + "MESH_INFLUXDB_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_INFLUXDB_TOKEN_FILE": "${dir:state}/admin-token.secret", + "MESH_RECEIVES": "${dir:grants}/mesh.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/invoicing/module.json.captured b/go/testdata/beside/mesh-catalog/modules/invoicing/module.json.captured new file mode 100644 index 0000000..29670a3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/invoicing/module.json.captured @@ -0,0 +1,128 @@ +{ + "module": "invoicing", + "version": "1", + "slug": "invoice", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "mongodb-database", + "s3-bucket", + "route" + ], + "contributes": { + "mongodb-database": { + "name": "invoicing" + }, + "route": { + "site": { + "label": "invoicing", + "endpoint": "web" + }, + "api": { + "label": "invoicing-api", + "endpoint": "api" + } + } + }, + "binds": { + "mongodb-database": "${dir:state}/database.json", + "s3-bucket": "${dir:state}/store.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "mongodb-database": "${dir:state}/database.secret", + "s3-bucket": "${dir:state}/store.secret" + }, + "listens": [ + { + "name": "web", + "port": 80, + "protocol": "tcp", + "from": "mesh", + "why": "the invoicing web frontend; a public name is a route grant later" + }, + { + "name": "api", + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the invoicing REST API the frontend and integrations call" + } + ], + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "api-env", + "type": "file", + "path": "${dir:state}/api.env", + "mode": "0600", + "template": "files/api.env" + }, + { + "id": "net", + "type": "network", + "name": "invoicing" + }, + { + "id": "app", + "type": "container", + "name": "invoicing-app", + "image": "registry-api.novox.be/novox/invoicing-app@sha256:1e6ed40822f07169b24867cbfe8fc1ab3ef6a15ad642f3b3a2882a8e15c3dbec", + "network": "invoicing", + "env": { + "UID": "2201", + "GID": "2201" + }, + "ports": [ + "80" + ], + "names-on-purpose": { + "registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)" + } + }, + { + "id": "api", + "type": "container", + "name": "invoicing-api", + "image": "registry-api.novox.be/novox/invoicing-api@sha256:efa6fba1fa9ba78849e94e958d33793a76654df0468e3012da9c02c54c265354", + "network": "invoicing", + "env": { + "UID": "2201", + "GID": "2201" + }, + "env-file": [ + "${dir:state}/api.env" + ], + "ports": [ + "9000" + ], + "secrets-in-environment": "the application's own code reads MONGO_URL and MINIO_SECRET from the environment (invoicing-app server/src/config.js); converting is that repository's change", + "names-on-purpose": { + "registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)" + } + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/jira/module.json.captured b/go/testdata/beside/mesh-catalog/modules/jira/module.json.captured new file mode 100644 index 0000000..9f98dd8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/jira/module.json.captured @@ -0,0 +1,75 @@ +{ + "module": "jira", + "version": "1", + "own-secrets": { + "token": "${dir:state}/token" + }, + "config": { + "atlassian-url": { + "type": "string", + "description": "The address of the operator's Atlassian site the jira tools call, as a URL such as https://example.atlassian.net, laid into the file as ATLASSIAN_URL; unset, the code falls back to its environment and answers no calls until it is given.", + "trusted": true, + "default": "" + }, + "atlassian-email": { + "type": "string", + "description": "The email address of the Atlassian account the jira tools sign in as with its API token, laid into the file as ATLASSIAN_EMAIL; unset, the code falls back to its environment and answers no calls until it is given.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:state}/config.json", + "merge": "json", + "template": "files/config.json", + "mode": "0600", + "takes-config": [ + { + "key": "atlassian-url", + "as": "ATLASSIAN_URL" + }, + { + "key": "atlassian-email", + "as": "ATLASSIAN_EMAIL" + } + ] + } + ], + "capabilities": [ + "container-runtime" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_JIRA_TOKEN_FILE": "${dir:state}/token", + "MESH_JIRA_CONFIG_FILE": "${dir:state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/kernel/module.json.captured b/go/testdata/beside/mesh-catalog/modules/kernel/module.json.captured new file mode 100644 index 0000000..67d3215 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/kernel/module.json.captured @@ -0,0 +1,78 @@ +{ + "module": "kernel", + "version": "1", + "capabilities": [ + "service-manager" + ], + "config": { + "transparent-huge-pages": { + "type": "string", + "description": "madvise is the value the mesh chose for every node (novox/hq ADR 0381), not the always the distribution's kernel boots with; a machine that needs another value is given it as its own setting; madvise gives huge pages only to programs that ask for them, which stops a small write to a mapped file (the journal) dirtying and writing back a whole 2 MiB page on a machine whose disks are slow (novox/hq issue 746)", + "trusted": true, + "default": "madvise" + } + }, + "tools": [ + "kernel_huge_pages" + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "huge-pages", + "type": "file", + "path": "${dir:state}/huge-pages", + "mode": "0755", + "trusted": true, + "template": "files/huge-pages.sh" + }, + { + "id": "unit", + "type": "file", + "path": "/etc/systemd/system/mesh-kernel-huge-pages.service", + "mode": "0644", + "template": "files/mesh-kernel-huge-pages.service" + }, + { + "id": "apply", + "type": "service", + "unit": "mesh-kernel-huge-pages.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "unit" + ], + "reload-on": [ + "huge-pages" + ], + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/kernel-tools", + "binary": "kernel-tools", + "loads": [ + "kernel-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/module.json.captured b/go/testdata/beside/mesh-catalog/modules/keycloak/module.json.captured new file mode 100644 index 0000000..847d5f4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/module.json.captured @@ -0,0 +1,245 @@ +{ + "module": "keycloak", + "version": "1", + "moved": true, + "upgrade": { + "policy": "record", + "why": "every person's sign-in to every site goes through it, and its new version migrates its database on start: a person takes each build, after a backup (hq ADR 0236)" + }, + "provides": [ + { + "name": "oidc-client", + "scope": "mesh", + "identity": { + "max": 255, + "in": "a Keycloak client id" + } + } + ], + "requires": [ + "postgres-database", + "route" + ], + "contributes": { + "postgres-database": { + "name": "keycloak" + }, + "route": { + "label": "keycloak", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "capabilities": [ + "container-runtime" + ], + "emits": [ + "user.created", + "user.deleted", + "password.reset", + "client.created", + "client.retired", + "group.created", + "role.created", + "admin.repaired", + "admin.unrepaired" + ], + "listens": [ + { + "name": "web", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "anything the mesh runs that authenticates a person" + } + ], + "serves": { + "oidc-client": { + "authorization-path": "/protocol/openid-connect/auth", + "token-path": "/protocol/openid-connect/token", + "userinfo-path": "/protocol/openid-connect/userinfo", + "issuer": "${config:issuer}" + } + }, + "receives": { + "oidc-client": "${dir:grants}/mesh.json" + }, + "grants": { + "oidc-client": "${dir:grants}" + }, + "own-secrets": { + "admin": "${dir:state}/admin.secret" + }, + "data": { + "consumers": { + "oidc-client": { + "class": "rebuildable", + "in": "postgres-database", + "why": "a consumer's client is made again from its declaration, with a new secret" + } + } + }, + "config": { + "url": { + "type": "string", + "description": "The address the module's provider reaches Keycloak's admin API at, as a URL; empty is this machine's Keycloak on its port.", + "trusted": true, + "default": "" + }, + "user": { + "type": "string", + "description": "The Keycloak administrator the module's provider signs in as; empty is admin.", + "trusted": true, + "default": "" + }, + "realm": { + "type": "string", + "description": "The realm the module's provider makes its consumers' clients in; empty is master.", + "trusted": true, + "default": "" + }, + "issuer": { + "type": "string", + "description": "The issuer Keycloak's tokens name and oidc consumers are told, as the realm's URL, such as https://keycloak.example.org/realms/Example; the operator's, with no default (novox/hq ADR 0112, ADR 0155).", + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "consumers", + "type": "file", + "path": "${dir:state}/consumers.json", + "mode": "0600", + "template": "files/consumers.json" + }, + { + "id": "admin-env", + "type": "file", + "path": "${dir:state}/admin.env", + "mode": "0600", + "template": "files/admin.env" + }, + { + "id": "database-env", + "type": "file", + "path": "${dir:state}/database.env", + "mode": "0600", + "template": "files/database.env" + }, + { + "id": "net", + "type": "network", + "name": "keycloak" + }, + { + "id": "hostname", + "type": "file", + "path": "${dir:state}/hostname.env", + "mode": "0644", + "template": "files/hostname.env" + }, + { + "id": "server", + "type": "container", + "name": "keycloak", + "image": "quay.io/keycloak/keycloak@sha256:ecd43971114b0c764f8a3288dddab73f98cb473daccc4feaffe4dc14adeaf866", + "network": "keycloak", + "args": [ + "start-dev" + ], + "env": { + "KC_DB": "postgres", + "KC_HTTP_ENABLED": "true", + "KC_HEALTH_ENABLED": "true", + "KC_PROXY_HEADERS": "xforwarded" + }, + "env-file": [ + "${dir:state}/admin.env", + "${dir:state}/database.env", + "${dir:state}/hostname.env" + ], + "ports": [ + "8080" + ], + "secrets-in-environment": "KC_DB_PASSWORD is convertible through a generated keycloak.conf (db-password=); KEYCLOAK_ADMIN_PASSWORD is env-only before Keycloak 26; not yet converted", + "restart-on": [ + "hostname" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": [ + "url", + "user", + "realm", + "issuer" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/keycloak-provider", + "binary": "keycloak-provider", + "loads": [ + "keycloak-provider" + ], + "env": { + "MESH_KEYCLOAK_URL": "http://127.0.0.1:${port:8080}", + "MESH_KEYCLOAK_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_KEYCLOAK_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_KEYCLOAK_CONTAINER": "keycloak", + "MESH_CONSUMERS_FILE": "${dir:state}/consumers.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "state": [ + "bound" + ], + "reads": [ + "mesh-controller.assignments" + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured new file mode 100644 index 0000000..e23963d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured @@ -0,0 +1,129 @@ +{ + "module": "lab", + "version": "1", + "capabilities": [ + "container-runtime", + "virtualisation" + ], + "requires": [ + "npm-package-registry" + ], + "contributes": { + "npm-package-registry": { + "access": "read" + } + }, + "binds": { + "npm-package-registry": "${dir:state}/package-registry.json" + }, + "secrets": { + "npm-package-registry": "${dir:state}/package-registry.secret" + }, + "data": { + "own": [ + { + "id": "work", + "path": "${dir:work}", + "class": "cache", + "why": "the lab's runs, each thrown away" + } + ] + }, + "config": { + "forge": { + "type": "string", + "description": "The forge the lab's machines take their packages from, as its address: the operator's, with no default (novox/hq ADR 0112).", + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-lab-runs", + "mode": "0700", + "unknown": "program" + }, + { + "id": "runtime-env", + "type": "file", + "path": "${dir:state}/lab.env", + "mode": "0600", + "template": "files/lab.env" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "make", + "type": "package", + "package": "make" + }, + { + "id": "python", + "type": "package", + "package": "python" + }, + { + "id": "file", + "type": "package", + "package": "file" + }, + { + "id": "iproute2", + "type": "package", + "package": "iproute2" + }, + { + "id": "npm", + "type": "package", + "package": "npm" + }, + { + "id": "go", + "type": "package", + "package": "go" + }, + { + "id": "incus", + "type": "package", + "package": "incus" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_LAB_WORK": "${dir:work}", + "MESH_LAB_ENV_FILE": "${dir:state}/lab.env", + "MESH_LAB_FORGE_BINDING": "${dir:state}/package-registry.json", + "MESH_LAB_FORGE_SECRET": "${dir:state}/package-registry.secret" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/lemurs/module.json.captured b/go/testdata/beside/mesh-catalog/modules/lemurs/module.json.captured new file mode 100644 index 0000000..702a959 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/lemurs/module.json.captured @@ -0,0 +1,84 @@ +{ + "module": "lemurs", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager", + "seat" + ], + "claims": [ + { + "name": "node-login-manager", + "scope": "node", + "serves": [ + "sessions" + ] + } + ], + "tools": [ + "lemurs_default_session", + "lemurs_logins" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "lemurs" + }, + { + "id": "xserver", + "type": "file", + "path": "/etc/lemurs/xserver", + "mode": "0755", + "template": "files/xserver" + }, + { + "id": "config", + "type": "file", + "path": "/etc/lemurs/config.toml", + "mode": "0644", + "template": "files/config.toml" + }, + { + "id": "xsessions", + "type": "directory", + "path": "/etc/lemurs/xsessions", + "mode": "0755", + "unknown": "program" + }, + { + "id": "wayland-sessions", + "type": "directory", + "path": "/etc/lemurs/wayland-sessions", + "mode": "0755", + "unknown": "program" + }, + { + "id": "service", + "type": "service", + "unit": "lemurs.service", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/lemurs-tools", + "binary": "lemurs-tools", + "loads": [ + "lemurs-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/letta/module.json.captured b/go/testdata/beside/mesh-catalog/modules/letta/module.json.captured new file mode 100644 index 0000000..de7baf6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/letta/module.json.captured @@ -0,0 +1,156 @@ +{ + "module": "letta", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database", + "route" + ], + "contributes": { + "postgres-database": { + "name": "letta", + "extensions": [ + "vector" + ] + }, + "route": { + "label": "letta", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "own-secrets": { + "server-password": { + "path": "${dir:state}/server-password.secret", + "taken": "at-start" + }, + "openai-api-key": { + "path": "${dir:state}/openai-api-key.secret", + "taken": "at-start", + "issued-by": "outside" + } + }, + "listens": [ + { + "name": "web", + "port": 8283, + "protocol": "tcp", + "from": "mesh", + "why": "the Letta agent server REST API and web UI, password-protected (--secure); a public name is the route's" + } + ], + "config": { + "url": { + "type": "string", + "description": "The address the module's tools reach Letta at, as a URL; unset, the code falls back to its environment, else this machine's Letta on port 8283.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "pgpass", + "type": "file", + "path": "${dir:state}/pgpass", + "mode": "0600", + "template": "files/pgpass" + }, + { + "id": "start", + "type": "file", + "path": "${dir:state}/start.sh", + "mode": "0644", + "template": "files/start.sh" + }, + { + "id": "net", + "type": "network", + "name": "letta" + }, + { + "id": "server", + "type": "container", + "name": "letta", + "image": "letta/letta@sha256:bfd1e49ce45b9a208c941e832c1d1d194017ff210a3784b0ca6c323aed767a29", + "network": "letta", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "8283" + ], + "volumes": [ + "${dir:state}/pgpass:/run/secrets/pgpass:ro", + "${dir:state}/start.sh:/run/letta/start.sh:ro" + ], + "args": [ + "sh", + "/run/letta/start.sh" + ], + "secrets-in-environment": "letta 0.6.x reads its settings from the environment only (pydantic settings, no secrets_dir or _FILE twin): LETTA_SERVER_PASSWORD and OPENAI_API_KEY have no file source. The database password is not here: LETTA_PG_URI, which letta prints at start, names no password, and libpq reads it from the mounted pgpass file (PGPASSFILE)" + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["url"], + "protected": ["password"] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_LETTA_URL": "http://127.0.0.1:${port:8283}", + "MESH_LETTA_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/local-model-consumer/module.json.captured b/go/testdata/beside/mesh-catalog/modules/local-model-consumer/module.json.captured new file mode 100644 index 0000000..7300209 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/local-model-consumer/module.json.captured @@ -0,0 +1,42 @@ +{ + "module": "local-model-consumer", + "version": "1", + "slug": "local", + "requires": [ + "model-access" + ], + "binds": { + "model-access": "${dir:state}/model.json" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "openai-env", + "type": "file", + "path": "${dir:config}/openai.env", + "mode": "0600", + "template": "files/openai.env" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/localization/module.json.captured b/go/testdata/beside/mesh-catalog/modules/localization/module.json.captured new file mode 100644 index 0000000..0974ed3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/localization/module.json.captured @@ -0,0 +1,61 @@ +{ + "module": "localization", + "version": "1", + "capabilities": [ + "service-manager" + ], + "tools": [ + "localization_get", + "localization_time_zone", + "localization_locales", + "localization_keymaps" + ], + "resources": [ + { + "id": "locale", + "type": "file", + "path": "/etc/locale.conf", + "mode": "0644", + "template": "files/locale.conf" + }, + { + "id": "keymap", + "type": "file", + "path": "/etc/vconsole.conf", + "mode": "0644", + "template": "files/vconsole.conf" + }, + { + "id": "time-zone", + "type": "process", + "name": "localization-time-zone", + "artifact": "tools", + "run": [ + "./localization-tools", + "set-time-zone", + "Europe/Brussels" + ], + "run-once": true + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/localization-tools", + "binary": "localization-tools", + "loads": [ + "localization-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/logrotate/module.json.captured b/go/testdata/beside/mesh-catalog/modules/logrotate/module.json.captured new file mode 100644 index 0000000..8318451 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/logrotate/module.json.captured @@ -0,0 +1,58 @@ +{ + "module": "logrotate", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "logrotate_status", + "logrotate_configs", + "logrotate_check", + "logrotate_big_logs", + "logrotate_force", + "logrotate_journal_usage", + "logrotate_journal_vacuum" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "logrotate" + }, + { + "id": "config", + "type": "file", + "path": "/etc/logrotate.conf", + "mode": "0644", + "template": "files/logrotate.conf" + }, + { + "id": "timer", + "type": "service", + "unit": "logrotate.timer", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/logrotate-tools", + "binary": "logrotate-tools", + "loads": [ + "logrotate-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mailu/module.json.captured new file mode 100644 index 0000000..568be14 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/module.json.captured @@ -0,0 +1,772 @@ +{ + "module": "mailu", + "version": "1", + "upgrade": { + "policy": "record", + "why": "people's mail: any change to how its containers are declared recreates them together in one send, and the mail is down for everyone until they are up again — a person chooses the moment (hq ADR 0242, issue 295)" + }, + "unhealthy": { + "severity": "urgent", + "why": "while mailu is broken, mail is refused or stuck for everyone with a mailbox here (hq ADR 0355, issue 687)" + }, + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database", + "route", + "secret" + ], + "contributes": { + "postgres-database": { + "name": "mailu" + }, + "route": { + "web": { + "label": "mail", + "endpoint": "web-tls", + "scheme": "https", + "insecure": true + }, + "acme": { + "label": "mail", + "path": "/.well-known/acme-challenge", + "endpoint": "web", + "priority": 100 + }, + "autoconfig": { + "label": "autoconfig", + "endpoint": "autoconfig" + }, + "autodiscover": { + "label": "autodiscover", + "endpoint": "autoconfig" + }, + "automx": { + "label": "automx", + "endpoint": "autoconfig" + } + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "secret": { + "secret-key": "${dir:state}/secret-key.secret", + "admin": "${dir:state}/admin.secret", + "api-token": "${dir:state}/api-token.secret" + } + }, + "tools": [ + "mailu_list_users", + "mailu_create_user", + "mailu_change_password", + "mailu_delete_user", + "mailu_list_aliases", + "mailu_create_alias", + "mailu_delete_alias", + "mailu_list_domains", + "mailu_read_mail", + "mailu_search_mail", + "mailu_health" + ], + "emits": [ + "user.created", + "user.deleted", + "alias.created", + "alias.deleted" + ], + "listens": [ + { + "name": "smtp", + "port": 25, + "protocol": "tcp", + "from": "anywhere", + "why": "mail from other mail servers", + "fixed": true + }, + { + "name": "pop3", + "port": 110, + "protocol": "tcp", + "from": "anywhere", + "why": "POP3, kept at parity with the predecessor; pruning legacy protocols is its own deliberate change", + "fixed": true + }, + { + "name": "imap", + "port": 143, + "protocol": "tcp", + "from": "anywhere", + "why": "IMAP with STARTTLS, kept at parity", + "fixed": true + }, + { + "name": "smtps", + "port": 465, + "protocol": "tcp", + "from": "anywhere", + "why": "submission over TLS", + "fixed": true + }, + { + "name": "submission", + "port": 587, + "protocol": "tcp", + "from": "anywhere", + "why": "submission; also what the smtp provision serves consumers", + "fixed": true + }, + { + "name": "imaps", + "port": 993, + "protocol": "tcp", + "from": "anywhere", + "why": "IMAP over TLS", + "fixed": true + }, + { + "name": "pop3s", + "port": 995, + "protocol": "tcp", + "from": "anywhere", + "why": "POP3 over TLS, kept at parity", + "fixed": true + }, + { + "name": "web", + "port": 7080, + "protocol": "tcp", + "from": "mesh", + "why": "the web front over http; only the ACME HTTP-01 passthrough is routed here — everything else 301s to https and would loop a proxy" + }, + { + "name": "web-tls", + "port": 7443, + "protocol": "tcp", + "from": "mesh", + "why": "the web front over its own TLS (admin, webmail, API); its public name is a route grant reaching it here" + }, + { + "name": "autoconfig", + "port": 4243, + "protocol": "tcp", + "from": "mesh", + "why": "automx: mail client autoconfiguration; the autoconfig, autodiscover and automx names are route grants reaching it here" + }, + { + "name": "admin-api", + "port": 8080, + "protocol": "tcp", + "from": "machine", + "why": "the admin API, which this module's own code reaches on loopback from the node's runtime now that it runs outside the mailu network" + } + ], + "data": { + "own": [ + { + "id": "mail", + "path": "${dir:data-mail}", + "class": "valuable", + "why": "every mailbox" + }, + { + "id": "dkim", + "path": "${dir:data-dkim}", + "class": "valuable", + "why": "the domain's signing keys; a new one means a new DNS record" + }, + { + "id": "data", + "path": "${dir:data-data}", + "class": "valuable", + "why": "the server's own data" + }, + { + "id": "dav", + "path": "${dir:data-dav}", + "class": "valuable", + "why": "calendars and contacts" + }, + { + "id": "webmail", + "path": "${dir:data-webmail}", + "class": "valuable", + "why": "the webmail's own data: its users' settings and address books" + }, + { + "id": "queue", + "path": "${dir:data-mailqueue}", + "class": "valuable", + "why": "mail accepted and not yet delivered, kept nowhere else" + }, + { + "id": "filter", + "path": "${dir:data-filter}", + "class": "rebuildable", + "why": "the spam filter's learned statistics; it learns again" + }, + { + "id": "certs", + "path": "${dir:data-certs}", + "class": "rebuildable", + "why": "certificates, issued again" + }, + { + "id": "automx", + "path": "${dir:data-automx}", + "class": "rebuildable", + "why": "client autoconfiguration, written again" + }, + { + "id": "fetchmail", + "path": "${dir:data-fetchmail}", + "class": "rebuildable", + "why": "which remote messages were fetched; lost, some are fetched twice" + }, + { + "id": "clamav", + "path": "${dir:data-clamav}", + "class": "cache", + "why": "virus signatures, downloaded again" + }, + { + "id": "redis", + "path": "${dir:data-redis}", + "class": "cache", + "why": "the filter's working set" + } + ], + "consumers": { + "smtp": { + "class": "valuable", + "in": "mail", + "why": "a consumer's mailbox holds the only copy of its mail" + } + } + }, + "config": { + "domain": { + "type": "string", + "description": "The mail domain Mailu serves, such as example.org: its first mail domain, the postmaster's and the one consumers send as; the operator's, with no default (novox/hq ADR 0112).", + "trusted": true, + "required": true + }, + "proxy-address": { + "type": "string", + "description": "The address of the reverse proxy in front of Mailu, whose X-Real-IP header Mailu believes; the operator's, with no default.", + "trusted": true, + "required": true + }, + "sitename": { + "type": "string", + "description": "The name Mailu's web pages show for the mail service; the operator's, with no default.", + "trusted": true, + "required": true + }, + "website": { + "type": "string", + "description": "The address of the website Mailu's web pages link to, as a URL; the operator's, with no default.", + "trusted": true, + "required": true + }, + "url": { + "type": "string", + "description": "The address the module's tools reach Mailu's API at, as a URL; empty leaves them to MESH_MAILU_URL.", + "trusted": true, + "default": "" + }, + "container": { + "type": "string", + "description": "The name of the container holding Mailu's mailboxes, which the module's tools read mail in; empty is mailu-imap.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-automx", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "config-env", + "type": "file", + "path": "${dir:state}/mailu.env", + "mode": "0644", + "template": "files/mailu.env" + }, + { + "id": "secret-env", + "type": "file", + "path": "${dir:state}/secret.env", + "mode": "0600", + "template": "files/secret.env" + }, + { + "id": "database-env", + "type": "file", + "path": "${dir:state}/database.env", + "mode": "0600", + "template": "files/database.env" + }, + { + "id": "admin-env", + "type": "file", + "path": "${dir:state}/admin.env", + "mode": "0600", + "template": "files/admin.env" + }, + { + "id": "data-certs", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-data", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-dkim", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-mail", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-mailqueue", + "type": "directory", + "mode": "0755", + "unknown": "program" + }, + { + "id": "data-filter", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-clamav", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-redis", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-webmail", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-dav", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-fetchmail", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-overrides-nginx", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-overrides-dovecot", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-overrides-postfix", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-overrides-rspamd", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data-overrides-roundcube", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "mailu" + }, + { + "id": "resolver", + "type": "container", + "name": "mailu-resolver", + "image": "ghcr.io/mailu/unbound@sha256:2c037bc5c988b5c0ab7e1b6e6ffa762e14ba1e45c96281ac87f8b9be8eee8b6e", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env", + "${dir:state}/secret.env" + ], + "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified", + "ip": "192.168.203.254" + }, + { + "id": "redis", + "type": "container", + "name": "mailu-redis", + "image": "redis@sha256:4bed291aa5efb9f0d77b76ff7d4ab71eee410962965d052552db1fb80576431d", + "network": "mailu", + "volumes": [ + "${dir:data-redis}:/data" + ] + }, + { + "id": "admin", + "type": "container", + "name": "mailu-admin", + "image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "ports": [ + "8080" + ], + "env-file": [ + "${dir:state}/mailu.env", + "${dir:state}/secret.env", + "${dir:state}/database.env", + "${dir:state}/admin.env" + ], + "volumes": [ + "${dir:data-data}:/data", + "${dir:data-dkim}:/dkim" + ], + "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified" + }, + { + "id": "imap", + "type": "container", + "name": "mailu-imap", + "image": "ghcr.io/mailu/dovecot@sha256:7f0ed5db996fbdc00adc5c5e38a08492e04f7eb4a9fbd66a03aa9a28ddf23993", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env" + ], + "volumes": [ + "${dir:data-mail}:/mail", + "${dir:data-overrides-dovecot}:/overrides:ro" + ], + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "smtp", + "type": "container", + "name": "mailu-smtp", + "image": "ghcr.io/mailu/postfix@sha256:e2e49f39e53b80eac9e7a2f18d9df11edeb4914fd62dbba89b3155e8e034f62e", + "health": { + "kind": "tool", + "tool": "mailu_health", + "interval": "60s", + "timeout": "30s", + "looks": 3, + "grace": "120s" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env" + ], + "volumes": [ + "${dir:data-mailqueue}:/queue", + "${dir:data-overrides-postfix}:/overrides:ro" + ], + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "antispam", + "type": "container", + "name": "mailu-antispam", + "image": "ghcr.io/mailu/rspamd@sha256:ff3666d8a61f17d309c5c6f6bcf4d40470b82299ca706ac650301175bb1a079d", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env" + ], + "volumes": [ + "${dir:data-filter}:/var/lib/rspamd", + "${dir:data-overrides-rspamd}:/etc/rspamd/override.d:ro" + ], + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "antivirus", + "type": "container", + "name": "mailu-antivirus", + "image": "clamav/clamav-debian@sha256:b12ef8fefddbba7d88de59bea8a32622f365339154adf02d38fd089112e6745a", + "network": "mailu", + "volumes": [ + "${dir:data-clamav}:/var/lib/clamav" + ], + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "webmail", + "type": "container", + "name": "mailu-webmail", + "image": "ghcr.io/mailu/webmail@sha256:bdbee44cdb05a4658f0e3b62cc448de55ca8f8aea172279fda594826144c04f6", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env", + "${dir:state}/secret.env" + ], + "volumes": [ + "${dir:data-webmail}:/data", + "${dir:data-overrides-roundcube}:/overrides:ro" + ], + "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified", + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "webdav", + "type": "container", + "name": "mailu-webdav", + "image": "ghcr.io/mailu/radicale@sha256:690ed6edf189dfef100a5a8b37c195ebf5d9241ac5f23f2f44b8b7b75726e3de", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env", + "${dir:state}/secret.env" + ], + "volumes": [ + "${dir:data-dav}:/data" + ], + "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified", + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "fetchmail", + "type": "container", + "name": "mailu-fetchmail", + "image": "ghcr.io/mailu/fetchmail@sha256:f881c8412d3bbe73d638469b48321558d6403a9d45bfa043c1e52c752103d42d", + "health": { + "kind": "runtime" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env", + "${dir:state}/secret.env" + ], + "volumes": [ + "${dir:data-fetchmail}:/data" + ], + "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified", + "dns": [ + "192.168.203.254" + ] + }, + { + "id": "front", + "type": "container", + "name": "mailu-front", + "image": "ghcr.io/mailu/nginx@sha256:36f98897cd1bc9d27628bbb4e04bdf60147af2ec7507d6da77f002c4f256896d", + "health": { + "kind": "exec", + "command": "curl -m3 -skfLo /dev/null http://127.0.0.1:10204/health && kill -0 $(cat /run/dovecot/master.pid) && python3 -c \"import smtplib; s = smtplib.SMTP('127.0.0.1', 25, timeout=5); s.ehlo('health'); s.quit()\" && openssl x509 -checkend 1209600 -noout -in /certs/letsencrypt/live/mailu/nginx-chain.pem", + "interval": "60s", + "timeout": "15s", + "looks": 3, + "grace": "120s" + }, + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env" + ], + "ports": [ + "25", + "110", + "143", + "465", + "587", + "993", + "995", + "7080:80", + "7443:443" + ], + "volumes": [ + "${dir:data-certs}:/certs", + "${dir:data-overrides-nginx}:/overrides:ro" + ], + "dns": [ + "192.168.203.254" + ], + "logging": "journald" + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "takes-config": ["domain", "proxy-address", "sitename", "website", "url", "container"], + "merge": "json" + }, + { + "id": "automx", + "type": "container", + "name": "mailu-automx", + "artifact": "automx", + "network": "mailu", + "env-file": [ + "${dir:state}/mailu.env" + ], + "ports": [ + "4243" + ], + "volumes": [ + "${dir:data-automx}:/data" + ] + } + ], + "build": { + "on": [ + { + "arg": "PYTHON_BASE", + "image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228" + } + ], + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_MAILU_URL": "http://127.0.0.1:${port:8080}/api/v1", + "MESH_MAILU_API_KEY_FILE": "${dir:state}/api-token.secret", + "MESH_MAILU_IMAP_CONTAINER": "mailu-imap", + "MESH_MAILU_SMTP_CONTAINER": "mailu-smtp", + "MESH_MAILU_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_RECEIVES": "${dir:grants}/mesh.json" + } + }, + { + "name": "automx", + "kind": "image", + "from": "automx/Dockerfile" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "provides": [ + { + "name": "smtp", + "scope": "mesh", + "identity": { + "max": 64, + "in": "a mailbox's local part" + } + } + ], + "serves": { + "smtp": { + "port": 587, + "domain": "${config:domain}" + } + }, + "receives": { + "smtp": "${dir:grants}/mesh.json" + }, + "grants": { + "smtp": "${dir:grants}" + }, + "jails": [ + { + "name": "mailu-front", + "failregex": "^.*(?:imap|pop3|submission|managesieve)-login: .*\\(auth failed, \\d+ attempts(?: in \\d+ secs)?\\):.*rip=(?:,|$)\n ^.*client login failed: \"AUTH not supported\" while in http auth state, client: , server: \\S+:25,", + "jail": "backend = systemd\njournalmatch = CONTAINER_NAME=mailu-front\nport = smtp,submission,submissions,imap,imaps,pop3,pop3s\nmaxretry = 3\nfindtime = 1d\nbantime = 1d" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/marrytts/module.json.captured b/go/testdata/beside/mesh-catalog/modules/marrytts/module.json.captured new file mode 100644 index 0000000..ed242aa --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/marrytts/module.json.captured @@ -0,0 +1,37 @@ +{ + "module": "marrytts", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "api", + "port": 59125, + "protocol": "tcp", + "from": "mesh", + "why": "the MaryTTS text-to-speech HTTP API and web interface" + } + ], + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "marrytts", + "image": "synesthesiam/marytts@sha256:45970ecb3e21a2981c66c60563a70cf00be8e95c02565e7d74b3a73dcec7db2c", + "env": { + "TZ": "Europe/Brussels" + }, + "ports": [ + "59125" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/matrix/module.json.captured b/go/testdata/beside/mesh-catalog/modules/matrix/module.json.captured new file mode 100644 index 0000000..170719c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/matrix/module.json.captured @@ -0,0 +1,257 @@ +{ + "module": "matrix", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "client", + "port": 6167, + "protocol": "tcp", + "from": "mesh", + "why": "Conduit's client-server and federation APIs over plain HTTP. Both arrive through the route on 443: Conduit answers /.well-known/matrix/server with :443, so other homeservers federate through the proxy and nothing needs the traditional 8448" + }, + { + "name": "web", + "port": 80, + "protocol": "tcp", + "from": "mesh", + "why": "Element Web, the static browser client, served by the image's nginx; reached through its route" + } + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "homeserver": { + "label": "matrix", + "endpoint": "client" + }, + "element": { + "label": "element", + "endpoint": "web" + } + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "data": { + "own": [ + { + "id": "db", + "path": "${dir:db}", + "class": "valuable", + "why": "every room, message and account" + } + ] + }, + "config": { + "default_server_name": { + "type": "string", + "description": "Element's default_server_name setting of config.json: the homeserver Element signs in to by default, by name; the file's default is the module's own route.", + "trusted": true, + "default": "${bound:route:name-homeserver}" + }, + "default_server_config": { + "type": "object", + "description": "Element's default_server_config setting of config.json: the homeserver and identity server Element uses by default, by URL.", + "trusted": true, + "default": { + "m.homeserver": { + "base_url": "https://${bound:route:name-homeserver}" + }, + "m.identity_server": { + "base_url": "https://vector.im" + } + } + }, + "brand": { + "type": "string", + "description": "Element's brand setting of config.json: the name Element shows itself as.", + "trusted": true, + "default": "Element" + }, + "integrations_ui_url": { + "type": "string", + "description": "Element's integrations_ui_url setting of config.json: the integration manager's page, a URL.", + "trusted": true, + "default": "https://scalar.vector.im/" + }, + "integrations_rest_url": { + "type": "string", + "description": "Element's integrations_rest_url setting of config.json: the integration manager's API, a URL.", + "trusted": true, + "default": "https://scalar.vector.im/api" + }, + "integrations_widgets_urls": { + "type": "list", + "description": "Element's integrations_widgets_urls setting of config.json: the URLs whose widgets Element trusts as the integration manager's.", + "trusted": true, + "default": [ + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ] + }, + "bug_report_endpoint_url": { + "type": "string", + "description": "Element's bug_report_endpoint_url setting of config.json: where Element sends a bug report, a URL; its default here is empty, and the file's own value (Element's, naming outside servers) stays in the file and is never replaced unless this is set.", + "trusted": true, + "default": "" + }, + "uisi_autorageshake_app": { + "type": "string", + "description": "Element's uisi_autorageshake_app setting of config.json: the app name Element files an automatic report of an undecryptable message under.", + "trusted": true, + "default": "element-auto-uisi" + }, + "show_labs_settings": { + "type": "boolean", + "description": "Element's show_labs_settings setting of config.json: whether Element shows its Labs settings.", + "trusted": true, + "default": true + }, + "room_directory": { + "type": "object", + "description": "Element's room_directory setting of config.json: the servers whose room directories Element offers; its default here is empty, and the file's own value (Element's, naming outside servers) stays in the file and is never replaced unless this is set.", + "trusted": true, + "default": {} + }, + "enable_presence_by_hs_url": { + "type": "object", + "description": "Element's enable_presence_by_hs_url setting of config.json: whether Element shows presence, per homeserver URL.", + "trusted": true, + "default": { + "https://matrix.org": false, + "https://matrix-client.matrix.org": false + } + }, + "terms_and_conditions_links": { + "type": "list", + "description": "Element's terms_and_conditions_links setting of config.json: the links Element shows as its terms, each a URL and its text; its default here is empty, and the file's own value (Element's, naming outside servers) stays in the file and is never replaced unless this is set.", + "trusted": true, + "default": [] + }, + "features": { + "type": "object", + "description": "Element's features setting of config.json: Element's feature switches, each on or off.", + "trusted": true, + "default": { + "feature_video_rooms": true, + "feature_rust_crypto": true + } + }, + "element_call": { + "type": "object", + "description": "Element's element_call setting of config.json: where Element starts a call with Element Call, a URL.", + "trusted": true, + "default": { + "url": "https://call.element.dev" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "db", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "conduit-conf", + "type": "file", + "path": "${dir:state}/conduit.toml", + "mode": "0644", + "template": "files/conduit.toml", + "names-on-purpose": { + "matrix.org": "the federation's public key server, trusted by default; the world's, not this mesh's" + } + }, + { + "id": "element-conf", + "type": "file", + "path": "${dir:state}/element.json", + "mode": "0644", + "merge": "json", + "takes-config": ["default_server_name", "default_server_config", "brand", "integrations_ui_url", "integrations_rest_url", "integrations_widgets_urls", "bug_report_endpoint_url", "uisi_autorageshake_app", "show_labs_settings", "room_directory", "enable_presence_by_hs_url", "terms_and_conditions_links", "features", "element_call"], + "template": "files/element.json", + "names-on-purpose": { + "matrix.org": "the public room directory and the federation's largest homeserver; the world's", + "matrix-client.matrix.org": "the same homeserver's client endpoint; the world's", + "vector.im": "Element's public identity server; the world's", + "scalar.vector.im": "Element's public integration manager; the world's", + "scalar-staging.vector.im": "Element's staging integration manager, named by the upstream default config; the world's", + "scalar-staging.riot.im": "the same, under its former name; the world's", + "element.io": "Element's bug reports, privacy and cookie pages; the world's", + "gitter.im": "a public room directory; the world's", + "libera.chat": "a public room directory; the world's", + "call.element.dev": "Element Call's public instance; the world's" + } + }, + { + "id": "net", + "type": "network", + "name": "matrix" + }, + { + "id": "homeserver", + "type": "container", + "name": "matrix", + "image": "matrixconduit/matrix-conduit@sha256:b0d24248e94f944ca49f90f10c429e3d65f4472bdde25661ecea9840134fb133", + "network": "matrix", + "env": { + "CONDUIT_CONFIG": "/etc/conduit/conduit.toml" + }, + "ports": [ + "6167" + ], + "volumes": [ + "${dir:db}:/var/lib/matrix-conduit", + "${dir:state}/conduit.toml:/etc/conduit/conduit.toml:ro" + ], + "restart-on": [ + "conduit-conf" + ] + }, + { + "id": "element", + "type": "container", + "name": "element-web", + "image": "vectorim/element-web@sha256:a8f415462ab8d2600a592ba1b92bea51efe5a4d10eb738aab9bed769f7099613", + "health": { + "kind": "runtime" + }, + "network": "matrix", + "ports": [ + "80" + ], + "volumes": [ + "${dir:state}/element.json:/app/config.json:ro" + ], + "restart-on": [ + "element-conf" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/module.json.captured b/go/testdata/beside/mesh-catalog/modules/memory-pressure/module.json.captured new file mode 100644 index 0000000..1af96e3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/module.json.captured @@ -0,0 +1,130 @@ +{ + "module": "memory-pressure", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "emits": [ + "pressure.high", + "pressure.cleared" + ], + "tools": [ + "memory_status", + "memory_top", + "memory_oom_history", + "memory_zram", + "memory_oomd", + "memory_guard" + ], + "resources": [ + { + "id": "zram-generator", + "type": "package", + "package": "zram-generator" + }, + { + "id": "zram", + "type": "file", + "path": "/etc/systemd/zram-generator.conf", + "mode": "0644", + "template": "files/zram-generator.conf" + }, + { + "id": "sysctl-drop-ins", + "type": "directory", + "path": "/etc/sysctl.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "swap-tunables", + "type": "file", + "path": "/etc/sysctl.d/90-memory-pressure.conf", + "mode": "0644", + "template": "files/sysctl.conf" + }, + { + "id": "sysctl", + "type": "service", + "unit": "systemd-sysctl.service", + "restart-on": [ + "swap-tunables" + ] + }, + { + "id": "oomd-drop-ins", + "type": "directory", + "path": "/etc/systemd/oomd.conf.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "oomd-limits", + "type": "file", + "path": "/etc/systemd/oomd.conf.d/memory-pressure.conf", + "mode": "0644", + "template": "files/oomd.conf" + }, + { + "id": "root-slice-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/-.slice.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "root-slice", + "type": "file", + "path": "/etc/systemd/system/-.slice.d/10-oomd.conf", + "mode": "0644", + "template": "files/root-slice.conf" + }, + { + "id": "user-manager-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/user@.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "user-manager", + "type": "file", + "path": "/etc/systemd/system/user@.service.d/10-oomd.conf", + "mode": "0644", + "template": "files/user-manager.conf" + }, + { + "id": "oomd", + "type": "service", + "unit": "systemd-oomd.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "oomd-limits", + "root-slice", + "user-manager" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/memory-pressure", + "binary": "memory-pressure", + "loads": [ + "memory-pressure" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-catalog/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-catalog/module.json.captured new file mode 100644 index 0000000..8d8c659 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-catalog/module.json.captured @@ -0,0 +1,98 @@ +{ + "module": "mesh-catalog", + "slug": "catalog", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "claims": [ + { + "name": "mesh-catalog", + "scope": "mesh" + } + ], + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "mesh_catalog" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "consumes": [ + "node-build-agent.built", + "mesh-build-machine.built", + "mesh-controller.built-before" + ], + "emits": [ + "registered", + "upgraded", + "rebuild-needed", + "catching-up" + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "template": "files/database.url" + }, + { + "id": "prepare", + "type": "process", + "name": "mesh-catalog-prepare", + "artifact": "code", + "run": [ + "node", + "prepare/index.js" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "prepare/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-cli/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-cli/module.json.captured new file mode 100644 index 0000000..681a3e5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-cli/module.json.captured @@ -0,0 +1,37 @@ +{ + "module": "mesh-cli", + "version": "1", + "moved": true, + "resources": [ + { + "id": "program", + "type": "archive", + "artifact": "program", + "path": "/usr/local/bin", + "unknown": "program" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "program", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-cli", + "binary": "mesh-cli" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-config/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-config/module.json.captured new file mode 100644 index 0000000..e03e891 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-config/module.json.captured @@ -0,0 +1,74 @@ +{ + "module": "mesh-config", + "version": "1", + "runs-as": "mesh-config", + "claims": [ + { + "name": "mesh-config", + "scope": "mesh", + "serves": [ + "config" + ] + } + ], + "emits": [ + "config-changed" + ], + "uses": [ + "operator-channel" + ], + "invokes": [ + "seat:mesh-controller.hand-act", + "seat:mesh-controller.warranted" + ], + "reads": [ + "mesh-controller.assignments", + "mesh-controller.declared-config", + "mesh-controller.predefined" + ], + "state": [ + "values", + "resolved", + "predefined", + "proposals" + ], + "logs": [ + "history" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-config", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-config", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-config" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-config", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-config", + "binary": "mesh-config", + "loads": [ + "mesh-config" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-delivery-gate/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-delivery-gate/module.json.captured new file mode 100644 index 0000000..ec83852 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-delivery-gate/module.json.captured @@ -0,0 +1,110 @@ +{ + "module": "mesh-delivery-gate", + "version": "1", + "slug": "gate", + "claims": [ + { + "name": "mesh-delivery", + "scope": "mesh", + "serves": [ + "deliveries", + "times", + "show", + "groups", + "what-if", + "checks", + "table", + "stalled", + "recheck", + "release", + "stop", + "close", + "retire-history" + ] + } + ], + "consumes": [ + "gitea.pull.updated", + "gitea.pull.merged", + "gitea.pull.closed", + "gitea.forge.status", + "gitea.forge.pull-request.edited", + "gitea.forge.pull-request.closed", + "mesh-controller.checked", + "mesh-controller.plan-moved", + "mesh-delivery-forge-bridge.forge.**" + ], + "emits": [ + "transition", + "group", + "merged" + ], + "invokes": [ + "seat:mesh-controller.deliver", + "seat:mesh-controller.delivery-stop", + "seat:mesh-controller.delivery-backups-again", + "seat:mesh-controller.delivery-walks", + "seat:mesh-controller.delivery-check", + "seat:mesh-controller.forget-checks", + "seat:mesh-controller.plans", + "gitea.gitea_commit_statuses", + "gitea.gitea_contains", + "gitea.gitea_open_pulls" + ], + "state": [ + "deliveries", + "groups", + "pulls", + { + "name": "asks", + "history": 5 + }, + { + "name": "merges", + "history": 10 + }, + { + "name": "owed", + "history": 5 + }, + { + "name": "source", + "history": 64 + } + ], + "reads": [ + "mesh-delivery.deliveries", + "mesh-delivery.groups", + "mesh-delivery.pulls", + "mesh-delivery-teller.answers", + "mesh-controller.assignments", + "node-engine.have" + ], + "tools": [ + "delivery_status" + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-delivery-gate", + "binary": "mesh-delivery-gate", + "loads": [ + "mesh-delivery-gate" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured new file mode 100644 index 0000000..1a40896 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured @@ -0,0 +1,95 @@ +{ + "module": "mesh-delivery", + "version": "1", + "slug": "deliver", + "claims": [ + { + "name": "mesh-delivery", + "scope": "mesh", + "serves": [ + "deliveries", + "times", + "show", + "groups", + "what-if", + "checks", + "table", + "stalled", + "recheck", + "release", + "stop", + "close", + "retire-history" + ] + } + ], + "consumes": [ + "gitea.pull.updated", + "gitea.pull.merged", + "gitea.pull.closed", + "gitea.forge.status", + "gitea.forge.pull-request.edited", + "gitea.forge.pull-request.closed", + "mesh-controller.checked", + "mesh-controller.plan-moved" + ], + "emits": [ + "transition", + "group", + "merged" + ], + "invokes": [ + "seat:mesh-controller.delivery-plan", + "seat:mesh-controller.delivery-check", + "seat:mesh-controller.deliver", + "seat:mesh-controller.delivery-backups-again", + "seat:mesh-controller.delivery-stop", + "seat:mesh-controller.delivery-walks", + "seat:mesh-controller.forget-checks", + "gitea.gitea_note_append", + "gitea.gitea_delivery_view", + "gitea.gitea_commit_status", + "gitea.gitea_commit_statuses", + "gitea.gitea_contains", + "gitea.gitea_open_pulls", + "gitea.gitea_merge_pull_request", + "gitea.gitea_add_comment", + "seat:operator-channel.notify" + ], + "uses": [ + "operator-channel" + ], + "state": [ + "deliveries", + "groups", + "asked", + "pulls" + ], + "tools": [ + "delivery_status" + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-delivery", + "binary": "mesh-delivery", + "loads": [ + "mesh-delivery" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-files/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-files/module.json.captured new file mode 100644 index 0000000..cc1df16 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-files/module.json.captured @@ -0,0 +1,57 @@ +{ + "module": "mesh-files", + "version": "1", + "claims": [ + { + "name": "node-files", + "scope": "node", + "serves": [ + "stat", + "readlink", + "list", + "read", + "search", + "account", + "write", + "copy", + "move", + "delete", + "mkdir", + "chmod", + "chown", + "trash", + "restore" + ] + } + ], + "invokes": [ + "seat:operator-channel.tell" + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-files", + "binary": "mesh-files", + "loads": [ + "mesh-files" + ], + "env": { + "MESH_FILES_STATE": "${dir:state}" + } + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured new file mode 100644 index 0000000..592e81c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured @@ -0,0 +1,330 @@ +{ + "module": "mesh-issues", + "version": "1", + "slug": "issues", + "seats": [ + { + "name": "issue-tracker", + "scope": "mesh", + "serves": [ + "open", + "get", + "list", + "move", + "note", + "link", + "import", + "groom", + "set", + "tracking", + "reserve", + "fix-times" + ] + } + ], + "claims": [ + { + "name": "issue-tracker", + "scope": "mesh", + "serves": [ + "open", + "get", + "list", + "move", + "note", + "link", + "import", + "groom", + "set", + "tracking", + "reserve", + "fix-times" + ] + } + ], + "emits": [ + "opened", + "moved", + "noted", + "linked", + "imported", + "set", + "triaged", + "ready", + "ready-to-verify", + "reserved" + ], + "consumes": [ + "mesh-issues.opened", + "mesh-issues.moved", + "mesh-delivery.transition", + "mesh-delivery-gate.transition" + ], + "invokes": [ + "claude-code.claude_code_judge", + "seat:mesh-delivery.checks", + "seat:operator-channel.notify", + "gitea.gitea_pull_reviews", + "records.records_status", + "records.records_list", + "records.records_read", + "mesh-knowledge.knowledge_highest" + ], + "logs": [ + "changes" + ], + "state": [ + { + "name": "issues", + "history": 2 + }, + "counters", + "grooming", + "reservations" + ], + "config": { + "first-number": { + "type": "number", + "description": "The number the first issue the tracker numbers gets, read once when its counter starts and never again; the import reads it as the boundary between the folders hq numbered and the tracker's issues.", + "minimum": 1, + "trusted": true, + "default": 365 + }, + "owners": { + "type": "list", + "description": "The registered places an issue's located-in may name: a repository, or a repository and a folder in it separated by a space.", + "trusted": true, + "default": [ + "hq", + "mesh-catalog", + "mesh-catalog modules/adwaita", + "mesh-catalog modules/artifact-store-tools", + "mesh-catalog modules/asus-zephyrus-g14", + "mesh-catalog modules/audit-logger", + "mesh-catalog modules/avahi", + "mesh-catalog modules/baserow", + "mesh-catalog modules/betterbird", + "mesh-catalog modules/blueman", + "mesh-catalog modules/bluetooth", + "mesh-catalog modules/ca-trust", + "mesh-catalog modules/claude-code", + "mesh-catalog modules/claude-licence-manager", + "mesh-catalog modules/clipmenu", + "mesh-catalog modules/confluence", + "mesh-catalog modules/cups", + "mesh-catalog modules/dbus", + "mesh-catalog modules/desk-channel", + "mesh-catalog modules/disk-load", + "mesh-catalog modules/distribution", + "mesh-catalog modules/dmenu", + "mesh-catalog modules/dnsmasq", + "mesh-catalog modules/docker", + "mesh-catalog modules/docker-compose", + "mesh-catalog modules/dunst", + "mesh-catalog modules/fail2ban", + "mesh-catalog modules/feh", + "mesh-catalog modules/flatpak", + "mesh-catalog modules/fonts", + "mesh-catalog modules/forticlient", + "mesh-catalog modules/gitea", + "mesh-catalog modules/gitlab", + "mesh-catalog modules/gnome-keyring", + "mesh-catalog modules/grafana", + "mesh-catalog modules/hello-web", + "mesh-catalog modules/home-assistant", + "mesh-catalog modules/hostname", + "mesh-catalog modules/i3", + "mesh-catalog modules/i3status-rust", + "mesh-catalog modules/icecast", + "mesh-catalog modules/influxdb", + "mesh-catalog modules/invoicing", + "mesh-catalog modules/jira", + "mesh-catalog modules/keycloak", + "mesh-catalog modules/lab", + "mesh-catalog modules/lemurs", + "mesh-catalog modules/letta", + "mesh-catalog modules/local-model-consumer", + "mesh-catalog modules/localization", + "mesh-catalog modules/logrotate", + "mesh-catalog modules/mailu", + "mesh-catalog modules/marrytts", + "mesh-catalog modules/matrix", + "mesh-catalog modules/memory-pressure", + "mesh-catalog modules/mesh-catalog", + "mesh-catalog modules/mesh-cli", + "mesh-catalog modules/mesh-delivery", + "mesh-catalog modules/mesh-delivery-gate", + "mesh-catalog modules/mesh-issues", + "mesh-catalog modules/mesh-vault", + "mesh-catalog modules/mesh-watcher", + "mesh-catalog modules/messenger", + "mesh-catalog modules/minio", + "mesh-catalog modules/model-usage", + "mesh-catalog modules/mongodb", + "mesh-catalog modules/mosquitto", + "mesh-catalog modules/mounts", + "mesh-catalog modules/mssql", + "mesh-catalog modules/n8n", + "mesh-catalog modules/nats", + "mesh-catalog modules/netcheck", + "mesh-catalog modules/networkmanager", + "mesh-catalog modules/nextcloud", + "mesh-catalog modules/nextcloud-client", + "mesh-catalog modules/nfs-server", + "mesh-catalog modules/nftables", + "mesh-catalog modules/nm-applet", + "mesh-catalog modules/node-env", + "mesh-catalog modules/nodered", + "mesh-catalog modules/ollama", + "mesh-catalog modules/only-office", + "mesh-catalog modules/openai-consumer", + "mesh-catalog modules/openrazer", + "mesh-catalog modules/pacman", + "mesh-catalog modules/photos-eef", + "mesh-catalog modules/photos-filip", + "mesh-catalog modules/picom", + "mesh-catalog modules/polychromatic", + "mesh-catalog modules/postgres", + "mesh-catalog modules/power", + "mesh-catalog modules/powerlevel10k", + "mesh-catalog modules/records", + "mesh-catalog modules/redis", + "mesh-catalog modules/restic", + "mesh-catalog modules/rofi", + "mesh-catalog modules/route-adapter", + "mesh-catalog modules/screen-lock", + "mesh-catalog modules/searxng", + "mesh-catalog modules/sensors", + "mesh-catalog modules/showcase", + "mesh-catalog modules/slack", + "mesh-catalog modules/snapd", + "mesh-catalog modules/ssh-client", + "mesh-catalog modules/sshd", + "mesh-catalog modules/step-ca", + "mesh-catalog modules/sudo", + "mesh-catalog modules/supabase", + "mesh-catalog modules/systemd", + "mesh-catalog modules/systemd-networkd", + "mesh-catalog modules/systemd-resolved", + "mesh-catalog modules/telegram", + "mesh-catalog modules/time-sync", + "mesh-catalog modules/triggerhappy", + "mesh-catalog modules/umami", + "mesh-catalog modules/unifi", + "mesh-catalog modules/website", + "mesh-catalog modules/xclip", + "mesh-catalog modules/xdg", + "mesh-catalog modules/xorg", + "mesh-catalog modules/xterm", + "mesh-catalog modules/zfs", + "mesh-catalog modules/zsh", + "mesh-catalog modules/zsh-autosuggestions", + "mesh-catalog modules/zsh-syntax-highlighting", + "mesh-controller", + "mesh-controller modules/build-agent", + "mesh-controller modules/route-proxy", + "mesh-delivery", + "mesh-host", + "mesh-lab", + "mesh-media-catalog", + "mesh-sdk", + "mesh-tools" + ] + }, + "duplicate-check": { + "type": "string", + "description": "Whether a new issue is checked against the open ones for a duplicate: off stops it, anything else is on (novox/hq ADR 0278 §7).", + "trusted": true, + "default": "on" + }, + "judge-model": { + "type": "string", + "description": "The model the duplicate check asks, by the agent's name for it, such as sonnet; empty is the tracker's own default.", + "trusted": true, + "default": "sonnet" + }, + "forge-owner": { + "type": "string", + "description": "The owner on the forge of the repositories an issue's fixed-by names, for asking after a pull request's delivery (novox/hq ADR 0279 §3); empty asks no forge.", + "trusted": true, + "default": "novox" + }, + "stuck-after": { + "type": "string", + "description": "How long a fix may be merged and not delivered before it is said stuck, as a duration such as 2h (novox/hq ADR 0281); empty is the tracker's own default.", + "pattern": "|[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*", + "trusted": true, + "default": "2h" + }, + "worker-stale-after": { + "type": "string", + "description": "How long an issue's worker stays current with nothing done on it, as a duration such as 24h (novox/hq ADR 0284 §4); empty is the tracker's own default.", + "pattern": "|[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*", + "trusted": true, + "default": "24h" + }, + "delivered-not-resolved-after": { + "type": "string", + "description": "How long an issue may stay in fixing once every fix it names is delivered before the operator is warned, as a duration such as 1h (novox/hq issue 483); empty is the tracker's own default.", + "pattern": "|[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*", + "trusted": true, + "default": "1h" + }, + "decision-floor": { + "type": "number", + "description": "The highest decision number already taken somewhere the tracker cannot read, such as an open branch of hq; the decision sequence's first number is one above it, read once when the counter starts (novox/hq issue 424).", + "minimum": 0, + "trusted": true, + "default": 293 + }, + "research-floor": { + "type": "number", + "description": "The highest research number already taken somewhere the tracker cannot read; the research sequence's first number is one above it, read once when the counter starts (novox/hq issue 424).", + "minimum": 0, + "trusted": true, + "default": 45 + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "settings", + "trusted": true, + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "takes-config": ["first-number", "owners", "duplicate-check", "judge-model", "forge-owner", "stuck-after", "worker-stale-after", "delivered-not-resolved-after", "decision-floor", "research-floor"], + "merge": "json", + "template": "files/settings.json" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-issues", + "binary": "mesh-issues", + "loads": [ + "mesh-issues" + ], + "env": { + "MESH_ISSUES_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/module.json.captured new file mode 100644 index 0000000..92b15f2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/module.json.captured @@ -0,0 +1,218 @@ +{ + "module": "mesh-knowledge", + "version": "1", + "slug": "knowledge", + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "knowledge" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "seats": [ + { + "name": "knowledge-base", + "scope": "mesh", + "serves": [ + "ask", + "get", + "list", + "findings", + "holds", + "links", + "history", + "reserve", + "propose", + "import", + "return", + "withdraw" + ] + } + ], + "claims": [ + { + "name": "knowledge-base", + "scope": "mesh", + "serves": [ + "ask", + "get", + "list", + "findings", + "holds", + "links", + "history", + "reserve", + "propose", + "import", + "return", + "withdraw" + ] + } + ], + "tools": [ + "knowledge_health", + "knowledge_highest" + ], + "emits": [ + "reserved", + "proposed", + "reviewed", + "accepted", + "changed", + "imported", + "drafted", + "confirmed", + "returned" + ], + "uses": [ + "operator-channel" + ], + "invokes": [ + "seat:issue-tracker.reserve", + "claude-code.claude_code_judge", + "claude-code.claude_code_judged", + "records.records_status", + "records.records_list", + "records.records_read", + "seat:operator-channel.notify" + ], + "config": { + "judge-model": { + "type": "string", + "description": "The model the knowledge base's drafting and confirming judgements ask, by the agent's name for it, such as sonnet; empty is sonnet.", + "default": "sonnet" + }, + "drafting": { + "type": "string", + "description": "Whether the knowledge base drafts and confirms clauses itself: off stops it, anything else is on.", + "default": "on" + }, + "reserved-for": { + "type": "string", + "description": "How long a reserved number waits for its proposal before it is withdrawn, as a duration such as 168h; empty is the knowledge base's own default.", + "pattern": "|[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*", + "default": "168h" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "template": "files/database.url" + }, + { + "id": "settings", + "trusted": false, + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "takes-config": ["judge-model", "drafting", "reserved-for"], + "merge": "json", + "template": "files/settings.json" + }, + { + "id": "prepare", + "type": "process", + "name": "mesh-knowledge-prepare", + "artifact": "tools", + "run": [ + "./mesh-knowledge", + "prepare" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + }, + { + "id": "keep", + "type": "process", + "name": "mesh-knowledge-keep", + "artifact": "tools", + "run": [ + "./mesh-knowledge", + "keep" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "watch", + "type": "process", + "name": "mesh-knowledge-watch", + "artifact": "tools", + "run": [ + "./mesh-knowledge", + "watch" + ], + "user": "${machine:account}", + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_KNOWLEDGE_STATUS": "${dir:state}/health.json" + }, + "restart-on": [ + "database-url" + ], + "health": { + "kind": "tool", + "tool": "knowledge_health", + "needs": "postgres-database", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "180s" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-knowledge", + "binary": "mesh-knowledge", + "loads": [ + "mesh-knowledge" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_KNOWLEDGE_SETTINGS": "${dir:state}/settings.json", + "MESH_KNOWLEDGE_STATUS": "${dir:state}/health.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-notifier/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail-notifier/module.json.captured new file mode 100644 index 0000000..9afba46 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-notifier/module.json.captured @@ -0,0 +1,68 @@ +{ + "module": "mesh-mail-notifier", + "version": "1", + "runs-as": "mesh-mail-notifier", + "claims": [ + { + "name": "mail-notifier", + "scope": "node", + "serves": ["status"] + } + ], + "consumes": [ + "mesh-mail.mail.*.message.added" + ], + "invokes": [ + "seat:node-notifier.send", + "seat:mail-store.accounts" + ], + "emits": [ + "shown" + ], + "state": [ + { + "name": "shown", + "ttl-seconds": 2592000 + } + ], + "tools": [ + "mail-notifier.status", + "mail_notifier_health" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-mail-notifier", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-mail-notifier", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-mail-notifier" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-mail-notifier", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-mail-notifier", + "binary": "mesh-mail-notifier", + "loads": [ + "mesh-mail-notifier" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured new file mode 100644 index 0000000..fd92839 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured @@ -0,0 +1,125 @@ +{ + "module": "mesh-mail-web", + "version": "1", + "slug": "mail-web", + "runs-as": "mesh-mail-web", + "claims": [ + { + "name": "mail-webclient", + "scope": "mesh", + "serves": ["open", "status"] + } + ], + "invokes": [ + "seat:mail-store.accounts", + "seat:mail-store.search", + "seat:mail-store.read", + "seat:mail-store.send", + "seat:mail-store.send-status" + ], + "reads": ["mesh-mail.accounts"], + "requires": ["route", "oidc-client"], + "contributes": { + "route": { + "label": "mail", + "endpoint": "web" + }, + "oidc-client": { + "label": "mail", + "endpoint": "web", + "callback": "/signed-in" + } + }, + "binds": { + "route": "${dir:state}/route.json", + "oidc-client": "${dir:state}/oidc-bound.json" + }, + "secrets": { + "oidc-client": "${dir:state}/oidc-bound.secret" + }, + "listens": [ + { + "name": "web", + "port": 8099, + "protocol": "tcp", + "from": "mesh", + "why": "the mail web client's pages, over http; only the route proxy reaches it, and the operator's own browser through the internal name (novox/hq ADR 0440 §2) — its public name, if any, is a route grant" + } + ], + "config": { + "operators": { + "type": "list", + "description": "The Keycloak identities (the ID token's preferred_username) let to sign in; nobody by default (novox/hq ADR 0440 §3).", + "trusted": true, + "default": [] + } + }, + "tools": [ + "mail-webclient.open", + "mail-webclient.status", + "mail_web_health" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-mail-web", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-mail-web", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-mail-web" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-mail-web", + "place": ".", + "unknown": "program" + }, + { + "id": "operators", + "trusted": true, + "type": "file", + "path": "${dir:state}/operators.json", + "mode": "0600", + "owner": "mesh-mail-web", + "merge": "json", + "template": "files/operators.json", + "takes-config": ["operators"] + }, + { + "id": "oidc-config", + "type": "file", + "path": "${dir:state}/oidc.json", + "mode": "0600", + "owner": "mesh-mail-web", + "template": "files/oidc.json" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-mail-web", + "binary": "mesh-mail-web", + "loads": ["mesh-mail-web"], + "env": { + "MESH_MAIL_WEB_OPERATORS": "${dir:state}/operators.json", + "MESH_MAIL_WEB_OIDC": "${dir:state}/oidc.json", + "MESH_MAIL_WEB_LISTEN": "${port:8099}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured new file mode 100644 index 0000000..1eba0cf --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured @@ -0,0 +1,312 @@ +{ + "module": "mesh-mail", + "version": "1", + "slug": "mail", + "seats": [ + { + "name": "mail-store", + "scope": "mesh", + "serves": ["accounts", "search", "read", "send", "send-status"] + }, + { + "name": "mail-webclient", + "scope": "mesh", + "serves": ["open", "status"] + }, + { + "name": "mail-notifier", + "scope": "node", + "needs": ["node-notifier"], + "serves": ["status"] + } + ], + "claims": [ + { + "name": "mail-store", + "scope": "mesh", + "serves": ["accounts", "search", "read", "send", "send-status"] + } + ], + "requires": [ + "postgres-database", + "s3-bucket" + ], + "contributes": { + "postgres-database": { + "name": "mail" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "s3-bucket": "${dir:state}/store-binding.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "s3-bucket": "${dir:state}/store.secret" + }, + "own-secrets": { + "mail-credential-*": { + "path": "${dir:state}/mail-credential-*.secret", + "issued-by": "outside", + "prompt": "the password of the mail account {member}", + "owner": "mesh-mail-send" + }, + "broker-send": { + "path": "${dir:state}/broker-send.secret", + "owner": "mesh-mail-send" + } + }, + "config": { + "accounts": { + "type": "object", + "description": "The mail accounts, each by a lower-case name that names it on the bus and its secret: its kind (imap), address, incoming and outgoing servers (host, port, security tls or starttls), and optionally the folders watched and the user it signs in as; no password, which is the own secret mail-credential- (novox/hq ADR 0359 §5).", + "trusted": true, + "default": {} + }, + "grants": { + "type": "object", + "description": "Per account, who may read its mail and who may send from it: a module's name (on every machine), ., or person.. Nobody by default; it restricts the stream MAIL and the tools mail_search, mail_read and mail_send alike (novox/hq ADR 0359 §4).", + "trusted": true, + "default": {} + } + }, + "tools": [ + "mail_accounts", + "mail_search", + "mail_read", + "mail_health", + "mail_send", + "mail_send_status", + "mail-store.accounts", + "mail-store.search", + "mail-store.read", + "mail-store.send", + "mail-store.send-status" + ], + "uses": [ + "operator-channel" + ], + "state": [ + "sends", + "accounts", + "mails" + ], + "emits": [ + "mail.send.sent", + "mail.send.failed" + ], + "streams": [ + { + "name": "mail", + "events": [ + "mail.*.message.added", + "mail.*.message.changed", + "mail.*.message.removed" + ], + "max-age-days": 7, + "grant": { + "config": "grants", + "events": "mail.{key}.**" + } + }, + { + "name": "mail-sends", + "events": [ + "mail.send.approved" + ], + "max-age-days": 1, + "max-mib": 64, + "retention": "work-queue", + "max-deliveries": 5, + "duplicates": 24 + } + ], + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-mail-send" + }, + { + "id": "state", + "type": "directory", + "mode": "0711", + "place": ".", + "unknown": "program" + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "template": "files/database-url" + }, + { + "id": "store", + "type": "file", + "path": "${dir:state}/store.json", + "mode": "0600", + "template": "files/store.json" + }, + { + "id": "accounts", + "trusted": true, + "type": "file", + "path": "${dir:state}/accounts.json", + "mode": "0644", + "merge": "json", + "template": "files/accounts.json", + "takes-config": [ + "accounts" + ] + }, + { + "id": "grants", + "trusted": true, + "type": "file", + "path": "${dir:state}/grants.json", + "mode": "0600", + "merge": "json", + "template": "files/grants.json", + "takes-config": [ + "grants" + ] + }, + { + "id": "prepare", + "type": "process", + "name": "mesh-mail-prepare", + "artifact": "tools", + "run": [ + "./mesh-mail", + "prepare" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + }, + { + "id": "keep", + "type": "process", + "name": "mesh-mail-keep", + "artifact": "tools", + "run": [ + "./mesh-mail", + "keep" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_MAIL_ACCOUNTS": "${dir:state}/accounts.json", + "MESH_MAIL_SECRETS": "${dir:state}", + "MESH_MAIL_STORE": "${dir:state}/store.json" + }, + "restart-on": [ + "database-url", + "store", + "accounts" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "watch", + "type": "process", + "name": "mesh-mail-watch", + "artifact": "tools", + "run": [ + "./mesh-mail", + "watch" + ], + "user": "${machine:account}", + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_MAIL_ACCOUNTS": "${dir:state}/accounts.json", + "MESH_MAIL_SECRETS": "${dir:state}", + "MESH_MAIL_STATUS": "${dir:state}/health.json" + }, + "restart-on": [ + "database-url", + "accounts" + ], + "health": { + "kind": "tool", + "tool": "mail_health", + "needs": "postgres-database", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "180s" + } + }, + { + "id": "send", + "type": "process", + "name": "mesh-mail-send", + "artifact": "tools", + "run": [ + "./mesh-mail", + "send" + ], + "user": "mesh-mail-send", + "env": { + "MESH_MAIL_ACCOUNTS": "${dir:state}/accounts.json", + "MESH_MAIL_SECRETS": "${dir:state}" + }, + "restart-on": [ + "accounts" + ], + "bus": { + "emits": [ + "mail.send.sent", + "mail.send.failed" + ], + "queues": [ + "mail-sends" + ], + "state": [ + "sends" + ], + "asks": true, + "invokes": [ + "seat:operator-channel.tell" + ] + }, + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-mail", + "binary": "mesh-mail", + "loads": [ + "mesh-mail" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_MAIL_ACCOUNTS": "${dir:state}/accounts.json", + "MESH_MAIL_GRANTS": "${dir:state}/grants.json", + "MESH_MAIL_SECRETS": "${dir:state}", + "MESH_MAIL_STORE": "${dir:state}/store.json", + "MESH_MAIL_STATUS": "${dir:state}/health.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-provisioner-client/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-provisioner-client/module.json.captured new file mode 100644 index 0000000..095128a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-provisioner-client/module.json.captured @@ -0,0 +1,60 @@ +{ + "module": "mesh-provisioner-client", + "version": "1", + "claims": [ + { + "name": "node-provisioner", + "scope": "node" + } + ], + "own-secrets": { + "provisioner-broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-provisioner-client", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-provisioner-client", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-provisioner-client" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-provisioner-client", + "place": ".", + "unknown": "program" + }, + { + "id": "client", + "type": "process", + "name": "mesh-provisioner-client", + "artifact": "client", + "run": [ + "./mesh-provisioner-client" + ], + "env": { + "MESH_PROVISIONER_CREDENTIAL": "${dir:state}/broker", + "MESH_PROVISIONER_STATE": "${dir:state}" + }, + "user": "mesh-provisioner-client", + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "client", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-provisioner-client", + "binary": "mesh-provisioner-client" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-provisioner/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-provisioner/module.json.captured new file mode 100644 index 0000000..650425b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-provisioner/module.json.captured @@ -0,0 +1,56 @@ +{ + "module": "mesh-provisioner", + "version": "1", + "runs-as": "mesh-provisioner", + "claims": [ + { + "name": "mesh-provisioner", + "scope": "mesh", + "serves": [ + "provisioning" + ] + } + ], + "state": [ + "registry", + "needs", + "applied", + "collect" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-provisioner", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-provisioner", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-provisioner" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-provisioner", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-provisioner", + "binary": "mesh-provisioner", + "loads": [ + "mesh-provisioner" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-reads/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-reads/module.json.captured new file mode 100644 index 0000000..1806275 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-reads/module.json.captured @@ -0,0 +1,55 @@ +{ + "module": "mesh-reads", + "version": "1", + "runs-as": "mesh-reads", + "claims": [ + { + "name": "node-reads", + "scope": "node", + "serves": [ + "stat", + "readlink", + "list", + "read", + "search", + "account" + ] + } + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-reads", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-reads", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-reads" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-reads", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-reads", + "binary": "mesh-reads", + "loads": [ + "mesh-reads" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-vault/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-vault/module.json.captured new file mode 100644 index 0000000..44944c9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-vault/module.json.captured @@ -0,0 +1,173 @@ +{ + "module": "mesh-vault", + "version": "1", + "runs-as": "mesh-vault", + "provides": [ + { + "name": "secret", + "scope": "mesh", + "identity": { + "in": "a vault entry" + } + } + ], + "emits": [ + "provisioned", + "rotated", + "secret-changed" + ], + "reads": [ + "mesh-controller.assignments" + ], + "receives": { + "secret": "${dir:grants}/mesh.json" + }, + "grants": { + "secret": "${dir:grants}" + }, + "keeps": "/var/lib/mesh-vault/root", + "data": { + "own": [ + { + "id": "state", + "path": "${dir:state}", + "class": "valuable", + "why": "the vault's own keys" + }, + { + "id": "ledger", + "path": "${dir:ledger}", + "class": "valuable", + "why": "every secret the vault keeps" + }, + { + "id": "root", + "path": "${dir:root}", + "class": "valuable", + "why": "the vault's root material" + }, + { + "id": "store", + "path": "${dir:store}", + "class": "valuable", + "why": "every secret the vault holds, sealed to its own key and to the operator key" + } + ], + "consumers": { + "secret": { + "class": "valuable", + "in": "ledger", + "why": "a secret given to a consumer is kept nowhere else in the clear" + } + } + }, + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-vault", + "runtime-health": { + "kind": "tool", + "tool": "vault_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-vault", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-vault" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-vault", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "owner": "mesh-vault", + "unknown": "program" + }, + { + "id": "ledger", + "type": "directory", + "mode": "0700", + "owner": "mesh-vault", + "unknown": "program" + }, + { + "id": "root", + "type": "directory", + "mode": "0700", + "owner": "mesh-vault", + "unknown": "program" + }, + { + "id": "store", + "type": "directory", + "mode": "0700", + "owner": "mesh-vault", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-vault", + "binary": "mesh-vault", + "loads": [ + "mesh-vault" + ], + "env": { + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_VAULT_LEDGER": "${dir:ledger}", + "MESH_VAULT_ROOT": "${dir:root}", + "MESH_VAULT_STORE": "${dir:store}", + "MESH_VAULT_STATE": "${dir:state}" + } + } + ] + }, + "tools": [ + "secret_export", + "secret_holders", + "secret_verify", + "secret_handed", + "secret_asks", + "secret_vault_export", + "provisioner_retirement", + "provisioner_retire_approve", + "provisioner_retire_reject", + "provisioner_delete", + "vault_status", + "vault_health" + ], + "claims": [ + { + "name": "mesh-vault", + "scope": "mesh", + "serves": [ + "get", + "accept", + "secret-ask", + "issued", + "rotate", + "move-in", + "node-key", + "move-asks" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-watcher/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-watcher/module.json.captured new file mode 100644 index 0000000..e9543a5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-watcher/module.json.captured @@ -0,0 +1,71 @@ +{ + "module": "mesh-watcher", + "version": "1", + "slug": "watch", + "consumes": [ + "mesh-controller.doctor-heartbeat" + ], + "invokes": [ + "mesh-watcher.watcher_ping", + "seat:mesh-controller.seats" + ], + "own-secrets": { + "telegram-token": "${dir:state}/telegram-token" + }, + "tools": [ + "watcher_status", + "watcher_last_heard", + "watcher_test", + "watcher_ping" + ], + "config": { + "telegram-chat-id": { + "type": "string", + "description": "The Telegram chat the watcher reports to: the chat's id, a number, or @name of a channel; empty reports to none.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "takes-config": ["telegram-chat-id"], + "merge": "json", + "template": "files/settings.json" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-watcher", + "binary": "mesh-watcher", + "loads": [ + "mesh-watcher" + ], + "env": { + "MESH_WATCHER_SETTINGS": "${dir:state}/settings.json", + "MESH_WATCHER_TELEGRAM_TOKEN_FILE": "${dir:state}/telegram-token" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured b/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured new file mode 100644 index 0000000..8501bd4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured @@ -0,0 +1,188 @@ +{ + "module": "messenger", + "version": "1", + "slug": "msgr", + "runs-as": "messenger", + "seats": [ + { + "name": "operator-channel", + "scope": "mesh", + "serves": [ + "open", + "history", + "notify", + "tell", + "asks" + ], + "accepts": [ + "ask", + "cancel", + "condition" + ], + "emits": [ + "decided" + ], + "by-caller": [ + "ask", + "cancel", + "condition", + "decided" + ], + "records": [ + "asks" + ] + }, + { + "name": "channel", + "scope": "mesh", + "kinded": true, + "accepts": [ + "show", + "edit", + "send" + ] + }, + { + "name": "intake", + "scope": "mesh", + "kinded": true, + "emits": [ + "choice", + "link", + "failed", + "standing" + ], + "proofs": [ + "code" + ] + } + ], + "claims": [ + { + "name": "operator-channel", + "scope": "mesh", + "serves": [ + "open", + "history", + "notify", + "tell", + "asks" + ] + } + ], + "uses": [ + "channel" + ], + "consumes": [ + "mesh-controller.condition-raised", + "mesh-controller.condition-changed", + "mesh-controller.condition-cleared", + "mesh-controller.plan-moved", + "intake.choice", + "intake.link", + "intake.failed", + "intake.standing" + ], + "emits": [ + "refused" + ], + "invokes": [ + "seat:mesh-controller.conditions", + "seat:mesh-controller.root-free", + "seat:issue-tracker.tracking" + ], + "state": [ + "open", + "sent", + { + "name": "asks", + "ttl-seconds": 2592000 + }, + "identities", + "walks" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "messenger", + "tools": [ + "messenger_status", + "messenger_recent", + "messenger_test", + "messenger_preview", + "messenger_check", + "messenger_identities", + "messenger_unlink" + ], + "config": { + "time-zone": { + "type": "string", + "description": "The time zone every time a message says is said in, an IANA name such as Europe/Brussels; empty is this machine's own zone.", + "trusted": true, + "default": "" + }, + "desktop-machines": { + "type": "list", + "description": "The machines whose desktop showed the operator's messages, by node name. Nothing in this module reads it since the desk channel took the desktop over; it is taken so the file stays as a layer that still sets it makes it, until that layer drops it.", + "trusted": true, + "default": [] + }, + "only-what-needs-me": { + "type": "list", + "description": "The kinds of channel set to say only what needs the operator: a walk that delivered with nothing held and nothing open is not told there (ADR 0308 §4); empty is none.", + "trusted": true, + "default": [] + } + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "messenger", + "shell": "/usr/bin/nologin", + "home": "/var/lib/messenger" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "owner": "messenger", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "takes-config": ["time-zone", "desktop-machines", "only-what-needs-me"], + "merge": "json", + "template": "files/settings.json", + "owner": "messenger" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/messenger", + "binary": "messenger", + "loads": [ + "messenger" + ], + "env": { + "MESH_MESSENGER_SETTINGS": "${dir:state}/settings.json", + "MESH_MESSENGER_STATE": "${dir:state}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/minio/module.json.captured b/go/testdata/beside/mesh-catalog/modules/minio/module.json.captured new file mode 100644 index 0000000..85b8253 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/minio/module.json.captured @@ -0,0 +1,188 @@ +{ + "module": "minio", + "version": "1", + "upgrade": { + "policy": "record", + "why": "holds the photos themselves (irreplaceable, kept by photos) for its consumers: a person takes each build, after a backup (hq ADR 0236)" + }, + "provides": [ + { + "name": "s3-bucket", + "scope": "mesh", + "identity": { + "max": 20, + "in": "an S3 access key" + } + } + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "api": { + "label": "files-api", + "endpoint": "s3" + }, + "console": { + "label": "files", + "endpoint": "console" + } + } + }, + "capabilities": [ + "container-runtime" + ], + "emits": [ + "bucket.created", + "bucket.removed" + ], + "listens": [ + { + "name": "s3", + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the S3 endpoint" + }, + { + "name": "console", + "port": 9001, + "protocol": "tcp", + "from": "mesh", + "why": "the admin console" + } + ], + "serves": { + "s3-bucket": { + "scheme": "http", + "region": "eu-west", + "port": 9000, + "bucket": "${consumer:as:dns}" + } + }, + "receives": { + "s3-bucket": "${dir:grants}/mesh.json" + }, + "grants": { + "s3-bucket": "${dir:grants}" + }, + "own-secrets": { + "root": "${dir:state}/root.secret" + }, + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "every consumer's bucket and its objects" + } + ], + "consumers": { + "s3-bucket": { + "class": "valuable", + "in": "data", + "why": "a consumer's objects are the only copy of what it stored; a consumer that keeps something irreplaceable here says so (kept-by)" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "root-env", + "type": "file", + "path": "${dir:state}/root.env", + "mode": "0600", + "template": "files/root.env" + }, + { + "id": "data", + "type": "directory", + "path": "/var/lib/minio-store", + "mode": "0700", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "minio-net" + }, + { + "id": "server", + "type": "container", + "name": "minio", + "image": "docker.io/pgsty/minio@sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372", + "network": "minio-net", + "args": [ + "server", + "/data", + "--console-address", + ":9001" + ], + "env-file": [ + "${dir:state}/root.env" + ], + "ports": [ + "9000", + "9001" + ], + "volumes": [ + "/var/lib/minio-store:/data", + "${dir:state}/root.secret:/run/secrets/root:ro" + ], + "env": { + "MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root", + "MINIO_REGION": "eu-west" + } + }, + { + "id": "client", + "type": "package", + "package": "minio-client" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_MINIO_ENDPOINT": "http://127.0.0.1:${port:9000}", + "MESH_MINIO_ROOT_USER": "meshroot", + "MESH_MINIO_ROOT_PASSWORD_FILE": "${dir:state}/root.secret", + "MESH_MINIO_REGION": "eu-west", + "MESH_MINIO_MC_BIN": "mcli", + "MESH_MINIO_MC_CONFIG": "${dir:state}/mc", + "MESH_RECEIVES": "${dir:grants}/mesh.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/model-usage/module.json.captured b/go/testdata/beside/mesh-catalog/modules/model-usage/module.json.captured new file mode 100644 index 0000000..50743a3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/model-usage/module.json.captured @@ -0,0 +1,107 @@ +{ + "module": "model-usage", + "version": "1", + "slug": "usage", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "model_usage" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "consumes": [ + "*.usage.*" + ], + "data": { + "own": [ + { + "id": "state", + "path": "${dir:state}", + "class": "cache", + "why": "the mesh's own files for it; its records are in its database" + }, + { + "id": "spool", + "path": "${dir:spool}", + "class": "valuable", + "why": "usage events the store could not take yet; after the bus gives one up, the only copy" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "template": "files/database.url" + }, + { + "id": "spool", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "prepare", + "type": "process", + "name": "model-usage-prepare", + "artifact": "code", + "run": [ + "node", + "prepare/index.js" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "prepare/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "USAGE_SPOOL": "${dir:spool}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mongodb/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mongodb/module.json.captured new file mode 100644 index 0000000..cbb1268 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mongodb/module.json.captured @@ -0,0 +1,170 @@ +{ + "module": "mongodb", + "version": "1", + "upgrade": { + "policy": "record", + "why": "a provider whose restart drops every consumer on its machine, and which holds the photos' albums (irreplaceable, kept by photos): a person takes each build, after a backup (hq ADR 0236)" + }, + "provides": [ + { + "name": "mongodb-database", + "scope": "mesh", + "identity": { + "max": 63, + "in": "a MongoDB database name" + } + } + ], + "capabilities": [ + "container-runtime" + ], + "emits": [ + "database.provisioned", + "database.deprovisioned" + ], + "consumes": [ + "mongodb.database.provisioned", + "mongodb.database.deprovisioned" + ], + "listens": [ + { + "name": "database", + "port": 27017, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a database" + } + ], + "serves": { + "mongodb-database": { + "port": 27017 + } + }, + "receives": { + "mongodb-database": "${dir:grants}/mesh.json" + }, + "grants": { + "mongodb-database": "${dir:grants}" + }, + "own-secrets": { + "root": "${dir:state}/root.secret" + }, + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "backup": { + "dump": "docker exec mongodb-server sh -c 'printf \"password: %s\\n\" \"$(cat /run/secrets/root)\" > /tmp/.backup.yaml && mongodump --quiet --config /tmp/.backup.yaml --username root --authenticationDatabase admin --archive; s=$?; rm -f /tmp/.backup.yaml; exit $s' > ${dir:dumps}/all.archive.partial && mv ${dir:dumps}/all.archive.partial ${dir:dumps}/all.archive", + "into": "dumps" + }, + "why": "every consumer's database; copied by the dump, not as live files" + }, + { + "id": "dumps", + "path": "${dir:dumps}", + "class": "rebuildable", + "why": "last night's dump, made again every night" + } + ], + "consumers": { + "mongodb-database": { + "class": "valuable", + "in": "data", + "why": "a consumer's documents are the only copy of what it wrote; a consumer that keeps something irreplaceable here says so (kept-by)" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "dumps", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "mongodb" + }, + { + "id": "server-root", + "type": "file", + "path": "${dir:state}/server-root.secret", + "mode": "0400", + "owner": "999:999", + "template": "files/server-root.secret" + }, + { + "id": "server", + "type": "container", + "name": "mongodb-server", + "image": "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3", + "health": { + "kind": "tcp", + "endpoint": "database" + }, + "network": "mongodb", + "env": { + "MONGO_INITDB_ROOT_USERNAME": "root", + "MONGO_INITDB_ROOT_PASSWORD_FILE": "/run/secrets/root" + }, + "ports": [ + "27017" + ], + "volumes": [ + "${dir:data}:/data/db", + "${dir:state}/server-root.secret:/run/secrets/root:ro" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_PROVISION_MONGODB": "mongodb://root@127.0.0.1:${port:27017}/admin?authSource=admin", + "MESH_PROVISION_PASSWORD_FILE": "${dir:state}/root.secret", + "MESH_RECEIVES": "${dir:grants}/mesh.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mosquitto/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mosquitto/module.json.captured new file mode 100644 index 0000000..21058e0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mosquitto/module.json.captured @@ -0,0 +1,194 @@ +{ + "module": "mosquitto", + "version": "1", + "slug": "mosq", + "provides": [ + { + "name": "mqtt-topic", + "scope": "mesh", + "identity": { + "in": "a Mosquitto client and topic prefix" + } + } + ], + "capabilities": [ + "container-runtime" + ], + "emits": [ + "topic.provisioned", + "topic.deprovisioned" + ], + "consumes": [ + "mosquitto.topic.provisioned", + "mosquitto.topic.deprovisioned" + ], + "serves": { + "mqtt-topic": { + "scheme": "mqtt", + "port": 1883 + } + }, + "receives": { + "mqtt-topic": "${dir:grants}/mesh.json" + }, + "grants": { + "mqtt-topic": "${dir:grants}" + }, + "own-secrets": { + "admin": { + "path": "${dir:mesh-state}/admin", + "taken": "at-start" + } + }, + "listens": [ + { + "name": "mqtt", + "port": 1883, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a topic namespace" + }, + { + "name": "mqtt-websockets", + "port": 8081, + "protocol": "tcp", + "from": "mesh", + "why": "the same broker over MQTT-on-WebSockets, for browser clients" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "rebuildable", + "why": "retained messages and sessions; devices publish them again" + } + ], + "consumers": { + "mqtt-topic": { + "class": "rebuildable", + "in": "data", + "why": "retained messages are published again by the devices" + } + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1883:1883", + "unknown": "program" + }, + { + "id": "server-conf", + "type": "file", + "path": "${dir:state}/mosquitto.conf", + "mode": "0600", + "owner": "1883:1883", + "template": "files/mosquitto.conf" + }, + { + "id": "net", + "type": "network", + "name": "mosquitto" + }, + { + "id": "bootstrap-env", + "type": "file", + "path": "${dir:state}/bootstrap.env", + "mode": "0600", + "template": "files/bootstrap.env" + }, + { + "id": "bootstrap", + "type": "process", + "name": "mosquitto-bootstrap", + "artifact": "code", + "run": [ + "node", + "bootstrap/index.js" + ], + "run-once": true, + "env-file": [ + "${dir:state}/bootstrap.env" + ], + "restart-on": [ + "bootstrap-env", + "needs-admin" + ] + }, + { + "id": "server", + "type": "container", + "name": "mosquitto", + "image": "eclipse-mosquitto@sha256:38c0da4f2ef84284d47b3b3eeea1cb3bdeabe81ee10caf0cd5c5ff61ee3ea408", + "health": { + "kind": "tcp", + "endpoint": "mqtt" + }, + "network": "mosquitto", + "ports": [ + "1883", + "8081" + ], + "volumes": [ + "${dir:data}:/mosquitto/data", + "${dir:state}/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js", + "bootstrap/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_PROVISION_MQTT": "127.0.0.1:${port:1883}", + "MESH_PROVISION_ADMIN_USER": "mesh-admin", + "MESH_PROVISION_PASSWORD_FILE": "${dir:mesh-state}/admin", + "MESH_MQTT_CTRL_CONTAINER": "mosquitto" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mounts/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mounts/module.json.captured new file mode 100644 index 0000000..488ebb9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mounts/module.json.captured @@ -0,0 +1,160 @@ +{ + "module": "mounts", + "version": "1", + "requires": [ + "nfs-share" + ], + "reads": [ + "nfs-server.exports" + ], + "invokes": [ + "seat:mesh-controller.data" + ], + "capabilities": [ + "package-manager", + "service-manager" + ], + "claims": [ + { + "name": "node-mounts", + "scope": "node", + "serves": [ + "list", + "test", + "mount", + "unmount", + "adopt" + ] + } + ], + "tools": [ + "mounts_list", + "mounts_exports", + "mounts_health" + ], + "state": [ + { + "name": "shares", + "per-machine": true + } + ], + "config": { + "shares": { + "type": "string", + "description": "The shares of another machine this machine mounts: space-separated name=mountpoint, each optionally :ro, or none (novox/hq ADR 0263 rule 5).", + "trusted": true, + "default": "none" + }, + "sources": { + "type": "string", + "description": "The occasional sources this machine mounts: space-separated name=smb://[@]/@ or name=disk:@, each optionally :ro, or none (novox/hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password- (novox/hq ADR 0283).", + "trusted": true, + "default": "none" + } + }, + "own-secrets": { + "smb-password-*": { + "path": "${dir:state}/smb-password-*.secret", + "issued-by": "outside" + } + }, + "resources": [ + { + "id": "nfs-utils", + "type": "package", + "package": "nfs-utils" + }, + { + "id": "cifs-utils", + "type": "package", + "package": "cifs-utils" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config-dir", + "type": "directory", + "path": "/etc/mesh-mounts", + "mode": "0755", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "/etc/mesh-mounts/mounts.conf", + "mode": "0644", + "template": "files/mounts.conf" + }, + { + "id": "bus-dir-parent", + "type": "directory", + "path": "/var/lib/mesh-mounts", + "mode": "0755", + "unknown": "program" + }, + { + "id": "bus-dir", + "type": "directory", + "path": "/var/lib/mesh-mounts/from-bus", + "mode": "0755", + "owner": "${machine:account}", + "unknown": "program" + }, + { + "id": "apply", + "type": "process", + "name": "mesh-mounts-apply", + "artifact": "tools-go", + "run": [ + "./mounts", + "apply" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "watch", + "type": "process", + "name": "mesh-mounts-watch", + "artifact": "tools-go", + "run": [ + "./mounts", + "watch" + ], + "health": { + "kind": "tool", + "tool": "mounts_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "90s" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mounts", + "binary": "mounts", + "loads": [ + "mounts" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mssql/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mssql/module.json.captured new file mode 100644 index 0000000..ed4e448 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mssql/module.json.captured @@ -0,0 +1,174 @@ +{ + "module": "mssql", + "version": "1", + "upgrade": { + "policy": "record", + "why": "a provider whose restart drops every consumer on its machine, and whose new version may upgrade its databases in place: a person takes each build, after a backup (hq ADR 0236)" + }, + "provides": [ + { + "name": "mssql-database", + "scope": "mesh", + "identity": { + "max": 128, + "in": "a SQL Server login and database name" + } + } + ], + "capabilities": [ + "container-runtime" + ], + "emits": [ + "database.provisioned", + "database.deprovisioned" + ], + "consumes": [ + "mssql.database.provisioned", + "mssql.database.deprovisioned" + ], + "listens": [ + { + "name": "database", + "port": 1433, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a database, and the people who were given its address; the machine port is the assignment's to pin" + } + ], + "serves": { + "mssql-database": {} + }, + "receives": { + "mssql-database": "${dir:grants}/mesh.json" + }, + "grants": { + "mssql-database": "${dir:grants}" + }, + "own-secrets": { + "sa": "${dir:state}/sa.secret", + "reader": "${dir:state}/reader.secret" + }, + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "backup": { + "dump": "{ cat ${dir:state}/sa.secret; echo; cat ${dir:state}/backup.sql; } | docker exec -i mssql sh -c 'read -r p; SQLCMDPASSWORD=\"$p\" exec /opt/mssql-tools18/bin/sqlcmd -C -b -S localhost -U sa -i /dev/stdin'", + "into": "dumps" + }, + "why": "every consumer's database; copied by the dump, not as live files" + }, + { + "id": "dumps", + "path": "${dir:dumps}", + "class": "rebuildable", + "why": "last night's dump, made again every night" + } + ], + "consumers": { + "mssql-database": { + "class": "valuable", + "in": "data", + "why": "a consumer's rows are the only copy of what it wrote" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "sa-env", + "type": "file", + "path": "${dir:state}/sa.env", + "mode": "0600", + "template": "files/sa.env" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "10001:0", + "unknown": "program" + }, + { + "id": "dumps", + "type": "directory", + "path": "${dir:data}/backup", + "mode": "0700", + "owner": "10001:0", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "mssql" + }, + { + "id": "server", + "type": "container", + "name": "mssql", + "image": "mcr.microsoft.com/mssql/server@sha256:4402d880dd4c34bfa7d8705e56a86cd6c88da80a1f6bbbe741f999e76264a090", + "network": "mssql", + "env-file": [ + "${dir:state}/sa.env" + ], + "ports": [ + "1433" + ], + "volumes": [ + "${dir:data}:/var/opt/mssql" + ], + "secrets-in-environment": "the image documents only MSSQL_SA_PASSWORD, no _FILE and no configuration field; not convertible without a wrapper entrypoint" + }, + { + "id": "backup-sql", + "type": "file", + "path": "${dir:state}/backup.sql", + "mode": "0600", + "template": "files/backup.sql" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_PROVISION_MSSQL": "mssql://sa@127.0.0.1:${port:1433}/master", + "MESH_PROVISION_PASSWORD_FILE": "${dir:state}/sa.secret", + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_MSSQL_READER_PASSWORD_FILE": "${dir:state}/reader.secret" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/n8n/module.json.captured b/go/testdata/beside/mesh-catalog/modules/n8n/module.json.captured new file mode 100644 index 0000000..975247f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/n8n/module.json.captured @@ -0,0 +1,173 @@ +{ + "module": "n8n", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database", + "route" + ], + "contributes": { + "postgres-database": { + "name": "n8n" + }, + "route": { + "label": "n8n", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "accesses": [ + { + "id": "media", + "mode": "read-write" + } + ], + "listens": [ + { + "name": "web", + "port": 5678, + "protocol": "tcp", + "from": "mesh", + "why": "the n8n editor, its REST API and the webhook endpoints workflows are triggered through; a public name is the route's" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "the instance's encryption key, settings and binary data; workflows are in the database" + }, + { + "id": "cache", + "path": "${dir:cache}", + "class": "cache", + "why": "scratch space" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "mode": "0700", + "owner": "999:999", + "unknown": "program" + }, + { + "id": "database-secret", + "type": "file", + "path": "${dir:state}/n8n-database.secret", + "mode": "0400", + "owner": "1000:1000", + "template": "files/n8n-database.secret" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "net", + "type": "network", + "name": "n8n" + }, + { + "id": "server", + "type": "container", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/healthz/readiness", + "status": 200, + "needs": "postgres-database" + }, + "name": "n8n", + "artifact": "server", + "network": "n8n", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "5678" + ], + "volumes": [ + "${dir:data}:/home/node/.n8n", + "${dir:state}/n8n-database.secret:/run/secrets/database:ro", + "${access:media}:/media-library" + ] + }, + { + "id": "cache-server", + "type": "container", + "name": "n8n-redis", + "image": "redis@sha256:8a1efc5f479551822b47424ccae982026b633f28818eab0387348120a61e10e2", + "network": "n8n", + "args": [ + "redis-server", + "--appendonly", + "yes" + ], + "volumes": [ + "${dir:cache}:/data" + ] + }, + { + "id": "browser", + "type": "container", + "name": "n8n-selenium", + "image": "selenium/standalone-chrome@sha256:9ae1c78e9b2ca9fe4b22e57873b5ee34aeb8e814e3122293eef4c3abe4c5f448", + "network": "n8n", + "env": { + "SE_ENABLE_TRACING": "false", + "SE_NODE_MAX_SESSIONS": "5", + "SE_NODE_OVERRIDE_MAX_SESSIONS": "true" + } + } + ], + "build": { + "on": [ + { + "arg": "N8N_BASE", + "image": "n8nio/n8n@sha256:4846eb2f4b874ab04cde7fc1e249d2ddaec66e9aea64439beb2972cfea88e3c0" + } + ], + "artifacts": [ + { + "name": "server", + "kind": "image", + "from": "Dockerfile" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nats/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nats/module.json.captured new file mode 100644 index 0000000..3ad66bc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nats/module.json.captured @@ -0,0 +1,171 @@ +{ + "module": "nats", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the bus: replaced only as a planned step a person starts (`bus upgrade`), its streams snapshotted first and checked after (hq ADR 0236); the controller holds this whatever is said here" + }, + "provides": [ + { + "name": "mesh-bus", + "scope": "mesh" + } + ], + "claims": [ + { + "name": "mesh-broker", + "scope": "mesh" + } + ], + "bus-users": "/var/lib/nats-module/conf/accounts.conf", + "own-secrets": { + "broker": "${dir:mesh-state}/broker" + }, + "capabilities": [ + "container-runtime" + ], + "emits": [], + "consumes": [], + "listens": [ + { + "name": "bus", + "port": 4222, + "protocol": "tcp", + "from": "mesh", + "why": "the mesh bus — every link the mesh has, over TLS, reached across the overlay" + } + ], + "tools": [ + "nats_server", + "nats_connections", + "nats_closed_connections", + "nats_subscriptions", + "nats_streams", + "nats_backlog", + "nats_buckets", + "nats_users", + "nats_user_can" + ], + "guards": [ + 8222 + ], + "data": { + "own": [ + { + "id": "jetstream", + "path": "${dir:jetstream-data}", + "class": "valuable", + "active": "1d", + "backup": { + "dump": "docker exec -i mesh-broker-nats mesh-nats-snapshot snapshot < ${dir:mesh-state}/broker > ${dir:snapshots}/bus.tar.partial && mv ${dir:snapshots}/bus.tar.partial ${dir:snapshots}/bus.tar || { rm -f ${dir:snapshots}/bus.tar.partial; exit 1; }", + "into": "snapshots" + }, + "why": "the bus's streams and key-value buckets: the hand-act log, conditions, every module's state; written all the time, so copied by the server's own snapshot of each stream (novox/hq ADR 0235), never as live files" + }, + { + "id": "snapshots", + "path": "${dir:snapshots}", + "class": "rebuildable", + "why": "last night's snapshot of every stream with its manifest, made again every night" + } + ] + }, + "resources": [ + { + "id": "jetstream-data", + "type": "directory", + "path": "/var/lib/mesh-broker-nats", + "mode": "0700", + "unknown": "program" + }, + { + "id": "conf-dir", + "type": "directory", + "path": "/var/lib/nats-module/conf", + "mode": "0700", + "unknown": "program" + }, + { + "id": "snapshots", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "server-conf", + "type": "file", + "path": "/var/lib/nats-module/conf/nats.conf", + "template": "files/nats.conf", + "mode": "0644" + }, + { + "id": "server", + "type": "container", + "name": "mesh-broker-nats", + "ports": [ + "4222:4222", + "127.0.0.1:8222:8222" + ], + "volumes": [ + "/var/lib/mesh-broker-nats:/data", + "/var/lib/nats-module/conf:/etc/nats:ro", + "${access:tls}:/tls:ro" + ], + "artifact": "server" + } + ], + "accesses": [ + { + "id": "tls", + "path": "/var/lib/mesh-broker-tls", + "mode": "read" + } + ], + "build": { + "on": [ + { + "arg": "NATS_BASE", + "image": "nats@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00" + }, + { + "arg": "GO_BASE", + "image": "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c" + } + ], + "artifacts": [ + { + "name": "server", + "kind": "image", + "from": "Dockerfile" + }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/nats-tools", + "binary": "nats-tools", + "loads": [ + "nats-tools" + ], + "env": { + "MESH_NATS_MONITOR": "http://127.0.0.1:8222", + "MESH_NATS_CONTAINER": "mesh-broker-nats", + "MESH_NATS_USERS_FILE": "/etc/nats/accounts.conf" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/netcheck/module.json.captured b/go/testdata/beside/mesh-catalog/modules/netcheck/module.json.captured new file mode 100644 index 0000000..ec01003 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/netcheck/module.json.captured @@ -0,0 +1,57 @@ +{ + "module": "netcheck", + "version": "1", + "emits": [ + "tunnel.endpoint-changed", + "tunnel.handshake-stale", + "tunnel.handshake-fresh" + ], + "tools": [ + "netcheck_tunnel", + "netcheck_tcp", + "netcheck_dns", + "netcheck_http", + "netcheck_listening" + ], + "replaces": { + "netcheck_listening": [ + "ss -lunt", + "ss -ltn", + "netstat -lnt" + ], + "netcheck_dns": [ + "dig", + "nslookup" + ], + "netcheck_tunnel": [ + "wg show", + "wg show mesh0 dump" + ] + }, + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/netcheck", + "binary": "netcheck", + "loads": [ + "netcheck" + ] + }, + { + "name": "tools-typescript", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/networkmanager/module.json.captured b/go/testdata/beside/mesh-catalog/modules/networkmanager/module.json.captured new file mode 100644 index 0000000..68ace34 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/networkmanager/module.json.captured @@ -0,0 +1,71 @@ +{ + "module": "networkmanager", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the machine's network: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)" + }, + "slug": "nm", + "requires": [ + "wildcard-resolution" + ], + "capabilities": [ + "package-manager", + "service-manager", + "uplink-networkmanager" + ], + "claims": [ + { + "name": "node-uplink", + "scope": "node" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/uplink-tools", + "binary": "uplink-tools", + "loads": [ + "uplink-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "facts": { + "resolvers": { + "path": "/etc/resolv.conf", + "template": "# Managed by the mesh, and written by the module holding this machine's uplink:\n# the program that manages the machine's network would otherwise rewrite this\n# file on every change of network, so its holder is the one that writes it\n# (novox/hq ADR 0117, ADR 0223). Replaced on every push; edit nothing here.\n#\n# Every resolver of the mesh, by address, and nothing else (novox/hq ADR 0223) \u2014\n# this machine's own first when it holds one, then the others by name. Each\n# answers the mesh's names from the same roster and forwards every other name, so\n# whichever answers first gives the one answer. There is no public resolver here:\n# a C library that asks every listed server at once and takes the first reply \u2014\n# musl, so every Alpine container \u2014 took a public resolver's \"no such name\" for\n# a mesh name and failed. A machine that reaches none of these has no names until\n# it does. Containers copy these lines from their machine.\n{{range index .Holders \"mesh-dns-resolver\"}}nameserver {{.Address}}\n{{end}}options timeout:1 attempts:2 edns0\n" + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "networkmanager" + }, + { + "id": "config", + "type": "file", + "path": "/etc/NetworkManager/conf.d/50-mesh.conf", + "mode": "0644", + "template": "files/50-mesh.conf" + }, + { + "id": "service", + "type": "service", + "unit": "NetworkManager.service", + "reload-on": [ + "config" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/nextcloud-client/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nextcloud-client/module.json.captured new file mode 100644 index 0000000..af71acb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nextcloud-client/module.json.captured @@ -0,0 +1,38 @@ +{ + "module": "nextcloud-client", + "version": "1", + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "tools": [ + "nextcloud_status", + "nextcloud_log", + "nextcloud_restart", + "nextcloud_check" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "nextcloud-client" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/nextcloud-client-tools", + "binary": "nextcloud-client-tools", + "loads": [ + "nextcloud-client-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nextcloud/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nextcloud/module.json.captured new file mode 100644 index 0000000..2854a16 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nextcloud/module.json.captured @@ -0,0 +1,168 @@ +{ + "module": "nextcloud", + "version": "1", + "slug": "ncloud", + "requires": [ + "postgres-database", + "s3-bucket", + "route" + ], + "contributes": { + "postgres-database": { + "name": "nextcloud" + }, + "route": { + "label": "drive", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "s3-bucket": "${dir:state}/store.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "s3-bucket": "${dir:state}/store.secret" + }, + "emits": [ + "user.created", + "share.created" + ], + "own-secrets": { + "admin": "${dir:state}/admin.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "web", + "port": 80, + "protocol": "tcp", + "from": "mesh", + "why": "files and sync, over http; a public name is a route grant later" + } + ], + "data": { + "own": [ + { + "id": "html", + "path": "${dir:html}", + "class": "valuable", + "why": "the instance's configuration, its secret and installed apps; the files are in the object store" + } + ] + }, + "config": { + "container": { + "type": "string", + "description": "The name of the container the module's tools run Nextcloud's occ in; empty is nextcloud.", + "trusted": true, + "default": "" + }, + "url": { + "type": "string", + "description": "The address the module's tools reach Nextcloud's sharing API at, as a URL; empty is this machine's Nextcloud on its port.", + "trusted": true, + "default": "" + }, + "user": { + "type": "string", + "description": "The Nextcloud account the module's tools act as; empty is admin.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "html", + "type": "directory", + "mode": "0750", + "owner": "33:33", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/status.php", + "status": 200, + "body": "\"installed\":true", + "needs": "postgres-database" + }, + "name": "nextcloud", + "image": "nextcloud@sha256:fb966733647ea03f0446b0c22eac9733c8eb616d37b960caca9d4c3010e14a08", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "80" + ], + "volumes": [ + "${dir:html}:/var/www/html" + ], + "secrets-in-environment": "the image honours POSTGRES_PASSWORD_FILE and NEXTCLOUD_ADMIN_PASSWORD_FILE (entrypoint file_env); OBJECTSTORE_S3_SECRET has none and needs a generated config fragment; convertible, awaiting a bed" + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "takes-config": ["container", "url", "user"], + "merge": "json" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js" + ], + "loads": [ + "index.js", + "tools/index.js" + ], + "env": { + "MESH_NEXTCLOUD_URL": "http://127.0.0.1:${port:80}", + "MESH_NEXTCLOUD_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_NEXTCLOUD_ADMIN_USER": "mesh-admin", + "MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nfs-server/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nfs-server/module.json.captured new file mode 100644 index 0000000..f9994d1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nfs-server/module.json.captured @@ -0,0 +1,191 @@ +{ + "module": "nfs-server", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the folders other machines mount: a build that breaks the exports leaves every client's mount hanging or refused, and the gate on this one machine does not see the clients (hq ADR 0236, ADR 0263)" + }, + "capabilities": [ + "package-manager", + "service-manager" + ], + "provides": [ + { + "name": "nfs-share", + "scope": "mesh", + "identity": false + } + ], + "data": { + "consumers": { + "nfs-share": { + "class": "none", + "why": "the shared folders are the operator's data (hq ADR 0051): what a client writes lands in them, and they are protected where the operator declares them, never by this module, which keeps nothing of a consumer's" + } + } + }, + "claims": [ + { + "name": "node-nfs-server", + "scope": "node", + "serves": [ + "exports", + "clients", + "test", + "reload", + "adopt" + ] + } + ], + "state": [ + { + "name": "exports", + "ttl-seconds": 120, + "per-machine": true + } + ], + "reads": [ + "mounts.shares" + ], + "invokes": [ + "seat:mesh-controller.seats", + "seat:mesh-controller.data" + ], + "config": { + "shares": { + "type": "string", + "description": "The folders this machine exports, as space-separated name=path; the operator's, with no default (novox/hq ADR 0263).", + "trusted": true, + "required": true + }, + "grants": { + "type": "string", + "description": "Which nodes may mount which share, and how: share=node:rw|ro[,node:rw|ro], entries separated by spaces, or none to export to no node whatever a node wants (novox/hq ADR 0263).", + "trusted": true, + "default": "none" + } + }, + "facts": { + "machines": { + "path": "/etc/nfs-server/machines", + "template": "# Written by the mesh (module nfs-server, novox/hq ADR 0263): every machine of the mesh and its private\n# address, from which the module takes a node's address. Replaced on every push.\n{{range .Machines}}{{.Name}} {{.Address}}\n{{end}}" + } + }, + "tools": [ + "nfs_health" + ], + "listens": [ + { + "name": "nfs", + "port": 2049, + "protocol": "tcp", + "from": "mesh", + "fixed": true, + "why": "the shares, to the mesh's machines only (hq ADR 0263): NFS version 4 alone, which needs no other port, and never the home network, where a device that is not a node could claim any user id" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "nfs-utils" + }, + { + "id": "nfs-conf", + "type": "file", + "path": "/etc/nfs.conf.d/50-mesh.conf", + "mode": "0644", + "template": "files/50-mesh.conf" + }, + { + "id": "run-dir", + "type": "directory", + "path": "/run/nfs-server", + "mode": "0755", + "unknown": "program" + }, + { + "id": "server", + "type": "service", + "unit": "nfs-server.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "nfs-conf" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "wants-dir-parent", + "type": "directory", + "path": "/var/lib/nfs-server", + "mode": "0755", + "unknown": "program" + }, + { + "id": "wants-dir", + "type": "directory", + "path": "/var/lib/nfs-server/from-bus", + "mode": "0755", + "owner": "${machine:account}", + "unknown": "program" + }, + { + "id": "exports", + "type": "process", + "name": "nfs-server-exports", + "artifact": "tools", + "run": [ + "./nfs-server", + "exports" + ], + "restart-on": [ + "config" + ], + "health": { + "kind": "tool", + "tool": "nfs_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "90s" + } + }, + { + "id": "config-dir", + "type": "directory", + "path": "/etc/nfs-server", + "mode": "0755", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "/etc/nfs-server/shares.conf", + "mode": "0644", + "template": "files/shares.conf" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/nfs-server", + "binary": "nfs-server", + "loads": [ + "nfs-server" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nftables/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nftables/module.json.captured new file mode 100644 index 0000000..b7b0c83 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nftables/module.json.captured @@ -0,0 +1,91 @@ +{ + "module": "nftables", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the machine's packet filter: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)" + }, + "capabilities": [ + "firewall" + ], + "claims": [ + { + "name": "node-packet-filter", + "scope": "node", + "serves": [ + "rules", + "reload", + "remove" + ] + } + ], + "filtering": { + "into": "/etc/nftables.conf" + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "nftables" + }, + { + "id": "legacy-tools", + "type": "package", + "package": "iptables" + }, + { + "id": "unit", + "type": "file", + "path": "/etc/systemd/system/mesh-filter.service", + "template": "files/mesh-filter.service", + "mode": "0644" + }, + { + "id": "stock-unit-stop", + "type": "file", + "path": "/etc/systemd/system/nftables.service.d/mesh.conf", + "template": "files/mesh.conf", + "mode": "0644" + }, + { + "id": "load", + "type": "service", + "unit": "mesh-filter.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "unit", + "stock-unit-stop" + ], + "reload-on": [ + "filtering" + ] + }, + { + "id": "front-end", + "type": "package", + "package": "ufw", + "absent": true + } + ], + "tools": [ + "firewall_rules" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nm-applet/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nm-applet/module.json.captured new file mode 100644 index 0000000..1c6764e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nm-applet/module.json.captured @@ -0,0 +1,37 @@ +{ + "module": "nm-applet", + "version": "1", + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "tools": [ + "nm_applet_status", + "nm_applet_restart", + "nm_applet_check" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "network-manager-applet" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/nm-applet-tools", + "binary": "nm-applet-tools", + "loads": [ + "nm-applet-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/node-env/module.json.captured b/go/testdata/beside/mesh-catalog/modules/node-env/module.json.captured new file mode 100644 index 0000000..85fd7e5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/node-env/module.json.captured @@ -0,0 +1,55 @@ +{ + "module": "node-env", + "version": "1", + "moved": true, + "claims": [ + { + "name": "node-environment", + "scope": "node" + } + ], + "resources": [ + { + "id": "mesh-config-dir", + "type": "directory", + "path": "${machine:account-home}/.config/mesh", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out", + "knows": ["forge.credentials"] + }, + { + "id": "environment-d", + "type": "directory", + "path": "${machine:account-home}/.config/environment.d", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "posix", + "type": "file", + "path": "${machine:account-home}/.config/mesh/environment.sh", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/environment.sh" + }, + { + "id": "systemd", + "type": "file", + "path": "${machine:account-home}/.config/environment.d/50-mesh.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/50-mesh.conf" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nodered/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nodered/module.json.captured new file mode 100644 index 0000000..f8068dd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodered/module.json.captured @@ -0,0 +1,297 @@ +{ + "module": "nodered", + "version": "1", + "emits": [ + "flows.deployed" + ], + "own-secrets": { + "admin": { + "path": "${dir:mesh-state}/admin", + "taken": "at-start" + }, + "api-token": { + "path": "${dir:mesh-state}/api-token", + "taken": "at-start" + } + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "web", + "port": 1880, + "protocol": "tcp", + "from": "mesh", + "why": "the flow editor and the endpoints flows expose" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "flows and their credentials" + } + ] + }, + "config": { + "flow-file": { + "type": "string", + "description": "Node-RED's flowFile setting: the file in the user directory the flows are kept in.", + "trusted": true, + "default": "flows.json" + }, + "flow-file-pretty": { + "type": "boolean", + "description": "Node-RED's flowFilePretty setting: whether the flows file is written indented.", + "trusted": true, + "default": true + }, + "diagnostics": { + "type": "object", + "description": "Node-RED's diagnostics setting: whether its diagnostics report is on, and shown in the editor.", + "trusted": true, + "default": { + "enabled": true, + "ui": true + } + }, + "runtime-state": { + "type": "object", + "description": "Node-RED's runtimeState setting: whether flows may be started and stopped from the editor.", + "trusted": true, + "default": { + "enabled": false, + "ui": false + } + }, + "logging": { + "type": "object", + "description": "Node-RED's logging setting: the console log's level, and whether metrics and audit events are logged.", + "trusted": true, + "default": { + "console": { + "level": "info", + "metrics": false, + "audit": false + } + } + }, + "export-global-context-keys": { + "type": "boolean", + "description": "Node-RED's exportGlobalContextKeys setting: whether a function node may list the global context's keys.", + "trusted": true, + "default": false + }, + "external-modules": { + "type": "object", + "description": "Node-RED's externalModules setting: which modules the editor may install, and from where.", + "trusted": true, + "default": {} + }, + "editor-theme": { + "type": "object", + "description": "Node-RED's editorTheme setting: the editor's look and features, projects among them.", + "trusted": true, + "default": { + "projects": { + "enabled": false + } + } + }, + "function-external-modules": { + "type": "boolean", + "description": "Node-RED's functionExternalModules setting: whether a function node may load modules of its own.", + "trusted": true, + "default": true + }, + "debug-max-length": { + "type": "number", + "description": "Node-RED's debugMaxLength setting: the most characters a debug message shows, a count.", + "trusted": true, + "default": 1000 + }, + "mqtt-reconnect-time": { + "type": "number", + "description": "Node-RED's mqttReconnectTime setting: how long an MQTT node waits before reconnecting, in milliseconds.", + "trusted": true, + "default": 15000 + }, + "serial-reconnect-time": { + "type": "number", + "description": "Node-RED's serialReconnectTime setting: how long a serial node waits before reconnecting, in milliseconds.", + "trusted": true, + "default": 15000 + }, + "mqtt": { + "type": "object", + "description": "The broker nodes the module's MQTT step keeps pointed at the mesh's broker, as {\"brokers\": [their ids]}; empty, none.", + "trusted": true, + "default": {} + }, + "time-zone": { + "type": "string", + "description": "The time zone the flows schedule and format in, an IANA name such as Europe/Brussels; unset, the container's UTC.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "written", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "settings-code", + "type": "file", + "path": "${dir:state}/settings.js", + "mode": "0600", + "owner": "1000:1000", + "template": "files/settings.js" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "owner": "1000:1000", + "merge": "json", + "takes-config": [{"key": "flow-file", "as": "flowFile"}, {"key": "flow-file-pretty", "as": "flowFilePretty"}, "diagnostics", {"key": "runtime-state", "as": "runtimeState"}, "logging", {"key": "export-global-context-keys", "as": "exportGlobalContextKeys"}, {"key": "external-modules", "as": "externalModules"}, {"key": "editor-theme", "as": "editorTheme"}, {"key": "function-external-modules", "as": "functionExternalModules"}, {"key": "debug-max-length", "as": "debugMaxLength"}, {"key": "mqtt-reconnect-time", "as": "mqttReconnectTime"}, {"key": "serial-reconnect-time", "as": "serialReconnectTime"}, "mqtt", {"key": "time-zone", "as": "timeZone"}], + "template": "files/settings.json" + }, + { + "id": "server", + "type": "container", + "name": "nodered", + "image": "nodered/node-red@sha256:a649dd711d55490151a2c39a8e48ad0c44325488fbc0e66315f2d2e19e5e1ace", + "health": { + "kind": "runtime" + }, + "env": { + "TZ": "Etc/UTC" + }, + "ports": [ + "1880" + ], + "args": [ + "--settings", + "/data/settings.js" + ], + "volumes": [ + "${dir:data}:/data", + "${dir:state}/settings.js:/data/settings.js:ro", + "${dir:state}/settings.json:/data/settings.json:ro" + ], + "restart-on": [ + "settings-code", + "settings" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json" + }, + { + "id": "mqtt-env", + "type": "file", + "path": "${dir:state}/mqtt.env", + "mode": "0600", + "template": "files/mqtt.env" + }, + { + "id": "mqtt", + "type": "process", + "name": "nodered-mqtt", + "artifact": "code", + "run": [ + "node", + "mqtt/index.js" + ], + "run-once": true, + "env-file": [ + "${dir:state}/mqtt.env" + ], + "restart-on": [ + "mqtt-env", + "bound-mqtt-topic", + "secret-mqtt-topic", + "settings" + ] + } + ], + "requires": [ + "mqtt-topic", + "route" + ], + "contributes": { + "mqtt-topic": { + "topics": [ + "#" + ] + }, + "route": { + "label": "nodered", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json", + "mqtt-topic": "${dir:state}/mqtt-topic.json" + }, + "secrets": { + "mqtt-topic": "${dir:state}/mqtt-topic.secret" + }, + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js", + "mqtt/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_NODERED_URL": "http://127.0.0.1:${port:1880}", + "MESH_NODERED_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/ollama/module.json.captured b/go/testdata/beside/mesh-catalog/modules/ollama/module.json.captured new file mode 100644 index 0000000..2d5fb48 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ollama/module.json.captured @@ -0,0 +1,62 @@ +{ + "module": "ollama", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "provides": [ + { + "name": "model-access", + "scope": "node" + } + ], + "listens": [ + { + "name": "api", + "port": 11434, + "protocol": "tcp", + "from": "machine", + "why": "consumers on this machine reaching the local model server's OpenAI-compatible API" + } + ], + "serves": { + "model-access": { + "port": 11434, + "model": "llama3.2" + } + }, + "data": { + "own": [ + { + "id": "models", + "path": "${dir:state}", + "class": "rebuildable", + "backup": "none", + "measure": "shallow", + "why": "models, downloaded again, and too large to copy every night" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/services/ollama", + "mode": "0700", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "ollama", + "image": "ollama/ollama@sha256:32931b46719f673c05fdbaa81ccb26da18ea4a1c57590a754874ab28ba269eb2", + "network": "host", + "env": { + "OLLAMA_HOST": "0.0.0.0:11434" + }, + "volumes": [ + "/services/ollama:/root/.ollama" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/only-office/module.json.captured b/go/testdata/beside/mesh-catalog/modules/only-office/module.json.captured new file mode 100644 index 0000000..4af651f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/only-office/module.json.captured @@ -0,0 +1,173 @@ +{ + "module": "only-office", + "version": "1", + "slug": "office", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "office", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "own-secrets": { + "jwt": "${dir:state}/jwt.secret" + }, + "listens": [ + { + "name": "web", + "port": 9070, + "protocol": "tcp", + "from": "mesh", + "why": "the document server over http; its public name is a route grant, and route-proxy reaches it on this published port" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "rebuildable", + "why": "documents being edited; the documents themselves are the file store's" + }, + { + "id": "lib", + "path": "${dir:lib}", + "class": "rebuildable", + "why": "the document server's working files" + }, + { + "id": "db", + "path": "${dir:db}", + "class": "rebuildable", + "why": "the document server's own database of editing sessions" + }, + { + "id": "fonts", + "path": "${dir:fonts}", + "class": "rebuildable", + "why": "fonts, installed again" + }, + { + "id": "logs", + "path": "${dir:logs}", + "class": "cache", + "why": "logs" + }, + { + "id": "rabbitmq", + "path": "${dir:rabbitmq}", + "class": "cache", + "why": "its queue's working set" + }, + { + "id": "redis", + "path": "${dir:redis}", + "class": "cache", + "why": "its cache" + } + ] + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "logs", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "lib", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "db", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "rabbitmq", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "redis", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "fonts", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "only-office" + }, + { + "id": "server", + "type": "container", + "name": "only-office", + "image": "onlyoffice/documentserver@sha256:3ab6ebc7c605e5a32b7ae3ff19daed4925090245acc8100ce2230bd766c88212", + "network": "only-office", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "9070:80" + ], + "volumes": [ + "${dir:logs}:/var/log/onlyoffice", + "${dir:data}:/var/www/onlyoffice/Data", + "${dir:lib}:/var/lib/onlyoffice", + "${dir:db}:/var/lib/postgresql", + "${dir:rabbitmq}:/var/lib/rabbitmq", + "${dir:redis}:/var/lib/redis", + "${dir:fonts}:/usr/share/fonts/truetype/custom" + ], + "secrets-in-environment": "run-document-server.sh regenerates JWT_SECRET from the environment on every start and overwrites local.json; not convertible" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/openai-consumer/module.json.captured b/go/testdata/beside/mesh-catalog/modules/openai-consumer/module.json.captured new file mode 100644 index 0000000..2e98414 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/openai-consumer/module.json.captured @@ -0,0 +1,61 @@ +{ + "module": "openai-consumer", + "version": "1", + "slug": "openai", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "model-access" + ], + "binds": { + "model-access": "${dir:state}/model.json" + }, + "secrets": { + "model-access": "${dir:state}/api-key" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "apply", + "type": "process", + "name": "openai-consumer-apply", + "artifact": "code", + "run": [ + "node", + "apply/index.js" + ], + "schedule": "*/5 * * * *", + "env": { + "MESH_MODEL_ACCESS_SECRET_FILE": "${dir:state}/api-key", + "MESH_MODEL_ACCESS_BIND_FILE": "${dir:state}/model.json", + "MESH_OPENAI_ENV_FILE": "${dir:state}/config/openai.env", + "MESH_OPENAI_CREDENTIALS_FILE": "${dir:state}/config/auth.json" + } + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "apply/index.js" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/openrazer/module.json.captured b/go/testdata/beside/mesh-catalog/modules/openrazer/module.json.captured new file mode 100644 index 0000000..faa34f8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/openrazer/module.json.captured @@ -0,0 +1,52 @@ +{ + "module": "openrazer", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "openrazer_status", + "openrazer_restart", + "openrazer_check" + ], + "resources": [ + { + "id": "driver", + "type": "package", + "package": "openrazer-driver-dkms" + }, + { + "id": "daemon", + "type": "package", + "package": "openrazer-daemon" + }, + { + "id": "library", + "type": "package", + "package": "python-openrazer" + }, + { + "id": "account", + "type": "user", + "name": "${machine:account}", + "groups": [ + "openrazer" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/openrazer-tools", + "binary": "openrazer-tools", + "loads": [ + "openrazer-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured b/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured new file mode 100644 index 0000000..746c8c5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured @@ -0,0 +1,96 @@ +{ + "module": "pacman", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "claims": [ + { + "name": "node-package-manager", + "scope": "node" + } + ], + "tools": [ + "pacman_search", + "pacman_info", + "pacman_installed", + "pacman_owns", + "pacman_files", + "pacman_updates", + "pacman_upgrade", + "pacman_orphans", + "pacman_remove_orphans", + "pacman_cache", + "pacman_history", + "pacman_mirrors", + "pacman_foreign", + "pacman_news", + "pacman_config" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "pacman" + }, + { + "id": "config", + "type": "file", + "path": "/etc/pacman.conf", + "mode": "0644", + "template": "files/pacman.conf" + }, + { + "id": "contrib", + "type": "package", + "package": "pacman-contrib" + }, + { + "id": "reflector", + "type": "package", + "package": "reflector" + }, + { + "id": "mirrors", + "type": "file", + "path": "/etc/xdg/reflector/reflector.conf", + "mode": "0644", + "template": "files/reflector.conf" + }, + { + "id": "mirror-refresh", + "type": "service", + "unit": "reflector.timer", + "state": "running", + "boot": "enabled" + }, + { + "id": "cache-cleaning", + "type": "service", + "unit": "paccache.timer", + "state": "running", + "boot": "enabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/pacman-tools", + "binary": "pacman-tools", + "loads": [ + "pacman-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/photos-eef/module.json.captured b/go/testdata/beside/mesh-catalog/modules/photos-eef/module.json.captured new file mode 100644 index 0000000..101bdc2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/photos-eef/module.json.captured @@ -0,0 +1,56 @@ +{ + "module": "photos-eef", + "version": "1", + "slug": "eef", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "eef", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "listens": [ + { + "name": "web", + "port": 4012, + "protocol": "tcp", + "from": "mesh", + "why": "the eef photos client site over http; its public name is a route grant, and route-proxy reaches it on this published port" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "photos-eef" + }, + { + "id": "client", + "type": "container", + "name": "photos-eef", + "image": "registry-api.novox.be/novox/photos-client@sha256:f87d63ee7bfb44c9f9748b99be6ba6dc0daf955a1d463699e9e7e3009693c0ab", + "network": "photos-eef", + "ports": [ + "4012:80" + ], + "names-on-purpose": { + "registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)" + } + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/photos-filip/module.json.captured b/go/testdata/beside/mesh-catalog/modules/photos-filip/module.json.captured new file mode 100644 index 0000000..9c1c83f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/photos-filip/module.json.captured @@ -0,0 +1,56 @@ +{ + "module": "photos-filip", + "version": "1", + "slug": "filip", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "filip", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "listens": [ + { + "name": "web", + "port": 4013, + "protocol": "tcp", + "from": "mesh", + "why": "the filip photos client site over http; its public name is a route grant, and route-proxy reaches it on this published port" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "photos-filip" + }, + { + "id": "client", + "type": "container", + "name": "photos-filip", + "image": "registry-api.novox.be/novox/photos-client@sha256:f87d63ee7bfb44c9f9748b99be6ba6dc0daf955a1d463699e9e7e3009693c0ab", + "network": "photos-filip", + "ports": [ + "4013:80" + ], + "names-on-purpose": { + "registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)" + } + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/picom/module.json.captured b/go/testdata/beside/mesh-catalog/modules/picom/module.json.captured new file mode 100644 index 0000000..cfdc397 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/picom/module.json.captured @@ -0,0 +1,90 @@ +{ + "module": "picom", + "version": "1", + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-compositor", + "scope": "node" + } + ], + "config": { + "backend": { + "type": "string", + "description": "How picom draws: glx with the graphics card, which has served every machine so far, or xrender on the CPU where glx leaves a window unpainted or stale on the machine's driver (novox/hq research 037, issue 345); xr_glx_hybrid and egl are picom's other backends.", + "enum": [ + "glx", + "xrender", + "xr_glx_hybrid", + "egl" + ], + "trusted": true, + "default": "glx" + }, + "use-damage": { + "type": "boolean", + "description": "Whether picom repaints only what changed (true, picom's own default) or the whole screen each frame (false), which costs work and cures a window left stale or unpainted on some drivers (novox/hq issue 345).", + "trusted": true, + "default": true + } + }, + "tools": [ + "picom_restart", + "picom_rules", + "picom_window_opacity", + "picom_toggle" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "picom" + }, + { + "id": "window-properties", + "type": "package", + "package": "xorg-xprop" + }, + { + "id": "configuration-dir", + "type": "directory", + "path": "${machine:account-home}/.config/picom", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "configuration", + "type": "file", + "path": "${machine:account-home}/.config/picom/picom.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/picom.conf" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/picom-tools", + "binary": "picom-tools", + "loads": [ + "picom-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/polychromatic/module.json.captured b/go/testdata/beside/mesh-catalog/modules/polychromatic/module.json.captured new file mode 100644 index 0000000..4d6ba4b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/polychromatic/module.json.captured @@ -0,0 +1,40 @@ +{ + "module": "polychromatic", + "version": "1", + "moved": true, + "requires": [ + "x11-display" + ], + "tools": [ + "polychromatic_status", + "polychromatic_restart", + "polychromatic_check" + ], + "items": [ + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/display-session" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/polychromatic-tools", + "binary": "polychromatic-tools", + "loads": [ + "polychromatic-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/postgres/module.json.captured b/go/testdata/beside/mesh-catalog/modules/postgres/module.json.captured new file mode 100644 index 0000000..919df42 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/postgres/module.json.captured @@ -0,0 +1,191 @@ +{ + "module": "postgres", + "version": "1", + "moved": true, + "upgrade": { + "policy": "record", + "why": "a provider whose restart drops every consumer on its machine, and whose new major version changes its data's format: a person takes each build, after a backup (hq ADR 0236)" + }, + "provides": [ + { + "name": "postgres-database", + "scope": "mesh", + "identity": { + "max": 63, + "in": "a PostgreSQL role and database name" + } + } + ], + "claims": [ + { + "name": "mesh-store", + "scope": "mesh", + "serves": [ + "databases", + "query" + ] + } + ], + "capabilities": [ + "container-runtime" + ], + "emits": [ + "database.provisioned", + "database.deprovisioned" + ], + "consumes": [ + "postgres.database.provisioned", + "postgres.database.deprovisioned" + ], + "listens": [ + { + "name": "database", + "port": 5432, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a database" + } + ], + "guards": [ + 5432 + ], + "serves": { + "postgres-database": { + "port": 5432 + } + }, + "receives": { + "postgres-database": "${dir:grants}/mesh.json" + }, + "grants": { + "postgres-database": "${dir:grants}" + }, + "own-secrets": { + "superuser": "${dir:state}/superuser.secret", + "reader": "${dir:state}/reader.secret" + }, + "data": { + "own": [ + { + "id": "store", + "path": "${dir:store-data}", + "class": "valuable", + "backup": { + "dump": "docker exec -u postgres postgres sh -c 'cd /var/lib/postgresql/data/dumps && for db in $(psql -Atc \"select datname from pg_database where oid >= 16384 order by 1\"); do pg_dump -Fc -f \"$db.dump.partial\" \"$db\" && mv \"$db.dump.partial\" \"$db.dump\" || exit 1; done'", + "into": "dumps" + }, + "why": "every consumer's database; copied by the dump below, since a running store's files are not a consistent copy" + }, + { + "id": "dumps", + "path": "${dir:dumps}", + "class": "rebuildable", + "why": "last night's dump of the store, made again every night" + } + ], + "consumers": { + "postgres-database": { + "class": "valuable", + "in": "store", + "why": "a consumer's rows are the only copy of what it wrote" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "consumers", + "type": "file", + "path": "${dir:state}/consumers.json", + "mode": "0600", + "template": "files/consumers.json" + }, + { + "id": "store-data", + "type": "directory", + "path": "/var/lib/mesh-store", + "mode": "0700", + "owner": "999:70", + "unknown": "program" + }, + { + "id": "dumps", + "type": "directory", + "path": "${dir:store-data}/dumps", + "mode": "0700", + "owner": "999:70", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "postgres", + "image": "pgvector/pgvector@sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f", + "health": { + "kind": "tcp", + "endpoint": "database" + }, + "env": { + "POSTGRES_PASSWORD_FILE": "/run/secrets/superuser", + "PGDATA": "/var/lib/postgresql/data/pgdata" + }, + "ports": [ + "5432:5432" + ], + "volumes": [ + "/var/lib/mesh-store:/var/lib/postgresql/data", + "${dir:state}/superuser.secret:/run/secrets/superuser:ro" + ] + }, + { + "id": "client", + "type": "package", + "package": "postgresql-libs" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/postgres-provider", + "binary": "postgres-provider", + "loads": [ + "postgres-provider" + ], + "env": { + "MESH_PROVISION_POSTGRES": "postgres://postgres@127.0.0.1:${port:5432}/postgres?sslmode=disable", + "MESH_PROVISION_PASSWORD_FILE": "${dir:state}/superuser.secret", + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_POSTGRES_READER_PASSWORD_FILE": "${dir:state}/reader.secret", + "MESH_CONSUMERS_FILE": "${dir:state}/consumers.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "state": [ + "bound" + ], + "reads": [ + "mesh-controller.assignments" + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/power/module.json.captured b/go/testdata/beside/mesh-catalog/modules/power/module.json.captured new file mode 100644 index 0000000..a110d41 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/module.json.captured @@ -0,0 +1,408 @@ +{ + "module": "power", + "version": "1", + "moved": true, + "templates": "templates", + "capabilities": [ + "package-manager", + "service-manager" + ], + "claims": [ + { + "name": "node-power", + "scope": "node" + } + ], + "emits": [ + "booted", + "sleeping", + "woke", + "shutting-down", + "on-mains", + "on-battery", + "battery-low" + ], + "state": [ + { + "name": "draw", + "ttl-seconds": 30, + "per-machine": true + } + ], + "tools": [ + "power_state", + "power_hooks", + "power_history", + "power_run", + "power_check" + ], + "config": { + "handle-power-key": { + "type": "string", + "description": "What logind does when the power key is pressed: one of logind's actions, such as suspend, ignore or poweroff (logind.conf(5)); the operator's choice for this machine, with no default.", + "enum": [ + "ignore", + "poweroff", + "reboot", + "halt", + "kexec", + "suspend", + "hibernate", + "hybrid-sleep", + "suspend-then-hibernate", + "lock", + "factory-reset", + "sleep" + ], + "trusted": true, + "required": true + }, + "handle-lid-switch": { + "type": "string", + "description": "What logind does when the lid is closed on battery: one of logind's actions, such as suspend, ignore or poweroff (logind.conf(5)); the operator's choice for this machine, with no default.", + "enum": [ + "ignore", + "poweroff", + "reboot", + "halt", + "kexec", + "suspend", + "hibernate", + "hybrid-sleep", + "suspend-then-hibernate", + "lock", + "factory-reset", + "sleep" + ], + "trusted": true, + "required": true + }, + "handle-lid-switch-external-power": { + "type": "string", + "description": "What logind does when the lid is closed on external power: one of logind's actions, such as suspend, ignore or poweroff (logind.conf(5)); the operator's choice for this machine, with no default.", + "enum": [ + "ignore", + "poweroff", + "reboot", + "halt", + "kexec", + "suspend", + "hibernate", + "hybrid-sleep", + "suspend-then-hibernate", + "lock", + "factory-reset", + "sleep" + ], + "trusted": true, + "required": true + }, + "handle-lid-switch-docked": { + "type": "string", + "description": "What logind does when the lid is closed while docked or with another screen attached: one of logind's actions, such as suspend, ignore or poweroff (logind.conf(5)); the operator's choice for this machine, with no default.", + "enum": [ + "ignore", + "poweroff", + "reboot", + "halt", + "kexec", + "suspend", + "hibernate", + "hybrid-sleep", + "suspend-then-hibernate", + "lock", + "factory-reset", + "sleep" + ], + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "polkit", + "type": "package", + "package": "polkit" + }, + { + "id": "scripts", + "type": "archive", + "path": "/usr/local/lib/mesh-power", + "artifact": "scripts", + "unknown": "program" + }, + { + "id": "config-dir", + "type": "directory", + "path": "/etc/mesh-power", + "mode": "0755", + "unknown": "program" + }, + { + "id": "moments-dir", + "type": "directory", + "path": "/etc/mesh-power/moments", + "mode": "0755", + "unknown": "program" + }, + { + "id": "moment-after-boot", + "type": "file", + "path": "/etc/mesh-power/moments/after-boot", + "mode": "0644", + "template": "templates/mesh-power/moments/after-boot" + }, + { + "id": "moment-before-sleep", + "type": "file", + "path": "/etc/mesh-power/moments/before-sleep", + "mode": "0644", + "template": "templates/mesh-power/moments/before-sleep" + }, + { + "id": "moment-after-wake", + "type": "file", + "path": "/etc/mesh-power/moments/after-wake", + "mode": "0644", + "template": "templates/mesh-power/moments/after-wake" + }, + { + "id": "moment-before-shutdown", + "type": "file", + "path": "/etc/mesh-power/moments/before-shutdown", + "mode": "0644", + "template": "templates/mesh-power/moments/before-shutdown" + }, + { + "id": "moment-on-mains", + "type": "file", + "path": "/etc/mesh-power/moments/on-mains", + "mode": "0644", + "template": "templates/mesh-power/moments/on-mains" + }, + { + "id": "moment-on-battery", + "type": "file", + "path": "/etc/mesh-power/moments/on-battery", + "mode": "0644", + "template": "templates/mesh-power/moments/on-battery" + }, + { + "id": "logind-drop-ins", + "type": "directory", + "path": "/etc/systemd/logind.conf.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "logind-power", + "type": "file", + "path": "/etc/systemd/logind.conf.d/power.conf", + "mode": "0644", + "template": "templates/systemd/logind.conf.d/power.conf" + }, + { + "id": "logind", + "type": "service", + "unit": "systemd-logind.service", + "reload-on": [ + "logind-power" + ] + }, + { + "id": "after-boot-unit", + "type": "file", + "path": "/etc/systemd/system/mesh-power-after-boot.service", + "mode": "0644", + "template": "templates/systemd/system/mesh-power-after-boot.service" + }, + { + "id": "before-shutdown-unit", + "type": "file", + "path": "/etc/systemd/system/mesh-power-before-shutdown.service", + "mode": "0644", + "template": "templates/systemd/system/mesh-power-before-shutdown.service" + }, + { + "id": "before-sleep-unit", + "type": "file", + "path": "/etc/systemd/system/mesh-power-before-sleep.service", + "mode": "0644", + "template": "templates/systemd/system/mesh-power-before-sleep.service" + }, + { + "id": "after-wake-unit", + "type": "file", + "path": "/etc/systemd/system/mesh-power-after-wake.service", + "mode": "0644", + "template": "templates/systemd/system/mesh-power-after-wake.service" + }, + { + "id": "supply-unit", + "type": "file", + "path": "/etc/systemd/system/mesh-power-supply.service", + "mode": "0644", + "template": "templates/systemd/system/mesh-power-supply.service" + }, + { + "id": "sleep-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/sleep.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "sleep-wants", + "type": "file", + "path": "/etc/systemd/system/sleep.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/sleep.target.d/mesh-power.conf" + }, + { + "id": "suspend-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/suspend.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "suspend-wants", + "type": "file", + "path": "/etc/systemd/system/suspend.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/suspend.target.d/mesh-power.conf" + }, + { + "id": "hibernate-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/hibernate.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "hibernate-wants", + "type": "file", + "path": "/etc/systemd/system/hibernate.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/hibernate.target.d/mesh-power.conf" + }, + { + "id": "hybrid-sleep-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/hybrid-sleep.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "hybrid-sleep-wants", + "type": "file", + "path": "/etc/systemd/system/hybrid-sleep.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/hybrid-sleep.target.d/mesh-power.conf" + }, + { + "id": "suspend-then-hibernate-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/suspend-then-hibernate.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "suspend-then-hibernate-wants", + "type": "file", + "path": "/etc/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf" + }, + { + "id": "udev-rule", + "type": "file", + "path": "/etc/udev/rules.d/90-mesh-power.rules", + "mode": "0644", + "template": "templates/udev/rules.d/90-mesh-power.rules" + }, + { + "id": "after-boot", + "type": "service", + "unit": "mesh-power-after-boot.service", + "state": "running", + "boot": "enabled" + }, + { + "id": "before-shutdown", + "type": "service", + "unit": "mesh-power-before-shutdown.service", + "state": "running", + "boot": "enabled" + }, + { + "id": "draw-dir", + "type": "directory", + "path": "/run/mesh-power", + "mode": "0755", + "unknown": "program" + }, + { + "id": "draw", + "type": "process", + "name": "mesh-power-draw", + "artifact": "tools-go", + "run": [ + "./power", + "draw" + ], + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools-go", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/power", + "binary": "power", + "loads": [ + "power" + ] + }, + { + "name": "scripts", + "kind": "archive", + "from": "files" + }, + { + "name": "templates", + "kind": "archive", + "from": "templates" + } + ] + }, + "items": [ + { + "collection": "contribution:node-bar:block", + "name": "battery", + "if-capability": "battery", + "attributes": { + "bar": "bottom", + "place": "status", + "order": "40" + }, + "template": "templates/bar-battery" + }, + { + "collection": "contribution:node-bar:block", + "name": "power-draw", + "if-capability": "power-meter", + "attributes": { + "bar": "bottom", + "place": "status", + "order": "45" + }, + "template": "templates/bar-power-draw" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/powerlevel10k/module.json.captured b/go/testdata/beside/mesh-catalog/modules/powerlevel10k/module.json.captured new file mode 100644 index 0000000..da1bd4d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/powerlevel10k/module.json.captured @@ -0,0 +1,49 @@ +{ + "module": "powerlevel10k", + "version": "1", + "moved": true, + "resources": [ + { + "id": "theme", + "type": "archive", + "path": "${machine:account-home}/.local/share/powerlevel10k", + "owner": "${machine:account}", + "artifact": "theme", + "unknown": "move-out", + "knows": ["powerlevel9k.zsh-theme.zwc", "powerlevel10k.zsh-theme.zwc", "internal/p10k.zsh.zwc", "internal/icons.zsh.zwc", "internal/configure.zsh.zwc", "internal/worker.zsh.zwc", "internal/parser.zsh.zwc", "gitstatus/gitstatus.plugin.zsh.zwc", "gitstatus/install.zwc"] + }, + { + "id": "configuration", + "type": "archive", + "path": "${machine:account-home}/.config/powerlevel10k", + "owner": "${machine:account}", + "artifact": "configuration", + "unknown": "move-out" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "theme", + "kind": "archive", + "from": "theme" + }, + { + "name": "configuration", + "kind": "archive", + "from": "config" + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/records/module.json.captured b/go/testdata/beside/mesh-catalog/modules/records/module.json.captured new file mode 100644 index 0000000..db6f7e5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/records/module.json.captured @@ -0,0 +1,103 @@ +{ + "module": "records", + "version": "1", + "slug": "records", + "requires": [ + "git" + ], + "binds": { + "git": "${dir:mesh-state}/git.json" + }, + "consumes": [ + "gitea.pull.merged" + ], + "tools": [ + "records_search", + "records_read", + "records_list", + "records_decisions", + "records_status", + "records_sync" + ], + "data": { + "own": [ + { + "id": "checkout", + "path": "${dir:checkout}", + "class": "rebuildable", + "why": "a clone of the record, cloned again" + } + ] + }, + "config": { + "repository": { + "type": "string", + "description": "The repository the records module keeps a checkout of and serves the record from, as owner/name on the mesh's forge; the operator's, with no default (novox/hq ADR 0112).", + "pattern": "[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "checkout", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "takes-config": ["repository"], + "merge": "json" + }, + { + "id": "origin", + "type": "file", + "path": "${dir:mesh-state}/origin", + "mode": "0600", + "template": "files/origin" + }, + { + "id": "git", + "type": "package", + "package": "git" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/records", + "binary": "records", + "loads": [ + "records" + ], + "env": { + "MESH_RECORDS_CONFIG_FILE": "${dir:mesh-state}/config.json", + "MESH_RECORDS_ORIGIN_FILE": "${dir:mesh-state}/origin", + "MESH_RECORDS_DIR": "${dir:checkout}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/redis/module.json.captured b/go/testdata/beside/mesh-catalog/modules/redis/module.json.captured new file mode 100644 index 0000000..58e1106 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/redis/module.json.captured @@ -0,0 +1,154 @@ +{ + "module": "redis", + "version": "1", + "provides": [ + { + "name": "redis-cache", + "scope": "mesh", + "identity": { + "in": "a Redis ACL user and key prefix" + } + } + ], + "requires": [ + "secret" + ], + "capabilities": [ + "container-runtime" + ], + "emits": [ + "cache.provisioned", + "cache.deprovisioned" + ], + "consumes": [ + "redis.cache.provisioned", + "redis.cache.deprovisioned" + ], + "serves": { + "redis-cache": { + "port": 6379 + } + }, + "receives": { + "redis-cache": "${dir:grants}/mesh.json" + }, + "grants": { + "redis-cache": "${dir:grants}" + }, + "secrets": { + "secret": "${dir:state}/default.secret" + }, + "listens": [ + { + "name": "cache", + "port": 6379, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a cache" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "cache", + "why": "the cache's own snapshot" + } + ], + "consumers": { + "redis-cache": { + "class": "cache", + "why": "a cache: nothing in this mesh requires it, and a consumer that kept data in it would be using a cache as a store" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "999:1000", + "unknown": "program" + }, + { + "id": "server-conf", + "type": "file", + "path": "${dir:state}/redis.conf", + "mode": "0600", + "template": "files/redis.conf", + "owner": "999:1000" + }, + { + "id": "net", + "type": "network", + "name": "redis" + }, + { + "id": "server", + "type": "container", + "name": "redis", + "image": "redis@sha256:520775a41a63e77e06c73e35d2fd9cc15921a609516818796b4ecbb813078bc7", + "health": { + "kind": "tcp", + "endpoint": "cache" + }, + "network": "redis", + "ports": [ + "6379" + ], + "volumes": [ + "${dir:data}:/data", + "${dir:state}/redis.conf:/etc/redis/redis.conf:ro" + ], + "args": [ + "/etc/redis/redis.conf" + ], + "restart-on": [ + "server-conf" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "index.js", + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_PROVISION_REDIS": "127.0.0.1:${port:6379}", + "MESH_PROVISION_PASSWORD_FILE": "${dir:state}/default.secret" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/restic/module.json.captured b/go/testdata/beside/mesh-catalog/modules/restic/module.json.captured new file mode 100644 index 0000000..18ee811 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/restic/module.json.captured @@ -0,0 +1,196 @@ +{ + "module": "restic", + "version": "1", + "moved": true, + "claims": [ + { + "name": "node-backup", + "scope": "node", + "serves": [ + "backed-up", + "now", + "restore" + ] + } + ], + "tools": [ + "backup_health", + "backup_delete_retired", + "backup_deleted" + ], + "own-secrets": { + "repository": "${dir:state}/repository.secret" + }, + "data": { + "own": [ + { + "id": "repository", + "path": "${dir:repository}", + "class": "rebuildable", + "backup": "none", + "why": "the restore points themselves: a copy of data kept elsewhere on this machine, never the only copy of anything; lost, the history goes and nothing live does" + }, + { + "id": "state", + "path": "${dir:state}", + "class": "cache", + "why": "what the last nights and measurements were; the next night writes it again" + } + ] + }, + "config": { + "keep-daily": { + "type": "number", + "description": "How many nightly restore points are kept, a count from 1 to 9999: two weeks undo a mistake noticed within a fortnight. Read by the night, which thins nothing while it does not read (novox/hq issue 416); lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone.", + "minimum": 1, + "maximum": 9999, + "trusted": true, + "default": 14 + }, + "keep-weekly": { + "type": "number", + "description": "How many weekly restore points are kept behind the dailies, a count from 1 to 9999, refused by the night like keep-daily; lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone.", + "minimum": 1, + "maximum": 9999, + "trusted": true, + "default": 8 + }, + "keep-monthly": { + "type": "number", + "description": "How many monthly restore points are kept behind the weeklies, a count from 1 to 9999, refused by the night like keep-daily; lowering it deletes the restore points beyond the new count at the next night, and that cannot be undone.", + "minimum": 1, + "maximum": 9999, + "trusted": true, + "default": 6 + }, + "keep-hand-taken-days": { + "type": "number", + "description": "How many days a restore point taken by hand (node-backup.now, or the mesh's before a module moves) is kept at least, a count from 1 to 9999; it is kept until a later good night of its module exists, and this many days in any case, so the next thinning never takes the one taken against a risky change (novox/hq issue 453). Lowering it lets go of the older ones taken by hand at the next thinning, and that cannot be undone.", + "minimum": 1, + "maximum": 9999, + "trusted": true, + "default": 7 + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "repository", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "declared", + "type": "file", + "path": "${dir:state}/backups.conf", + "mode": "0600", + "template": "files/backups.conf" + }, + { + "id": "data-declared", + "type": "file", + "path": "${dir:state}/data.conf", + "mode": "0600", + "template": "files/data.conf" + }, + { + "id": "retention", + "type": "file", + "path": "${dir:state}/retention.conf", + "mode": "0644", + "trusted": true, + "template": "files/retention.conf" + }, + { + "id": "tool", + "type": "package", + "package": "restic" + }, + { + "id": "sqlite", + "type": "package", + "package": "sqlite" + }, + { + "id": "night", + "type": "process", + "name": "restic-night", + "artifact": "tools", + "run": [ + "./restic-backups", + "night" + ], + "user": "${machine:account}", + "env": { + "MESH_BACKUP_DECLARED": "${dir:state}/backups.conf", + "MESH_BACKUP_REPOSITORY": "${dir:repository}", + "MESH_BACKUP_PASSWORD_FILE": "${dir:state}/repository.secret", + "MESH_BACKUP_STATE": "${dir:state}", + "MESH_BACKUP_DATA": "${dir:state}/data.conf" + }, + "health": { + "kind": "tool", + "tool": "backup_health", + "interval": "60s", + "timeout": "20s", + "looks": 2, + "grace": "60s" + } + }, + { + "id": "measure", + "type": "process", + "name": "restic-measure", + "artifact": "tools", + "run": [ + "./restic-backups", + "measure" + ], + "user": "${machine:account}", + "env": { + "MESH_BACKUP_DECLARED": "${dir:state}/backups.conf", + "MESH_BACKUP_REPOSITORY": "${dir:repository}", + "MESH_BACKUP_PASSWORD_FILE": "${dir:state}/repository.secret", + "MESH_BACKUP_STATE": "${dir:state}", + "MESH_BACKUP_DATA": "${dir:state}/data.conf" + }, + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/restic-backups", + "binary": "restic-backups", + "loads": [ + "restic-backups" + ], + "env": { + "MESH_BACKUP_DECLARED": "${dir:state}/backups.conf", + "MESH_BACKUP_REPOSITORY": "${dir:repository}", + "MESH_BACKUP_PASSWORD_FILE": "${dir:state}/repository.secret", + "MESH_BACKUP_STATE": "${dir:state}", + "MESH_BACKUP_DATA": "${dir:state}/data.conf" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/module.json.captured b/go/testdata/beside/mesh-catalog/modules/rofi/module.json.captured new file mode 100644 index 0000000..fbd061d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/module.json.captured @@ -0,0 +1,124 @@ +{ + "module": "rofi", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-launcher", + "scope": "node", + "serves": [ + "menu", + "secret" + ] + } + ], + "tools": [ + "rofi_applications", + "rofi_themes", + "rofi_run" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "rofi" + }, + { + "id": "configuration-dir", + "type": "directory", + "path": "${machine:account-home}/.config/rofi", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "themes-dir", + "type": "directory", + "path": "${machine:account-home}/.config/rofi/themes", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "configuration", + "type": "file", + "path": "${machine:account-home}/.config/rofi/config.rasi", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/config.rasi" + }, + { + "id": "theme", + "type": "file", + "path": "${machine:account-home}/.config/rofi/themes/mesh.rasi", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/themes/mesh.rasi" + }, + { + "id": "theme-powermenu", + "type": "file", + "path": "${machine:account-home}/.config/rofi/themes/mesh-powermenu.rasi", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/themes/mesh-powermenu.rasi" + }, + { + "id": "dmenu", + "type": "file", + "path": "${machine:account-home}/.local/bin/dmenu", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/dmenu" + }, + { + "id": "launch", + "type": "file", + "path": "${machine:account-home}/.local/bin/rofi-launch", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/rofi-launch" + }, + { + "id": "powermenu", + "type": "file", + "path": "${machine:account-home}/.local/bin/rofi-powermenu", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/rofi-powermenu" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/rofi-tools", + "binary": "rofi-tools", + "loads": [ + "rofi-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "items": [ + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/i3/50-rofi.conf" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/route-adapter/module.json.captured b/go/testdata/beside/mesh-catalog/modules/route-adapter/module.json.captured new file mode 100644 index 0000000..5234c78 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/route-adapter/module.json.captured @@ -0,0 +1,134 @@ +{ + "module": "route-adapter", + "version": "1", + "moved": true, + "slug": "radapt", + "capabilities": [ + "container-runtime" + ], + "provides": [ + { + "name": "route", + "scope": "mesh", + "identity": false + } + ], + "serves": { + "route": {} + }, + "receives": { + "route": "${dir:routes-dir}/mesh.json" + }, + "accesses": [ + { + "id": "dynamic", + "path": "/services/traefik/dynamic", + "mode": "read-write" + } + ], + "config": { + "dynamic": { + "type": "string", + "description": "The directory the predecessor proxy's file provider reads, on this machine, as an absolute path.", + "pattern": "/.*", + "trusted": true, + "default": "/services/traefik/dynamic" + }, + "entrypoint": { + "type": "string", + "description": "The predecessor proxy's entry point it serves public HTTPS on, by its own name.", + "trusted": true, + "default": "websecure" + }, + "certificate-resolver": { + "type": "string", + "description": "The predecessor proxy's certificate resolver, by its own name.", + "trusted": true, + "default": "le" + }, + "machine": { + "type": "string", + "description": "How the predecessor's proxy reaches this machine: a host name or an address.", + "trusted": true, + "default": "host.docker.internal" + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "routes-dir", + "type": "directory", + "path": "/var/lib/route-adapter/routes", + "mode": "0700", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:state}/config.json", + "takes-config": ["dynamic", "entrypoint", "certificate-resolver", "machine"], + "merge": "json", + "mode": "0644", + "template": "files/config.json" + }, + { + "id": "adapt", + "type": "process", + "name": "route-adapter", + "artifact": "code", + "run": [ + "node", + "index.js" + ], + "run-once": true, + "env": { + "MESH_RECEIVES": "${dir:routes-dir}/mesh.json", + "MESH_ROUTE_ADAPTER_CONFIG": "${dir:state}/config.json" + }, + "restart-on": [ + "received-route", + "config" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js" + ] + }, + { + "name": "provider", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/route-provider", + "binary": "route-provider", + "loads": [ + "route-provider" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "state": [ + "bound" + ], + "reads": [ + "mesh-controller.assignments" + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/screen-lock/module.json.captured b/go/testdata/beside/mesh-catalog/modules/screen-lock/module.json.captured new file mode 100644 index 0000000..1791c79 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/screen-lock/module.json.captured @@ -0,0 +1,78 @@ +{ + "module": "screen-lock", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-lock-screen", + "scope": "node", + "serves": [ + "lock" + ] + } + ], + "tools": [ + "screen_lock_idle", + "screen_lock_inhibit", + "screen_lock_locked" + ], + "items": [ + { + "collection": "shell:xinitrc:normal", + "name": "xinitrc-normal", + "template": "files/xinitrc-normal" + } + ], + "resources": [ + { + "id": "screensaver", + "type": "package", + "package": "xscreensaver", + "absent": true + }, + { + "id": "watcher", + "type": "package", + "package": "xss-lock" + }, + { + "id": "locker", + "type": "package", + "package": "i3lock" + }, + { + "id": "wrapper", + "type": "file", + "path": "${machine:account-home}/.local/bin/screen-lock", + "owner": "${machine:account}", + "mode": "0755", + "template": "files/bin/screen-lock" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/screen-lock-tools", + "binary": "screen-lock-tools", + "loads": [ + "screen-lock-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/searxng/module.json.captured b/go/testdata/beside/mesh-catalog/modules/searxng/module.json.captured new file mode 100644 index 0000000..cee18e4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/searxng/module.json.captured @@ -0,0 +1,229 @@ +{ + "module": "searxng", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "secret": { + "path": "${dir:mesh-state}/secret", + "taken": "at-start" + } + }, + "listens": [ + { + "name": "web", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the search pages" + } + ], + "data": { + "own": [ + { + "id": "valkey", + "path": "${dir:valkey-data}", + "class": "cache", + "why": "the search cache" + } + ] + }, + "config": { + "general": { + "type": "object", + "description": "SearXNG's general section of settings.yml, laid over the file's own and SearXNG's defaults: the instance's name, contact and debug switches.", + "trusted": true, + "default": {} + }, + "brand": { + "type": "object", + "description": "SearXNG's brand section of settings.yml, laid over the file's own and SearXNG's defaults: the links the pages show for the instance's source, issues and documentation.", + "trusted": true, + "default": {} + }, + "search": { + "type": "object", + "description": "SearXNG's search section of settings.yml, laid over the file's own and SearXNG's defaults: safe search, autocomplete, the result formats offered and how long an engine is suspended after an error.", + "trusted": true, + "default": { + "formats": [ + "html", + "json" + ] + } + }, + "server": { + "type": "object", + "description": "SearXNG's server section of settings.yml, laid over the file's own and SearXNG's defaults: the public base URL, the request method, the limiter and the image proxy; its secret_key is the vault's.", + "trusted": true, + "default": { + "secret_key": "${secret:secret}", + "base_url": false, + "limiter": false, + "image_proxy": false, + "public_instance": false + } + }, + "ui": { + "type": "object", + "description": "SearXNG's ui section of settings.yml, laid over the file's own and SearXNG's defaults: the theme, the default locale and how results are shown.", + "trusted": true, + "default": {} + }, + "outgoing": { + "type": "object", + "description": "SearXNG's outgoing section of settings.yml, laid over the file's own and SearXNG's defaults: how SearXNG reaches the engines: timeouts, connection pool and proxies.", + "trusted": true, + "default": {} + }, + "engines": { + "type": "list", + "description": "SearXNG's engines section of settings.yml, laid over the file's own and SearXNG's defaults: the engines to add or change, each by its name.", + "trusted": true, + "default": [] + }, + "plugins": { + "type": "object", + "description": "SearXNG's plugins section of settings.yml, laid over the file's own and SearXNG's defaults: the plugins to switch on or off, by their id.", + "trusted": true, + "default": {} + }, + "categories_as_tabs": { + "type": "object", + "description": "SearXNG's categories_as_tabs section of settings.yml, laid over the file's own and SearXNG's defaults: the categories shown as tabs above the results.", + "trusted": true, + "default": {} + }, + "preferences": { + "type": "object", + "description": "SearXNG's preferences section of settings.yml, laid over the file's own and SearXNG's defaults: the preferences a user cannot change (lock).", + "trusted": true, + "default": {} + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "valkey-data", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "searxng" + }, + { + "id": "cache", + "type": "container", + "name": "valkey", + "image": "valkey/valkey@sha256:b21fd94099dcd4bc6b2b9230daef69b6558b887ad4a2a1afe56ff6e745a88cdb", + "network": "searxng", + "args": [ + "valkey-server", + "--save", + "30", + "1", + "--loglevel", + "warning" + ], + "volumes": [ + "${dir:valkey-data}:/data" + ] + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.yml", + "mode": "0600", + "merge": "json", + "takes-config": [ + "general", + "brand", + "search", + "server", + "ui", + "outgoing", + "engines", + "plugins", + "categories_as_tabs", + "preferences" + ], + "template": "files/settings.yml" + }, + { + "id": "server", + "type": "container", + "name": "searxng", + "image": "searxng/searxng@sha256:cd8812607ab73730a0b1a0dc4990223fe1b9e383f6f35947114d0bef7f8bb441", + "network": "searxng", + "ports": [ + "8080" + ], + "volumes": [ + "${dir:state}/settings.yml:/etc/searxng/settings.yml:ro" + ], + "restart-on": [ + "settings" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json" + } + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "searxng", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_SEARXNG_URL": "http://127.0.0.1:${port:8080}", + "MESH_SEARXNG_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/sensors/module.json.captured b/go/testdata/beside/mesh-catalog/modules/sensors/module.json.captured new file mode 100644 index 0000000..3df6783 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/sensors/module.json.captured @@ -0,0 +1,69 @@ +{ + "module": "sensors", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "sensors_summary", + "sensors_temperatures", + "sensors_fans", + "sensors_drives", + "sensors_faults", + "sensors_health" + ], + "resources": [ + { + "id": "smartmontools", + "type": "package", + "package": "smartmontools" + }, + { + "id": "drives", + "type": "process", + "name": "sensors-drives", + "artifact": "tools", + "run": [ + "./sensors", + "drives" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "watch", + "type": "process", + "name": "sensors-watch", + "artifact": "tools", + "run": [ + "./sensors", + "watch" + ], + "health": { + "kind": "tool", + "tool": "sensors_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/sensors", + "binary": "sensors", + "loads": [ + "sensors" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/showcase/module.json.captured b/go/testdata/beside/mesh-catalog/modules/showcase/module.json.captured new file mode 100644 index 0000000..e657e2a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/showcase/module.json.captured @@ -0,0 +1,207 @@ +{ + "module": "showcase", + "version": "1", + "templates": "templates", + "slug": "show", + "capabilities": [ + "container-runtime" + ], + "provides": [ + { + "name": "greeting", + "scope": "mesh" + } + ], + "serves": { + "greeting": { + "path": "/greeting" + } + }, + "requires": [ + "postgres-database" + ], + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "own-secrets": { + "broker": "${dir:mesh-state}/broker" + }, + "claims": [ + { + "name": "the-showcase", + "scope": "node" + } + ], + "emits": [ + "greeted", + "acknowledged" + ], + "consumes": [ + "showcase.greeted" + ], + "listens": [ + { + "name": "web", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the port the daemon itself listens on. The mesh assigns the machine-side number and tells consumers that one (ADR 0038)" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js", + "daemon/index.js", + "step/index.js", + "report/index.js" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + }, + { + "name": "helper", + "kind": "upstream", + "from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" + }, + { + "name": "templates", + "kind": "archive", + "from": "templates" + } + ] + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "showcase", + "shell": "/usr/bin/nologin", + "home": "/var/lib/showcase" + }, + { + "id": "logs", + "type": "access", + "path": "/var/log", + "mode": "0755" + }, + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0755", + "place": ".", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/showcase.env", + "mode": "0600", + "template": "templates/showcase.env" + }, + { + "id": "packed", + "type": "archive", + "path": "/opt/showcase", + "artifact": "files", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "showcase" + }, + { + "id": "tooling", + "type": "package", + "package": "jq" + }, + { + "id": "migrate", + "type": "process", + "name": "showcase-migrate", + "artifact": "code", + "run": [ + "node", + "step/index.js" + ], + "run-once": true, + "env-file": [ + "${dir:state}/showcase.env" + ] + }, + { + "id": "server", + "type": "process", + "name": "showcase", + "artifact": "code", + "run": [ + "node", + "daemon/index.js" + ], + "user": "showcase", + "env-file": [ + "${dir:state}/showcase.env" + ], + "restart-on": [ + "settings" + ] + }, + { + "id": "reporting", + "type": "process", + "name": "showcase-report", + "artifact": "code", + "run": [ + "node", + "report/index.js" + ], + "schedule": "0 3 * * *", + "env-file": [ + "${dir:state}/showcase.env" + ] + }, + { + "id": "tools", + "type": "container", + "name": "the-showcase", + "artifact": "helper", + "network": "showcase", + "volumes": [ + "${dir:mesh-state}/broker:/run/secrets/broker:ro" + ], + "env": { + "MESH_BROKER_FILE": "/run/secrets/broker" + }, + "args": [ + "sleep", + "infinity" + ] + } + ], + "seats": [ + { + "name": "the-showcase", + "scope": "node" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/slack/module.json.captured b/go/testdata/beside/mesh-catalog/modules/slack/module.json.captured new file mode 100644 index 0000000..9feeedb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/slack/module.json.captured @@ -0,0 +1,41 @@ +{ + "module": "slack", + "version": "1", + "moved": true, + "requires": [ + "x11-display" + ], + "tools": [ + "slack_status", + "slack_log", + "slack_restart", + "slack_check" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/slack-tools", + "binary": "slack-tools", + "loads": [ + "slack-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "items": [ + { + "collection": "contribution:node-display-session:config", + "name": "display-session", + "template": "files/display-session" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/snapd/module.json.captured b/go/testdata/beside/mesh-catalog/modules/snapd/module.json.captured new file mode 100644 index 0000000..93ad730 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/snapd/module.json.captured @@ -0,0 +1,31 @@ +{ + "module": "snapd", + "version": "1", + "tools": [ + "snapd_status", + "snapd_list", + "snapd_info", + "snapd_updates", + "snapd_disk_usage", + "snapd_services", + "snapd_changes", + "snapd_install", + "snapd_remove", + "snapd_refresh" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/snapd-tools", + "binary": "snapd-tools", + "loads": [ + "snapd-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/ssh-client/module.json.captured b/go/testdata/beside/mesh-catalog/modules/ssh-client/module.json.captured new file mode 100644 index 0000000..a696544 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ssh-client/module.json.captured @@ -0,0 +1,112 @@ +{ + "module": "ssh-client", + "version": "1", + "moved": true, + "tools": [ + "ssh_client_hosts", + "ssh_client_resolve", + "ssh_client_check", + "ssh_client_keys", + "ssh_client_authorized", + "ssh_client_revoke", + "ssh_client_known_host", + "ssh_client_test" + ], + "requires": [ + "forge-account" + ], + "state": [ + "keys" + ], + "data": { + "own": [ + { + "id": "ssh", + "path": "${dir:ssh-dir}", + "class": "valuable", + "why": "the operator's keys and known hosts" + } + ] + }, + "resources": [ + { + "id": "ssh-dir", + "type": "directory", + "path": "${machine:account-home}/.ssh", + "owner": "${machine:account}", + "mode": "0700", + "unknown": "module" + }, + { + "id": "config-d", + "type": "directory", + "path": "${machine:account-home}/.ssh/config.d", + "owner": "${machine:account}", + "mode": "0700", + "unknown": "module" + }, + { + "id": "config", + "type": "file", + "path": "${machine:account-home}/.ssh/config", + "owner": "${machine:account}", + "mode": "0600", + "into": "block", + "at": "start", + "template": "files/config" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${machine:account-home}/.ssh/known_hosts", + "owner": "${machine:account}", + "mode": "0644", + "into": "block", + "template": "files/known_hosts" + }, + { + "id": "forge-host", + "type": "file", + "path": "${machine:account-home}/.ssh/config.d/10-forge", + "owner": "${machine:account}", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "git-config", + "type": "file", + "path": "${machine:account-home}/.config/git/config", + "owner": "${machine:account}", + "mode": "0644", + "into": "block", + "template": "files/gitconfig" + } + ], + "facts": { + "mesh-hosts": { + "path": ".ssh/config.d/00-mesh", + "home": true, + "template": "# Generated by the mesh. Do not edit — module ssh-client writes this file whenever a machine\n# joins, leaves or is renamed. ~/.ssh/config includes it first, so these hosts win over every\n# later line; a host of your own goes below the mesh's region in ~/.ssh/config.\n{{range .Machines}}{{if ne .Name $.Node}}\nHost {{.Name}} {{.FQDN}}\n HostName {{.FQDN}}\n{{if .Account}} User {{.Account}}\n{{end}}{{end}}{{end}}" + } + }, + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/ssh-client-tools", + "binary": "ssh-client-tools", + "loads": [ + "ssh-client-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/sshd/module.json.captured b/go/testdata/beside/mesh-catalog/modules/sshd/module.json.captured new file mode 100644 index 0000000..066e058 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/sshd/module.json.captured @@ -0,0 +1,54 @@ +{ + "module": "sshd", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the operator's way into the machine when the mesh cannot reach it: a build that breaks it is found only when that way is needed, which no gate sees (hq ADR 0236)" + }, + "capabilities": [ + "package-manager", + "service-manager" + ], + "listens": [ + { + "name": "ssh", + "port": 22, + "protocol": "tcp", + "from": "anywhere", + "why": "the operator's own door. From anywhere because the machines that need it are exactly the ones not on the mesh yet \u2014 and locking the operator out is the one failure a firewall must never arrange" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "openssh" + }, + { + "id": "config", + "type": "file", + "path": "/etc/ssh/sshd_config.d/10-mesh.conf", + "mode": "0644", + "template": "files/10-mesh.conf" + }, + { + "id": "run", + "type": "service", + "unit": "sshd.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "config" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/step-ca/module.json.captured b/go/testdata/beside/mesh-catalog/modules/step-ca/module.json.captured new file mode 100644 index 0000000..b11d2b9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/step-ca/module.json.captured @@ -0,0 +1,106 @@ +{ + "module": "step-ca", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "provides": [ + { + "name": "internal-acme-ca", + "scope": "mesh" + } + ], + "serves": { + "internal-acme-ca": { + "path": "/acme/acme/directory", + "roots": "/roots.pem" + } + }, + "listens": [ + { + "name": "acme", + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the ACME directory and step-ca API; a proxy on any node reaches it here over the mesh to obtain certificates, and its single listen is what the consumer is told the port is" + } + ], + "own-secrets": { + "password": "${dir:mesh-state}/password" + }, + "data": { + "own": [ + { + "id": "home", + "path": "${dir:home}", + "class": "valuable", + "why": "the mesh's certificate authority: its keys and its database of what it issued" + } + ] + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "home", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "place": ".", + "unknown": "program" + }, + { + "id": "config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0644", + "template": "files/config.json", + "takes-config": [], + "merge": "json" + }, + { + "id": "init-env", + "type": "file", + "path": "${dir:mesh-state}/init.env", + "mode": "0600", + "template": "files/init.env" + }, + { + "id": "server", + "type": "container", + "name": "step-ca", + "image": "smallstep/step-ca@sha256:a2b17872915c193259b75a5474c398326f41bd199f0842093e52cf4182bc8270", + "health": { + "kind": "runtime" + }, + "network": "host", + "env-file": [ + "${dir:mesh-state}/init.env" + ], + "env": { + "DOCKER_STEPCA_INIT_NAME": "Mesh Internal CA", + "DOCKER_STEPCA_INIT_ACME": "true", + "DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT": "false" + }, + "volumes": [ + "${dir:home}:/home/step", + "${dir:mesh-state}:/run/mesh:ro" + ], + "secrets-in-environment": "the entrypoint honours DOCKER_STEPCA_INIT_PASSWORD_FILE; convertible, awaiting a bed that proves it" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/sudo/module.json.captured b/go/testdata/beside/mesh-catalog/modules/sudo/module.json.captured new file mode 100644 index 0000000..364ce5f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/sudo/module.json.captured @@ -0,0 +1,47 @@ +{ + "module": "sudo", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "sudo_rules", + "sudo_check", + "sudo_escalation", + "sudo_drop_ins" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "sudo" + }, + { + "id": "operator", + "type": "file", + "path": "/etc/sudoers.d/10-mesh-operator", + "mode": "0440", + "template": "files/10-mesh-operator" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/sudo-tools", + "binary": "sudo-tools", + "loads": [ + "sudo-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/module.json.captured b/go/testdata/beside/mesh-catalog/modules/supabase/module.json.captured new file mode 100644 index 0000000..14ddbcd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/module.json.captured @@ -0,0 +1,659 @@ +{ + "module": "supabase", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "api", + "port": 8000, + "protocol": "tcp", + "from": "mesh", + "why": "the Kong gateway: the REST, auth, storage, realtime, functions and GraphQL APIs under their /\u2026/v1 paths, and Studio at / behind the dashboard password. The one surface people and apps use, reached through the route" + }, + { + "name": "pooler-session", + "port": 5432, + "protocol": "tcp", + "from": "mesh", + "why": "Supavisor's session-mode Postgres port, for clients that connect to the database directly as ." + }, + { + "name": "pooler-transaction", + "port": 6543, + "protocol": "tcp", + "from": "mesh", + "why": "Supavisor's transaction-mode Postgres port" + } + ], + "requires": [ + "route" + ], + "own-secrets": { + "postgres": "${dir:state}/postgres.secret", + "jwt": "${dir:state}/jwt.secret", + "anon-key": "${dir:state}/anon-key.secret", + "service-role-key": "${dir:state}/service-role-key.secret", + "dashboard-user": "${dir:state}/dashboard-user.secret", + "dashboard": "${dir:state}/dashboard.secret", + "logflare": { + "path": "${dir:state}/logflare.secret", + "taken": "at-start" + }, + "pooler-vault": "${dir:state}/pooler-vault.secret", + "key-base-a": "${dir:state}/key-base-a.secret", + "key-base-b": "${dir:state}/key-base-b.secret", + "openai": { + "path": "${dir:state}/openai.secret", + "issued-by": "outside" + } + }, + "contributes": { + "route": { + "label": "supabase", + "endpoint": "api" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "data": { + "own": [ + { + "id": "db", + "path": "${dir:db-data}", + "class": "valuable", + "backup": { + "dump": "docker exec supabase-db pg_dumpall -h 127.0.0.1 -U supabase_admin > ${dir:dumps}/all.sql.partial && mv ${dir:dumps}/all.sql.partial ${dir:dumps}/all.sql", + "into": "dumps" + }, + "why": "every table; copied by pg_dumpall inside the database's container (local connections are trusted there), since a running store's files are not a consistent copy" + }, + { + "id": "dumps", + "path": "${dir:dumps}", + "class": "rebuildable", + "why": "last night's dump of the database, made again every night" + }, + { + "id": "storage", + "path": "${dir:storage}", + "class": "valuable", + "why": "uploaded objects" + }, + { + "id": "functions", + "path": "${dir:functions}", + "class": "valuable", + "why": "the edge functions' code" + }, + { + "id": "db-config", + "path": "${dir:db-config}", + "class": "rebuildable", + "why": "the database's configuration, seeded again" + } + ] + }, + "config": { + "pooler": { + "type": "object", + "description": "The connection pooler's settings, laid over the file's: its tenant, pool size and the most client connections it takes.", + "trusted": true, + "default": { + "tenant": "default", + "pool-size": 20, + "max-client-connections": 100 + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "db-init", + "type": "directory", + "path": "${dir:state}/db-init", + "mode": "0755", + "unknown": "program" + }, + { + "id": "functions-main-dir", + "type": "directory", + "path": "${dir:state}/functions-main", + "mode": "0755", + "unknown": "program" + }, + { + "id": "db-data", + "type": "directory", + "mode": "0700", + "owner": "105:106", + "unknown": "program" + }, + { + "id": "dumps", + "type": "directory", + "path": "${dir:state}/dumps", + "mode": "0700", + "unknown": "program" + }, + { + "id": "db-config", + "type": "directory", + "mode": "0755", + "owner": "105:106", + "unknown": "program" + }, + { + "id": "storage", + "type": "directory", + "mode": "0755", + "unknown": "program" + }, + { + "id": "functions", + "type": "directory", + "mode": "0755", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0644", + "merge": "json", + "takes-config": ["pooler"], + "template": "files/settings.json" + }, + { + "id": "kong-conf", + "type": "file", + "path": "${dir:state}/kong.yml", + "mode": "0600", + "owner": "100:65533", + "template": "files/kong.yml" + }, + { + "id": "auth-conf", + "type": "file", + "path": "${dir:state}/gotrue.env", + "mode": "0600", + "owner": "1000:1000", + "template": "files/gotrue.env" + }, + { + "id": "rest-conf", + "type": "file", + "path": "${dir:state}/postgrest.conf", + "mode": "0600", + "owner": "1000:1000", + "template": "files/postgrest.conf" + }, + { + "id": "vector-conf", + "type": "file", + "path": "${dir:state}/vector.yml", + "mode": "0600", + "template": "files/vector.yml" + }, + { + "id": "pooler-conf", + "type": "file", + "path": "${dir:state}/pooler.exs", + "mode": "0644", + "template": "files/pooler.exs" + }, + { + "id": "db-init-roles", + "type": "file", + "path": "${dir:state}/db-init/roles.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/roles.sql" + }, + { + "id": "db-init-jwt", + "type": "file", + "path": "${dir:state}/db-init/jwt.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/jwt.sql" + }, + { + "id": "db-init-realtime", + "type": "file", + "path": "${dir:state}/db-init/realtime.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/realtime.sql" + }, + { + "id": "db-init-supabase", + "type": "file", + "path": "${dir:state}/db-init/supabase.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/supabase.sql" + }, + { + "id": "db-init-logs", + "type": "file", + "path": "${dir:state}/db-init/logs.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/logs.sql" + }, + { + "id": "db-init-pooler", + "type": "file", + "path": "${dir:state}/db-init/pooler.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/pooler.sql" + }, + { + "id": "db-init-webhooks", + "type": "file", + "path": "${dir:state}/db-init/webhooks.sql", + "mode": "0600", + "owner": "105:106", + "template": "files/db-init/webhooks.sql" + }, + { + "id": "functions-main", + "type": "file", + "path": "${dir:state}/functions-main/index.ts", + "mode": "0644", + "template": "files/functions-main/index.ts" + }, + { + "id": "realtime-env", + "type": "file", + "path": "${dir:state}/realtime-env.env", + "mode": "0600", + "template": "files/realtime-env.env" + }, + { + "id": "storage-env", + "type": "file", + "path": "${dir:state}/storage-env.env", + "mode": "0600", + "template": "files/storage-env.env" + }, + { + "id": "meta-env", + "type": "file", + "path": "${dir:state}/meta-env.env", + "mode": "0600", + "template": "files/meta-env.env" + }, + { + "id": "functions-env", + "type": "file", + "path": "${dir:state}/functions-env.env", + "mode": "0600", + "template": "files/functions-env.env" + }, + { + "id": "analytics-env", + "type": "file", + "path": "${dir:state}/analytics-env.env", + "mode": "0600", + "template": "files/analytics-env.env" + }, + { + "id": "analytics-start", + "type": "file", + "path": "${dir:state}/analytics-start.sh", + "mode": "0644", + "template": "files/analytics-start.sh" + }, + { + "id": "analytics-reconcile", + "type": "file", + "path": "${dir:state}/analytics-reconcile.exs", + "mode": "0644", + "template": "files/analytics-reconcile.exs" + }, + { + "id": "studio-env", + "type": "file", + "path": "${dir:state}/studio-env.env", + "mode": "0600", + "template": "files/studio-env.env" + }, + { + "id": "pooler-env", + "type": "file", + "path": "${dir:state}/pooler-env.env", + "mode": "0600", + "template": "files/pooler-env.env" + }, + { + "id": "net", + "type": "network", + "name": "supabase" + }, + { + "id": "vector", + "type": "container", + "name": "supabase-vector", + "image": "timberio/vector@sha256:4bc04aca94a44f04b427a490f346e7397ef7ce61fe589d718f744f7d92cb5c80", + "network": "supabase", + "args": [ + "--config", + "/etc/vector/vector.yml" + ], + "volumes": [ + "${dir:state}/vector.yml:/etc/vector/vector.yml:ro", + "/var/run/docker.sock:/var/run/docker.sock:ro" + ], + "restart-on": [ + "vector-conf" + ] + }, + { + "id": "db-config-seed", + "type": "container", + "name": "supabase-db-config-seed", + "image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777", + "network": "supabase", + "run-once": true, + "args": [ + "sh", + "-c", + "cp -rn /etc/postgresql-custom/. /seed/ && chown -R postgres:postgres /seed" + ], + "volumes": [ + "${dir:db-config}:/seed" + ] + }, + { + "id": "db", + "type": "container", + "name": "supabase-db", + "image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777", + "health": { + "kind": "runtime" + }, + "network": "supabase", + "args": [ + "postgres", + "-c", + "config_file=/etc/postgresql/postgresql.conf", + "-c", + "log_min_messages=fatal" + ], + "env": { + "POSTGRES_HOST": "/var/run/postgresql", + "PGPORT": "5432", + "POSTGRES_PORT": "5432", + "PGDATABASE": "postgres", + "POSTGRES_DB": "postgres", + "POSTGRES_PASSWORD_FILE": "/run/secrets/postgres" + }, + "volumes": [ + "${dir:state}/postgres.secret:/run/secrets/postgres:ro", + "${dir:state}/db-init/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro", + "${dir:state}/db-init/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro", + "${dir:state}/db-init/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro", + "${dir:state}/db-init/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro", + "${dir:state}/db-init/supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro", + "${dir:state}/db-init/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro", + "${dir:state}/db-init/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:ro", + "${dir:db-data}:/var/lib/postgresql/data", + "${dir:db-config}:/etc/postgresql-custom" + ] + }, + { + "id": "db-ready", + "type": "container", + "name": "supabase-db-ready", + "image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777", + "network": "supabase", + "run-once": true, + "args": [ + "sh", + "-c", + "for i in $(seq 150); do pg_isready -h supabase-db -p 5432 -U postgres && exit 0; sleep 2; done; echo 'the database did not come up' >&2; exit 1" + ] + }, + { + "id": "analytics", + "type": "container", + "name": "supabase-analytics", + "image": "supabase/logflare@sha256:e693c787ffe1ae17b6e4e920a3cdd212416d3e1f97e1bd7cb5b67de0abbb0264", + "network": "supabase", + "env-file": [ + "${dir:state}/analytics-env.env" + ], + "volumes": [ + "${dir:state}/analytics-start.sh:/run/logflare/start.sh:ro", + "${dir:state}/vector.yml:/run/logflare/vector.yml:ro", + "${dir:state}/analytics-reconcile.exs:/run/logflare/reconcile.exs:ro" + ], + "args": [ + "sh", + "/run/logflare/start.sh" + ], + "restart-on": [ + "analytics-env", + "analytics-start", + "analytics-reconcile", + "vector-conf" + ], + "secrets-in-environment": "Logflare reads its database password and API key from the environment only. The API key is never in a URL the mesh writes: logflare prints a failed request's URL, so vector sends the key in the x-api-key header, and the start script refuses to start logflare while vector.yml says otherwise" + }, + { + "id": "analytics-ready", + "type": "container", + "name": "supabase-analytics-ready", + "image": "supabase/logflare@sha256:e693c787ffe1ae17b6e4e920a3cdd212416d3e1f97e1bd7cb5b67de0abbb0264", + "network": "supabase", + "run-once": true, + "args": [ + "sh", + "-c", + "for i in $(seq 150); do curl -fsS -o /dev/null http://supabase-analytics:4000/health && exit 0; sleep 2; done; echo 'analytics did not come up' >&2; exit 1" + ] + }, + { + "id": "kong", + "type": "container", + "name": "supabase-kong", + "image": "kong@sha256:1b53405d8680a09d6f44494b7990bf7da2ea43f84a258c59717d4539abf09f6d", + "health": { + "kind": "runtime" + }, + "network": "supabase", + "ports": [ + "8000" + ], + "env": { + "KONG_DATABASE": "off", + "KONG_DECLARATIVE_CONFIG": "/home/kong/kong.yml", + "KONG_DNS_ORDER": "LAST,A,CNAME", + "KONG_PLUGINS": "request-transformer,cors,key-auth,acl,basic-auth", + "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE": "160k", + "KONG_NGINX_PROXY_PROXY_BUFFERS": "64 160k" + }, + "volumes": [ + "${dir:state}/kong.yml:/home/kong/kong.yml:ro" + ], + "restart-on": [ + "kong-conf" + ] + }, + { + "id": "auth", + "type": "container", + "name": "supabase-auth", + "image": "supabase/gotrue@sha256:f8b871f46f3f9c5306af00d2fb856fb5479db730bb6d4fd2757b2da8abe953f9", + "network": "supabase", + "args": [ + "auth", + "-c", + "/etc/gotrue/gotrue.env" + ], + "volumes": [ + "${dir:state}/gotrue.env:/etc/gotrue/gotrue.env:ro" + ], + "restart-on": [ + "auth-conf" + ] + }, + { + "id": "rest", + "type": "container", + "name": "supabase-rest", + "image": "postgrest/postgrest@sha256:2cf1efd2c9c2e7606610c113cc73e936d8ce9ba089271cb9cbf11aa564bc30c7", + "network": "supabase", + "args": [ + "postgrest", + "/etc/postgrest/postgrest.conf" + ], + "volumes": [ + "${dir:state}/postgrest.conf:/etc/postgrest/postgrest.conf:ro" + ], + "restart-on": [ + "rest-conf" + ] + }, + { + "id": "realtime", + "type": "container", + "name": "realtime-dev.supabase-realtime", + "image": "supabase/realtime@sha256:104fe2e4e09a7c81eebe0c39e3a9b9dca4684f55470ca01cb3c080d1d56453a1", + "network": "supabase", + "env-file": [ + "${dir:state}/realtime-env.env" + ], + "secrets-in-environment": "Supabase Realtime (Elixir release) reads DB_PASSWORD, API_JWT_SECRET and SECRET_KEY_BASE from its environment only; no file or _FILE form" + }, + { + "id": "imgproxy", + "type": "container", + "name": "supabase-imgproxy", + "image": "darthsim/imgproxy@sha256:0facd355d50f3be665ebe674486f2b2e9cdaebd3f74404acd9b7fece2f661435", + "network": "supabase", + "env": { + "IMGPROXY_BIND": ":5001", + "IMGPROXY_LOCAL_FILESYSTEM_ROOT": "/", + "IMGPROXY_USE_ETAG": "true", + "IMGPROXY_ENABLE_WEBP_DETECTION": "true" + }, + "volumes": [ + "${dir:storage}:/var/lib/storage" + ] + }, + { + "id": "storage-api", + "type": "container", + "name": "supabase-storage", + "image": "supabase/storage-api@sha256:1e85dad48e8b3e85890a555e5114dc7ee48c2e8be4cfd97dd4e3564b4f104fcd", + "network": "supabase", + "volumes": [ + "${dir:storage}:/var/lib/storage" + ], + "env-file": [ + "${dir:state}/storage-env.env" + ], + "secrets-in-environment": "Supabase Storage reads its keys and DATABASE_URL from the environment only" + }, + { + "id": "meta", + "type": "container", + "name": "supabase-meta", + "image": "supabase/postgres-meta@sha256:d0a96973e9f1b6303f7500421a459af3d7273b3f9f1db38610899f9f573da7c7", + "health": { + "kind": "runtime" + }, + "network": "supabase", + "env-file": [ + "${dir:state}/meta-env.env" + ], + "secrets-in-environment": "postgres-meta reads PG_META_DB_PASSWORD from the environment only" + }, + { + "id": "edge-functions", + "type": "container", + "name": "supabase-edge-functions", + "image": "supabase/edge-runtime@sha256:6f7ac3b363f6b278ff9235672a81504ad07709a07f62c50cd4fffb6708842ff1", + "network": "supabase", + "args": [ + "start", + "--main-service", + "/home/deno/main" + ], + "volumes": [ + "${dir:functions}:/home/deno/functions", + "${dir:state}/functions-main/index.ts:/home/deno/main/index.ts:ro" + ], + "env-file": [ + "${dir:state}/functions-env.env" + ], + "secrets-in-environment": "the edge runtime's main service hands its own environment (JWT secret, service key, database URL) to every function worker by design; there is no other channel" + }, + { + "id": "studio", + "type": "container", + "name": "supabase-studio", + "image": "supabase/studio@sha256:ebf492ba82db1fc9497f56e4cdebf5cddb464b61f6227e21c8f5f3693f9dbbc5", + "health": { + "kind": "runtime" + }, + "network": "supabase", + "env-file": [ + "${dir:state}/studio-env.env" + ], + "restart-on": [ + "studio-env" + ], + "secrets-in-environment": "Studio (Next.js) reads its keys, the database password and the OpenAI key from the environment only" + }, + { + "id": "pooler", + "type": "container", + "name": "supabase-pooler", + "image": "supabase/supavisor@sha256:2f24ab556380db7aaf402d2ec5db09d1974739e685781a91a77ab5ae0dadb3a5", + "network": "supabase", + "ports": [ + "5432", + "6543" + ], + "args": [ + "/bin/sh", + "-c", + "/app/bin/migrate && /app/bin/supavisor eval \"$(cat /etc/pooler/pooler.exs)\" && /app/bin/server" + ], + "volumes": [ + "${dir:state}/pooler.exs:/etc/pooler/pooler.exs:ro", + "${dir:state}/settings.json:/etc/pooler/settings.json:ro" + ], + "restart-on": [ + "pooler-conf", + "settings" + ], + "env-file": [ + "${dir:state}/pooler-env.env" + ], + "secrets-in-environment": "Supavisor reads DATABASE_URL, SECRET_KEY_BASE, VAULT_ENC_KEY and the JWT secret from the environment only" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/systemd-networkd/module.json.captured b/go/testdata/beside/mesh-catalog/modules/systemd-networkd/module.json.captured new file mode 100644 index 0000000..0c4c109 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/systemd-networkd/module.json.captured @@ -0,0 +1,65 @@ +{ + "module": "systemd-networkd", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the machine's network: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)" + }, + "slug": "networkd", + "requires": [ + "wildcard-resolution" + ], + "capabilities": [ + "service-manager", + "uplink-systemd-networkd" + ], + "claims": [ + { + "name": "node-uplink", + "scope": "node" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/uplink-tools", + "binary": "uplink-tools", + "loads": [ + "uplink-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + }, + "facts": { + "resolvers": { + "path": "/etc/resolv.conf", + "template": "# Managed by the mesh, and written by the module holding this machine's uplink:\n# the program that manages the machine's network would otherwise rewrite this\n# file on every change of network, so its holder is the one that writes it\n# (novox/hq ADR 0117, ADR 0223). Replaced on every push; edit nothing here.\n#\n# Every resolver of the mesh, by address, and nothing else (novox/hq ADR 0223) —\n# this machine's own first when it holds one, then the others by name. Each\n# answers the mesh's names from the same roster and forwards every other name, so\n# whichever answers first gives the one answer. There is no public resolver here:\n# a C library that asks every listed server at once and takes the first reply —\n# musl, so every Alpine container — took a public resolver's \"no such name\" for\n# a mesh name and failed. A machine that reaches none of these has no names until\n# it does. Containers copy these lines from their machine.\n{{range index .Holders \"mesh-dns-resolver\"}}nameserver {{.Address}}\n{{end}}options timeout:1 attempts:2 edns0\n" + } + }, + "resources": [ + { + "id": "config", + "type": "file", + "path": "/etc/systemd/network/00-mesh0.network", + "mode": "0644", + "template": "files/00-mesh0.network" + }, + { + "id": "service", + "type": "service", + "unit": "systemd-networkd.service", + "reload-on": [ + "config" + ] + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/systemd-resolved/module.json.captured b/go/testdata/beside/mesh-catalog/modules/systemd-resolved/module.json.captured new file mode 100644 index 0000000..b850afc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/systemd-resolved/module.json.captured @@ -0,0 +1,110 @@ +{ + "module": "systemd-resolved", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the machine's names: a build that breaks this resolver stops every name resolving on a machine a person works on, the bus's included, and then neither the gate's rollback nor a push reaches it (hq ADR 0236, ADR 0247)" + }, + "provides": [ + { + "name": "split-dns", + "reach": "machine" + } + ], + "requires": [ + "wildcard-resolution" + ], + "capabilities": [ + "service-manager" + ], + "claims": [ + { + "name": "node-resolver", + "scope": "node", + "serves": [ + "routes", + "route", + "unroute" + ] + } + ], + "listens": [ + { + "name": "dns-udp", + "port": 53, + "protocol": "udp", + "from": "machine", + "fixed": true, + "why": "this machine's own resolver (ADR 0247), on loopback and on its private address for its own containers; never another machine's, so a VPN's domains routed here are asked by nothing beyond this machine" + }, + { + "name": "dns-tcp", + "port": 53, + "protocol": "tcp", + "from": "machine", + "fixed": true, + "why": "the same names over tcp, which a resolver answers on for an answer too large for a datagram" + } + ], + "facts": { + "kept": { + "path": "/etc/node-resolver/resolv.conf", + "template": "# Managed by the mesh, and written by the module holding this machine's own resolver\n# (module systemd-resolved, novox/hq ADR 0247). On every other machine the module holding\n# the uplink writes this file, listing the mesh's resolvers (ADR 0223); here something\n# requires names routed by domain - a VPN client's domains to its own servers - so the file\n# names this machine's own resolver alone, which sends those domains over the VPN's link and\n# every other name to the mesh's resolvers. One server listed, so one answer per name.\n#\n# Its address on the private network, not loopback: a container copies this file, and\n# this address is one it can reach. Another program writing this file is put back by the\n# module's guard, which keeps what it wrote for whoever handles it on this machine.\n# Replaced on every push; edit nothing here.\n{{$own := \"\"}}{{range .Machines}}{{if eq .Name $.Node}}{{$own = .Address}}{{end}}{{end}}nameserver {{if $own}}{{$own}}{{else}}127.0.0.53{{end}}\noptions timeout:1 attempts:2 edns0\n" + }, + "resolved": { + "path": "/etc/systemd/resolved.conf.d/50-mesh.conf", + "template": "# Managed by the mesh (module systemd-resolved, novox/hq ADR 0247). Replaced on every\n# push; a drop-in of the operator's that sorts after this one overrides it, and is theirs.\n#\n# The mesh's resolvers, every one of them (ADR 0223): they answer every name no link's own\n# domains route elsewhere. ~. makes them the default route for names, and a link's servers\n# answer only the domains routed to them (the module's verb `route`).\n[Resolve]\nDNS={{range index .Holders \"mesh-dns-resolver\"}}{{.Address}} {{end}}\nDomains=~.\n# No compiled-in public fallback: a public resolver beside the mesh's is what ADR 0223\n# removed, because one of them said \"no such name\" for a mesh name and was believed.\nFallbackDNS=\n# The stub on loopback for this machine, and on its private address for its containers,\n# which cannot reach loopback. The packet filter admits this machine's own guests and\n# nobody else: another machine never asks this resolver (ADR 0247).\nDNSStubListener=yes\n{{$own := \"\"}}{{range .Machines}}{{if eq .Name $.Node}}{{$own = .Address}}{{end}}{{end}}DNSStubListenerExtra={{$own}}\n# No cache: nothing on this machine keeps a copy of a mesh name or of a \"no such name\",\n# as before this resolver - each question is asked again (ADR 0223's objection to a\n# local forwarder was a copy disagreeing with the truth).\nCache=no\n# What this machine's own programs read from /etc/hosts they read themselves; containers\n# asking here get what the mesh's resolvers say, as before.\nReadEtcHosts=no\n# Names are the mesh's resolvers' and a routed link's to answer, never the local network's\n# guesses; validation stays the upstreams' (the mesh's resolvers pass the bit through).\nLLMNR=no\nMulticastDNS=no\nDNSSEC=no\nDNSOverTLS=no\n" + }, + "resolvers": { + "path": "/etc/resolv.conf", + "template": "# Managed by the mesh, and written by the module holding this machine's own resolver\n# (module systemd-resolved, novox/hq ADR 0247). On every other machine the module holding\n# the uplink writes this file, listing the mesh's resolvers (ADR 0223); here something\n# requires names routed by domain - a VPN client's domains to its own servers - so the file\n# names this machine's own resolver alone, which sends those domains over the VPN's link and\n# every other name to the mesh's resolvers. One server listed, so one answer per name.\n#\n# Its address on the private network, not loopback: a container copies this file, and\n# this address is one it can reach. Another program writing this file is put back by the\n# module's guard, which keeps what it wrote for whoever handles it on this machine.\n# Replaced on every push; edit nothing here.\n{{$own := \"\"}}{{range .Machines}}{{if eq .Name $.Node}}{{$own = .Address}}{{end}}{{end}}nameserver {{if $own}}{{$own}}{{else}}127.0.0.53{{end}}\noptions timeout:1 attempts:2 edns0\n" + }, + "suffix": { + "path": "/etc/node-resolver/suffix", + "template": "{{.Suffix}}\n" + } + }, + "resources": [ + { + "id": "service", + "type": "service", + "unit": "systemd-resolved.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "systemd-resolved.fact-resolved" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "guard", + "type": "process", + "name": "systemd-resolved-guard", + "artifact": "tools", + "run": [ + "./resolver-tools", + "guard" + ], + "health": { + "kind": "unit" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/resolver-tools", + "binary": "resolver-tools", + "loads": [ + "resolver-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/systemd/module.json.captured b/go/testdata/beside/mesh-catalog/modules/systemd/module.json.captured new file mode 100644 index 0000000..984ccf3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/systemd/module.json.captured @@ -0,0 +1,63 @@ +{ + "module": "systemd", + "version": "1", + "capabilities": [ + "service-manager", + "package-manager" + ], + "claims": [ + { + "name": "node-service-manager", + "scope": "node", + "serves": [ + "units", + "status", + "start", + "stop", + "restart", + "enable", + "disable", + "journal", + "failed", + "reset-failed", + "wanted-by", + "unlink-dangling" + ] + } + ], + "emits": [ + "link.removal" + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/systemd-tools", + "binary": "systemd-tools", + "loads": [ + "systemd-tools" + ], + "env": { + "MESH_SYSTEMD_STATE": "${dir:state}" + } + } + ] + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "systemd" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/telegram/module.json.captured b/go/testdata/beside/mesh-catalog/modules/telegram/module.json.captured new file mode 100644 index 0000000..95ae6f1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/telegram/module.json.captured @@ -0,0 +1,79 @@ +{ + "module": "telegram", + "version": "1", + "slug": "tgrm", + "runs-as": "telegram", + "claims": [ + { + "name": "channel", + "scope": "mesh", + "kind": "telegram", + "capabilities": [ + "deliver", + "reaches-away", + "silent", + "edit", + "max-length:4096", + "choice", + "verified-sender", + "exact-render", + "code-factor" + ] + }, + { + "name": "intake", + "scope": "mesh", + "kind": "telegram" + } + ], + "state": [ + "offset", + { + "name": "messages", + "ttl-seconds": 2592000 + } + ], + "own-secrets": { + "broker": "${dir:state}/broker", + "telegram-token": "${dir:state}/telegram-token" + }, + "secrets-owner": "telegram", + "tools": [ + "telegram_status" + ], + "resources": [ + { + "id": "account", + "type": "user", + "name": "telegram", + "shell": "/usr/bin/nologin", + "home": "/var/lib/telegram" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "telegram", + "place": ".", + "unknown": "program" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/telegram", + "binary": "telegram", + "loads": [ + "telegram" + ], + "env": { + "MESH_TELEGRAM_TOKEN_FILE": "${dir:state}/telegram-token" + } + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/time-sync/module.json.captured b/go/testdata/beside/mesh-catalog/modules/time-sync/module.json.captured new file mode 100644 index 0000000..c3d9b0c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/time-sync/module.json.captured @@ -0,0 +1,81 @@ +{ + "module": "time-sync", + "version": "1", + "capabilities": [ + "package-manager", + "service-manager" + ], + "tools": [ + "time_sync_status", + "time_sync_servers", + "time_sync_sync_now" + ], + "resources": [ + { + "id": "servers", + "type": "file", + "path": "/etc/systemd/timesyncd.conf.d/50-mesh.conf", + "mode": "0644", + "template": "files/50-mesh.conf", + "names-on-purpose": { + "0.europe.pool.ntp.org": "the public NTP pool's European zone: the world's time service, the same for every installation; nearest to every machine of this mesh", + "1.europe.pool.ntp.org": "the public NTP pool's European zone: the world's time service, the same for every installation; nearest to every machine of this mesh", + "2.europe.pool.ntp.org": "the public NTP pool's European zone: the world's time service, the same for every installation; nearest to every machine of this mesh", + "3.europe.pool.ntp.org": "the public NTP pool's European zone: the world's time service, the same for every installation; nearest to every machine of this mesh", + "0.arch.pool.ntp.org": "the distribution's own NTP pool, timesyncd's compiled-in fallback, stated so the drop-in says the whole of what timesyncd uses", + "1.arch.pool.ntp.org": "the distribution's own NTP pool, timesyncd's compiled-in fallback, stated so the drop-in says the whole of what timesyncd uses", + "2.arch.pool.ntp.org": "the distribution's own NTP pool, timesyncd's compiled-in fallback, stated so the drop-in says the whole of what timesyncd uses", + "3.arch.pool.ntp.org": "the distribution's own NTP pool, timesyncd's compiled-in fallback, stated so the drop-in says the whole of what timesyncd uses" + } + }, + { + "id": "retire-ntpd", + "type": "process", + "name": "time-sync-retire-ntpd", + "artifact": "tools", + "run": [ + "./time-sync-tools", + "retire", + "ntpd.service", + "ntpdate.service" + ], + "run-once": true + }, + { + "id": "daemon", + "type": "service", + "unit": "systemd-timesyncd.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "servers" + ] + }, + { + "id": "ntp", + "type": "package", + "package": "ntp", + "absent": true + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/time-sync-tools", + "binary": "time-sync-tools", + "loads": [ + "time-sync-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/triggerhappy/module.json.captured b/go/testdata/beside/mesh-catalog/modules/triggerhappy/module.json.captured new file mode 100644 index 0000000..18faaf4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/triggerhappy/module.json.captured @@ -0,0 +1,81 @@ +{ + "module": "triggerhappy", + "version": "1", + "moved": true, + "capabilities": [ + "service-manager" + ], + "claims": [ + { + "name": "node-hotkeys", + "scope": "node" + } + ], + "tools": [ + "triggerhappy_keys", + "triggerhappy_devices", + "triggerhappy_dump", + "triggerhappy_check" + ], + "resources": [ + { + "id": "drop-ins", + "type": "directory", + "path": "/etc/systemd/system/triggerhappy.service.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "as-account", + "type": "file", + "path": "/etc/systemd/system/triggerhappy.service.d/mesh.conf", + "mode": "0644", + "template": "files/service.conf" + }, + { + "id": "triggers-dir", + "type": "directory", + "path": "/etc/triggerhappy/triggers.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "triggers", + "type": "file", + "path": "/etc/triggerhappy/triggers.d/mesh.conf", + "mode": "0644", + "template": "files/triggers.conf" + }, + { + "id": "service", + "type": "service", + "unit": "triggerhappy.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "as-account", + "triggers" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/triggerhappy-tools", + "binary": "triggerhappy-tools", + "loads": [ + "triggerhappy-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/umami/module.json.captured b/go/testdata/beside/mesh-catalog/modules/umami/module.json.captured new file mode 100644 index 0000000..22ead41 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/umami/module.json.captured @@ -0,0 +1,142 @@ +{ + "module": "umami", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "postgres-database", + "route", + "secret" + ], + "contributes": { + "postgres-database": { + "name": "umami" + }, + "route": { + "label": "umami", + "endpoint": "web" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret", + "secret": { + "app-secret": "${dir:state}/app.secret", + "admin": "${dir:state}/admin.secret" + } + }, + "provides": [ + { + "name": "analytics", + "scope": "mesh" + } + ], + "serves": { + "analytics": {} + }, + "receives": { + "analytics": "${dir:grants}/mesh.json" + }, + "grants": { + "analytics": "${dir:grants}" + }, + "listens": [ + { + "name": "web", + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "one port serves two surfaces — the dashboard and the collection endpoint that the browsers of every tracked site POST to. Both are reached through the proxy, by name, so the port is how the proxy reaches this module and nothing else (novox/hq ADR 0045). It said \"anywhere\" and gave the reason that the collection endpoint must be public, which is true of the name and not of the port: opened, the machine-side port served the dashboard over plain HTTP to the internet, bypassing every rule the proxy applies by path" + } + ], + "data": { + "consumers": { + "analytics": { + "class": "valuable", + "in": "postgres-database", + "why": "a site's page views are recorded nowhere else" + } + } + }, + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "grants", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "template": "files/server.env" + }, + { + "id": "net", + "type": "network", + "name": "umami" + }, + { + "id": "server", + "type": "container", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/api/share/mesh-health", + "status": 404, + "body": "\"code\":\"not-found\"", + "timeout": "10s", + "needs": "postgres-database" + }, + "name": "umami", + "image": "ghcr.io/umami-software/umami@sha256:85909afc45bdcda1917394594a087421fdbb05610fded0fa9f6fb861abb2f367", + "network": "umami", + "env-file": [ + "${dir:state}/server.env" + ], + "ports": [ + "3000" + ], + "secrets-in-environment": "a Next.js/Prisma application: DATABASE_URL and APP_SECRET are read from the environment only; not convertible" + } + ], + "build": { + "artifacts": [ + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js", + "provisioner/index.js" + ], + "loads": [ + "tools/index.js", + "provisioner/index.js" + ], + "env": { + "MESH_RECEIVES": "${dir:grants}/mesh.json", + "MESH_UMAMI_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret", + "MESH_PROVISION_UMAMI_URL": "http://127.0.0.1:${port:3000}" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/unifi/module.json.captured b/go/testdata/beside/mesh-catalog/modules/unifi/module.json.captured new file mode 100644 index 0000000..2ee5cf4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/unifi/module.json.captured @@ -0,0 +1,203 @@ +{ + "module": "unifi", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "name": "web", + "port": 8443, + "protocol": "tcp", + "from": "mesh", + "why": "the controller web UI and API, over its own self-signed tls; named through the proxy as an https route" + }, + { + "name": "inform", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "device inform, how APs and switches check in and are adopted; the controller tells devices this number, so the machine must publish it on the same one" + }, + { + "name": "stun", + "port": 3478, + "protocol": "udp", + "from": "mesh", + "why": "STUN for managed devices; the controller tells devices this number, so the machine must publish it on the same one" + }, + { + "name": "discovery", + "port": 10001, + "protocol": "udp", + "from": "mesh", + "why": "device discovery broadcasts from unadopted devices and the UniFi apps" + }, + { + "name": "discovery-l2", + "port": 1900, + "protocol": "udp", + "from": "mesh", + "why": "make-controller-discoverable-on-L2 (SSDP); the software listens on 1900, which machines commonly have taken by another SSDP speaker" + }, + { + "name": "portal-tls", + "port": 8843, + "protocol": "tcp", + "from": "mesh", + "why": "the guest captive portal over https" + }, + { + "name": "portal", + "port": 8880, + "protocol": "tcp", + "from": "mesh", + "why": "the guest captive portal over http" + }, + { + "name": "speedtest", + "port": 6789, + "protocol": "tcp", + "from": "mesh", + "why": "mobile-app speed-test throughput measurement" + }, + { + "name": "syslog", + "port": 5514, + "protocol": "udp", + "from": "mesh", + "why": "remote syslog from managed devices" + } + ], + "data": { + "own": [ + { + "id": "data", + "path": "${dir:data}", + "class": "valuable", + "why": "the network's configuration and every adopted device" + } + ] + }, + "config": { + "site": { + "type": "string", + "description": "The UniFi site the module's tools act on, by its id; the file's default is default.", + "trusted": true, + "default": "default" + }, + "url": { + "type": "string", + "description": "The address the module's tools reach the UniFi controller at, as a URL; unset, the code falls back to its environment, else this machine's controller on its HTTPS port.", + "trusted": true, + "default": "" + }, + "username": { + "type": "string", + "description": "The UniFi account the module's tools sign in as with the vault's password; unset, the code falls back to its environment.", + "trusted": true, + "default": "" + } + }, + "resources": [ + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "data", + "type": "directory", + "mode": "0700", + "owner": "1000:1000", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "unifi-controller", + "image": "lscr.io/linuxserver/unifi-controller@sha256:0ae315a3a45635e443899e30e86bd507c2c48922cb27f4bc7241777885f4650e", + "ports": [ + "8443", + "8080", + "3478/udp", + "10001/udp", + "1900/udp", + "8843", + "8880", + "6789", + "5514/udp" + ], + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC", + "MEM_LIMIT": "1024", + "MEM_STARTUP": "1024" + }, + "volumes": [ + "${dir:data}:/config" + ] + }, + { + "id": "runtime-config", + "type": "file", + "path": "${dir:mesh-state}/config.json", + "mode": "0600", + "template": "files/config.json", + "merge": "json", + "takes-config": ["site", "url", "username"], + "protected": ["password"] + } + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "unifi", + "endpoint": "web", + "scheme": "https", + "insecure": true + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "own-secrets": { + "controller": "${dir:mesh-state}/controller" + }, + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_UNIFI_URL": "https://127.0.0.1:${port:8443}", + "MESH_UNIFI_CONFIG_FILE": "${dir:mesh-state}/config.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/website/module.json.captured b/go/testdata/beside/mesh-catalog/modules/website/module.json.captured new file mode 100644 index 0000000..481cfc6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/website/module.json.captured @@ -0,0 +1,60 @@ +{ + "module": "website", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "@", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, + "listens": [ + { + "name": "web", + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the public website over http; its public name is a route grant, and route-proxy reaches it on this published port" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "net", + "type": "network", + "name": "website" + }, + { + "id": "server", + "type": "container", + "name": "website", + "image": "registry-api.novox.be/novox/www@sha256:aa7ed20a293e1d7444c5c5d59b6d8bdb382bad159559a22e006810e379cae69c", + "health": { + "kind": "http", + "endpoint": "web", + "path": "/" + }, + "network": "website", + "ports": [ + "8080" + ], + "names-on-purpose": { + "registry-api.novox.be": "built outside the mesh, from the application's own repository, and pulled from the registry that built it; moves when that repository is a build source on the git seat (novox/hq ADR 0155, issue 122)" + } + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/xclip/module.json.captured b/go/testdata/beside/mesh-catalog/modules/xclip/module.json.captured new file mode 100644 index 0000000..bf06f6f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/xclip/module.json.captured @@ -0,0 +1,35 @@ +{ + "module": "xclip", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "xclip_copy", + "xclip_paste", + "xclip_targets", + "xclip_session" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "xclip" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/xclip-tools", + "binary": "xclip-tools", + "loads": [ + "xclip-tools" + ] + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/xdg/module.json.captured b/go/testdata/beside/mesh-catalog/modules/xdg/module.json.captured new file mode 100644 index 0000000..c3d191a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/xdg/module.json.captured @@ -0,0 +1,97 @@ +{ + "module": "xdg", + "version": "1", + "capabilities": [ + "package-manager" + ], + "tools": [ + "xdg_user_dirs", + "xdg_default", + "xdg_open_handlers", + "xdg_autostart", + "xdg_check" + ], + "config": { + "desktop-dir": { + "type": "string", + "description": "The desktop folder, as user-dirs.dirs writes it: \"$HOME/\", or \"$HOME/\" when the folder is not wanted; it differs between the workstations, so each says its own.", + "trusted": true, + "required": true + }, + "publicshare-dir": { + "type": "string", + "description": "The public share folder, as user-dirs.dirs writes it: \"$HOME/\", or \"$HOME/\" when the folder is not wanted; it differs between the workstations, so each says its own.", + "trusted": true, + "required": true + }, + "videos-dir": { + "type": "string", + "description": "The videos folder, as user-dirs.dirs writes it: \"$HOME/\", or \"$HOME/\" when the folder is not wanted; it differs between the workstations, so each says its own.", + "trusted": true, + "required": true + } + }, + "resources": [ + { + "id": "package-xdg-user-dirs", + "type": "package", + "package": "xdg-user-dirs" + }, + { + "id": "package-xdg-utils", + "type": "package", + "package": "xdg-utils" + }, + { + "id": "user-dirs", + "type": "file", + "path": "${machine:account-home}/.config/user-dirs.dirs", + "owner": "${machine:account}", + "mode": "0600", + "template": "files/user-dirs.dirs" + }, + { + "id": "user-dirs-locale", + "type": "file", + "path": "${machine:account-home}/.config/user-dirs.locale", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/user-dirs.locale" + }, + { + "id": "user-dirs-update", + "type": "file", + "path": "${machine:account-home}/.config/user-dirs.conf", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/user-dirs.conf" + }, + { + "id": "default-applications", + "type": "file", + "path": "/etc/xdg/mimeapps.list", + "mode": "0644", + "template": "files/mimeapps.list" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/xdg-tools", + "binary": "xdg-tools", + "loads": [ + "xdg-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/xorg/module.json.captured b/go/testdata/beside/mesh-catalog/modules/xorg/module.json.captured new file mode 100644 index 0000000..7b0b6b1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/xorg/module.json.captured @@ -0,0 +1,134 @@ +{ + "module": "xorg", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager", + "seat" + ], + "provides": [ + { + "name": "x11-display", + "reach": "machine" + } + ], + "claims": [ + { + "name": "node-display-server", + "scope": "node", + "serves": [ + "displays", + "layout" + ] + } + ], + "tools": [ + "xorg_set_mode", + "xorg_primary", + "xorg_dpi", + "xorg_input_devices", + "xorg_input_set", + "xorg_keyboard", + "xorg_screenshot", + "xorg_windows", + "xorg_x_log" + ], + "resources": [ + { + "id": "package-xorg-server", + "type": "package", + "package": "xorg-server" + }, + { + "id": "package-xorg-xinit", + "type": "package", + "package": "xorg-xinit" + }, + { + "id": "package-xorg-xrandr", + "type": "package", + "package": "xorg-xrandr" + }, + { + "id": "package-xorg-xset", + "type": "package", + "package": "xorg-xset" + }, + { + "id": "package-xorg-xrdb", + "type": "package", + "package": "xorg-xrdb" + }, + { + "id": "package-xorg-xinput", + "type": "package", + "package": "xorg-xinput" + }, + { + "id": "package-xorg-setxkbmap", + "type": "package", + "package": "xorg-setxkbmap" + }, + { + "id": "package-xorg-xwd", + "type": "package", + "package": "xorg-xwd" + }, + { + "id": "package-xorg-xwininfo", + "type": "package", + "package": "xorg-xwininfo" + }, + { + "id": "package-autorandr", + "type": "package", + "package": "autorandr" + }, + { + "id": "config", + "type": "directory", + "path": "${machine:account-home}/.config/xorg", + "owner": "${machine:account}", + "mode": "0755", + "unknown": "move-out" + }, + { + "id": "xresources", + "type": "file", + "path": "${machine:account-home}/.config/xorg/xresources", + "owner": "${machine:account}", + "mode": "0644", + "template": "files/xresources" + }, + { + "id": "xinitrc", + "type": "file", + "path": "${machine:account-home}/.xinitrc", + "owner": "${machine:account}", + "mode": "0755", + "into": "block", + "at": "start", + "template": "files/xinitrc" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/xorg", + "binary": "xorg", + "loads": [ + "xorg" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/xterm/module.json.captured b/go/testdata/beside/mesh-catalog/modules/xterm/module.json.captured new file mode 100644 index 0000000..e6a565a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/xterm/module.json.captured @@ -0,0 +1,67 @@ +{ + "module": "xterm", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "requires": [ + "x11-display" + ], + "claims": [ + { + "name": "node-terminal-emulator", + "scope": "node", + "serves": [ + "open" + ] + } + ], + "tools": [ + "xterm_font", + "xterm_colours" + ], + "items": [ + { + "collection": "environment", + "name": "terminal", + "attributes": { + "kind": "variable", + "name": "TERMINAL" + }, + "template": "files/env-terminal" + }, + { + "collection": "shell:xresources:normal", + "name": "xresources-normal", + "template": "files/xresources-normal" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "xterm" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/xterm-tools", + "binary": "xterm-tools", + "loads": [ + "xterm-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/zfs/module.json.captured b/go/testdata/beside/mesh-catalog/modules/zfs/module.json.captured new file mode 100644 index 0000000..c109320 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/zfs/module.json.captured @@ -0,0 +1,235 @@ +{ + "module": "zfs", + "version": "1", + "capabilities": [ + "service-manager" + ], + "emits": [ + "snapshot.taken", + "scrub.requested" + ], + "config": { + "scrub-pool": { + "type": "string", + "description": "The name of the ZFS pool the module scrubs; the operator's, with no default.", + "trusted": true, + "required": true + }, + "scrub-cadence": { + "type": "string", + "description": "How often the pool named by scrub-pool is scrubbed: weekly, or monthly on the first scrub-day of each month. Weekly is what the distribution's timer did on the home server before the module, so taking it over moves nothing until the operator chooses.", + "enum": [ + "weekly", + "monthly" + ], + "trusted": true, + "default": "weekly" + }, + "scrub-day": { + "type": "string", + "description": "The weekday the scrub starts, Mon to Sun; for a monthly scrub, the first such day of the month. Checked by the scheduled run, which refuses any other value and runs nothing; Monday is when the distribution's weekly timer fired.", + "enum": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "trusted": true, + "default": "Mon" + }, + "scrub-time": { + "type": "string", + "description": "The time the scrub starts, HH:MM from 00:00 to 23:59 in the machine's time zone; the scheduled run checks it every quarter of an hour, so the scrub starts within 15 minutes of it. Midnight is when the distribution's weekly timer fired.", + "pattern": "([01][0-9]|2[0-3]):[0-5][0-9]", + "trusted": true, + "default": "00:00" + } + }, + "tools": [ + "zfs_datasets", + "zfs_snapshots", + "zfs_snapshot", + "zfs_pools", + "zfs_space", + "zfs_pool_layout", + "zfs_scrub", + "zfs_scrub_start", + "zfs_scrub_pause", + "zfs_scrub_stop", + "zfs_io", + "zfs_arc", + "zfs_events", + "zfs_properties", + "zfs_health" + ], + "replaces": { + "zfs_datasets": [ + "zfs list" + ], + "zfs_snapshots": [ + "zfs list -t snapshot" + ], + "zfs_snapshot": [ + "zfs snapshot" + ], + "zfs_pools": [ + "zpool list", + "zpool status -x" + ], + "zfs_space": [ + "zfs list -o space", + "zpool list -v" + ], + "zfs_pool_layout": [ + "zpool status", + "zpool status -v" + ], + "zfs_scrub": [ + "zpool history -i" + ], + "zfs_io": [ + "zpool iostat", + "zpool iostat -v" + ], + "zfs_arc": [ + "arcstat", + "arc_summary", + "cat /proc/spl/kstat/zfs/arcstats" + ], + "zfs_events": [ + "zpool events", + "zpool events -v" + ], + "zfs_properties": [ + "zfs get all", + "zfs get" + ], + "zfs_scrub_start": [ + "zpool scrub" + ], + "zfs_scrub_pause": [ + "zpool scrub -p" + ], + "zfs_scrub_stop": [ + "zpool scrub -s" + ] + }, + "resources": [ + { + "id": "watch", + "type": "process", + "name": "zfs-watch", + "artifact": "tools", + "run": [ + "./zfs-tools", + "watch" + ], + "user": "${machine:account}", + "health": { + "kind": "tool", + "tool": "zfs_health", + "interval": "60s", + "timeout": "20s", + "looks": 2, + "grace": "60s" + } + }, + { + "id": "state-dir", + "type": "directory", + "path": "/var/lib/zfs-tools", + "mode": "0755", + "owner": "${machine:account}", + "unknown": "program" + }, + { + "id": "cursor-dir", + "type": "directory", + "path": "/run/zfs-events", + "mode": "0700", + "unknown": "program" + }, + { + "id": "events", + "type": "process", + "name": "zfs-events", + "artifact": "tools", + "run": [ + "./zfs-tools", + "events" + ], + "health": { + "kind": "unit" + } + }, + { + "id": "config-dir", + "type": "directory", + "path": "/etc/zfs-tools", + "mode": "0755", + "unknown": "program" + }, + { + "id": "scrub-config", + "type": "file", + "path": "/etc/zfs-tools/scrub.conf", + "mode": "0644", + "template": "files/scrub.conf" + }, + { + "id": "scrub-state-dir", + "type": "directory", + "path": "/var/lib/zfs-scrub", + "mode": "0700", + "unknown": "program" + }, + { + "id": "scrub-run", + "type": "process", + "name": "zfs-mesh-scrub", + "artifact": "tools", + "run": [ + "./zfs-tools", + "scrub-run" + ], + "schedule": "*/15 * * * *" + }, + { + "id": "distribution-weekly", + "type": "service", + "unit": "zfs-scrub-weekly@${config:scrub-pool}.timer", + "state": "stopped", + "boot": "disabled" + }, + { + "id": "distribution-monthly", + "type": "service", + "unit": "zfs-scrub-monthly@${config:scrub-pool}.timer", + "state": "stopped", + "boot": "disabled" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/zfs-tools", + "binary": "zfs-tools", + "loads": [ + "zfs-tools" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/zsh-autosuggestions/module.json.captured b/go/testdata/beside/mesh-catalog/modules/zsh-autosuggestions/module.json.captured new file mode 100644 index 0000000..e883921 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/zsh-autosuggestions/module.json.captured @@ -0,0 +1,30 @@ +{ + "module": "zsh-autosuggestions", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "zsh-autosuggestions" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/zsh-syntax-highlighting/module.json.captured b/go/testdata/beside/mesh-catalog/modules/zsh-syntax-highlighting/module.json.captured new file mode 100644 index 0000000..b9c81a9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/zsh-syntax-highlighting/module.json.captured @@ -0,0 +1,30 @@ +{ + "module": "zsh-syntax-highlighting", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "zsh-syntax-highlighting" + } + ], + "items": [ + { + "collection": "shell:zsh:last", + "template": "files/zsh-last" + } + ], + "build": { + "artifacts": [ + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/zsh/module.json.captured b/go/testdata/beside/mesh-catalog/modules/zsh/module.json.captured new file mode 100644 index 0000000..da125af --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/zsh/module.json.captured @@ -0,0 +1,160 @@ +{ + "module": "zsh", + "version": "1", + "moved": true, + "capabilities": [ + "package-manager" + ], + "claims": [ + { + "name": "node-login-shell", + "scope": "node", + "serves": [ + "execute" + ] + } + ], + "tools": [ + "zsh_config" + ], + "config": { + "execute": { + "type": "string", + "description": "Whether this machine offers execute, which runs any command as the operator account and so can become root without a person: serve, or withhold on a machine that does not offer it (novox/hq ADR 0268), such as the control-node until a call to it needs a person's approval.", + "enum": [ + "serve", + "withhold" + ], + "trusted": true, + "default": "serve" + } + }, + "items": [ + { + "collection": "environment", + "name": "editor", + "attributes": { + "kind": "variable", + "name": "EDITOR" + }, + "template": "files/env-editor" + }, + { + "collection": "environment", + "name": "visual", + "attributes": { + "kind": "variable", + "name": "VISUAL" + }, + "template": "files/env-visual" + }, + { + "collection": "environment", + "name": "xdg-config-home", + "attributes": { + "kind": "variable", + "name": "XDG_CONFIG_HOME" + }, + "template": "files/env-xdg-config-home" + }, + { + "collection": "environment", + "name": "path-local-bin", + "attributes": { + "kind": "path", + "at": "start", + "order": "10" + }, + "template": "files/env-path-local-bin" + }, + { + "collection": "environment", + "name": "path-scripts", + "attributes": { + "kind": "path", + "at": "start", + "order": "20" + }, + "template": "files/env-path-scripts" + }, + { + "collection": "environment", + "name": "path-scripts-bin", + "attributes": { + "kind": "path", + "at": "start", + "order": "30" + }, + "template": "files/env-path-scripts-bin" + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "zsh" + }, + { + "id": "env", + "type": "file", + "path": "${machine:account-home}/.zshenv", + "owner": "${machine:account}", + "mode": "0644", + "into": "block", + "at": "start", + "template": "files/zshenv" + }, + { + "id": "rc", + "type": "file", + "path": "${machine:account-home}/.zshrc", + "owner": "${machine:account}", + "mode": "0644", + "into": "block", + "at": "start", + "template": "files/zshrc" + }, + { + "id": "login", + "type": "user", + "name": "${machine:account}", + "shell": "/usr/bin/zsh" + }, + { + "id": "state", + "type": "directory", + "place": ".", + "owner": "root", + "mode": "0755", + "unknown": "program" + }, + { + "id": "tools-conf", + "type": "file", + "path": "${dir:state}/tools.conf", + "owner": "root", + "mode": "0644", + "template": "files/tools.conf" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "env": { + "ZSH_TOOLS_CONF": "${dir:state}/tools.conf" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} From 013d6231ed49902571aaec752a5335fd52cd79ff Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 08:19:14 +0000 Subject: [PATCH 3/9] Capture the catalogue modules' files directories (adwaita to mailu) at mesh-catalog bb0fe93b for the moved catalogue tests (issue 1379) Part 3 of 4 of go/testdata/beside. --- .../modules/adwaita/files/cursor-index.theme | 5 + .../modules/adwaita/files/env-gtk-theme | 1 + .../modules/adwaita/files/env-gtk2-rc-files | 1 + .../adwaita/files/env-qt-qpa-platformtheme | 1 + .../modules/adwaita/files/env-qt-select | 1 + .../adwaita/files/env-qt-style-override | 1 + .../modules/adwaita/files/env-xcursor-size | 1 + .../modules/adwaita/files/env-xcursor-theme | 1 + .../modules/adwaita/files/gtk-settings.ini | 9 + .../modules/adwaita/files/portals.conf | 11 + .../modules/adwaita/files/qt6ct.conf | 29 ++ .../modules/adwaita/files/xinitrc-normal | 10 + .../modules/adwaita/files/xresources-normal | 3 + .../artifact-store-tools/files/store-names | 4 + .../files/bin/zephyrus-backlight | 33 ++ .../files/bin/zephyrus-display | 41 ++ .../files/bin/zephyrus-kbd-notify | 20 + .../files/bin/zephyrus-media | 27 ++ .../files/bin/zephyrus-notify | 12 + .../files/bin/zephyrus-session | 46 +++ .../files/bin/zephyrus-touchpad | 29 ++ .../modules/baserow/files/config.json | 4 + .../modules/baserow/files/server.env | 7 + .../modules/betterbird/files/accounts.json | 3 + .../modules/betterbird/files/betterbird | 10 + .../betterbird/files/configuration.json | 6 + .../files/eu.betterbird.Betterbird.desktop | 25 ++ .../modules/betterbird/files/prefs.json | 3 + .../modules/betterbird/files/tags.json | 3 + .../modules/ca-trust/files/anchor | 58 +++ .../ca-trust/files/mesh-ca-trust.service | 16 + .../modules/claude-code/files/agent-account | 1 + .../modules/claude-code/files/claude-agent | 13 + .../modules/claude-code/files/facts.json | 4 + .../modules/claude-code/files/settings.json | 10 + .../modules/claude-code/files/zsh-normal | 12 + .../claude-licence-manager/files/database.url | 1 + .../files/settings.json | 7 + .../clipmenu/files/bin/clipmenud-session | 41 ++ .../modules/clipmenu/files/bin/xsel | 20 + .../modules/clipmenu/files/env-cm-histlength | 1 + .../modules/clipmenu/files/env-cm-max-clips | 1 + .../modules/clipmenu/files/env-cm-selections | 1 + .../clipmenu/files/i3/50-clipmenu.conf | 5 + .../files/systemd/clipmenud-mesh.conf | 14 + .../modules/confluence/files/config.json | 1 + .../modules/desk-channel/files/settings.json | 3 + .../modules/dnsmasq/files/dnsmasq.conf | 87 +++++ .../modules/dnsmasq/files/mesh.conf | 14 + .../modules/docker/files/daemon.json | 1 + .../modules/docker/files/docker-prune.service | 15 + .../modules/docker/files/docker-prune.timer | 11 + .../modules/docker/files/log-rotation.conf | 5 + .../mesh-catalog/modules/dunst/files/dunstrc | 110 ++++++ .../modules/dunst/files/i3/50-dunst.conf | 4 + .../modules/fail2ban/files/fail2ban | 7 + .../files/iptables-allports-dualchain.conf | 40 ++ .../modules/fail2ban/files/jail.local | 48 +++ .../fail2ban/files/mesh-own-addresses.json | 3 + .../modules/fail2ban/files/own-addresses | 0 .../modules/fail2ban/files/recidive.conf | 9 + .../modules/fail2ban/files/sshd.conf | 8 + .../mesh-catalog/modules/feh/files/fehbg | 6 + .../modules/feh/files/i3/50-feh.conf | 3 + .../modules/feh/files/xinitrc-normal | 2 + .../modules/fonts/files/50-mesh-fonts.conf | 40 ++ .../modules/gitea/files/agent-teams.json | 1 + .../modules/gitea/files/config.json | 1 + .../modules/gitea/files/consumers.json | 1 + .../mesh-catalog/modules/gitea/files/origins | 1 + .../modules/gitea/files/repositories.json | 1 + .../modules/gitea/files/server.env | 7 + .../modules/gitlab/files/config.json | 1 + .../modules/gnome-keyring/files/pam/login | 4 + .../modules/gnome-keyring/files/pam/passwd | 3 + .../modules/gnome-keyring/files/xinitrc-first | 5 + .../modules/grafana/files/admin.secret | 1 + .../modules/grafana/files/config.json | 4 + .../modules/grafana/files/influxdb-api | 1 + .../modules/grafana/files/mesh-influxdb.yaml | 19 + .../modules/grafana/files/oidc-client.secret | 1 + .../modules/grafana/files/oidc.env | 13 + .../modules/hello-web/files/index.html | 1 + .../modules/home-assistant/files/config.json | 1 + .../home-assistant/files/provisions.env | 4 + .../modules/hostname/files/hostname | 1 + .../mesh-catalog/modules/hostname/files/hosts | 7 + .../mesh-catalog/modules/i3/files/config | 201 ++++++++++ .../modules/i3/files/env-xdg-current-desktop | 1 + .../modules/i3/files/env-xdg-session-desktop | 1 + .../mesh-catalog/modules/i3/files/wms/i3 | 9 + .../modules/i3/files/xinitrc-last | 2 + .../i3status-rust/files/bin/i3bar-watchdog | 64 +++ .../i3status-rust/files/bin/i3status-updates | 63 +++ .../i3status-rust/files/bin/i3status-value | 13 + .../i3status-rust/files/bottom-bar.toml | 109 ++++++ .../files/i3/60-i3status-rust.conf | 34 ++ .../files/icons/custom-icons.toml | 135 +++++++ .../modules/i3status-rust/files/top-bar.toml | 37 ++ .../i3status-rust/files/xinitrc-normal | 3 + .../modules/icecast/files/config.json | 1 + .../modules/icecast/files/icecast.xml | 56 +++ .../modules/influxdb/files/config.json | 1 + .../modules/influxdb/files/server.env | 6 + .../modules/invoicing/files/api.env | 9 + .../modules/jira/files/config.json | 1 + .../modules/kernel/files/huge-pages.sh | 84 ++++ .../files/mesh-kernel-huge-pages.service | 25 ++ .../modules/keycloak/files/admin.env | 2 + .../modules/keycloak/files/config.json | 1 + .../modules/keycloak/files/consumers.json | 1 + .../modules/keycloak/files/database.env | 3 + .../modules/keycloak/files/hostname.env | 1 + .../mesh-catalog/modules/lab/files/lab.env | 3 + .../modules/lemurs/files/config.toml | 369 ++++++++++++++++++ .../mesh-catalog/modules/lemurs/files/xserver | 8 + .../modules/letta/files/config.json | 3 + .../mesh-catalog/modules/letta/files/pgpass | 1 + .../modules/letta/files/server.env | 6 + .../mesh-catalog/modules/letta/files/start.sh | 23 ++ .../local-model-consumer/files/openai.env | 3 + .../modules/localization/files/locale.conf | 3 + .../modules/localization/files/vconsole.conf | 3 + .../modules/logrotate/files/logrotate.conf | 32 ++ .../modules/mailu/files/admin.env | 2 + .../modules/mailu/files/config.json | 1 + .../modules/mailu/files/database.env | 5 + .../modules/mailu/files/mailu.env | 78 ++++ .../modules/mailu/files/secret.env | 1 + 129 files changed, 2451 insertions(+) create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/cursor-index.theme create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk-theme create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk2-rc-files create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-qpa-platformtheme create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-select create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-style-override create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-size create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-theme create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/gtk-settings.ini create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/portals.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/qt6ct.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/xinitrc-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/adwaita/files/xresources-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/artifact-store-tools/files/store-names create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad create mode 100644 go/testdata/beside/mesh-catalog/modules/baserow/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/baserow/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/accounts.json create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/configuration.json create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/eu.betterbird.Betterbird.desktop create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/prefs.json create mode 100644 go/testdata/beside/mesh-catalog/modules/betterbird/files/tags.json create mode 100644 go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor create mode 100644 go/testdata/beside/mesh-catalog/modules/ca-trust/files/mesh-ca-trust.service create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/files/agent-account create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/files/facts.json create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-code/files/zsh-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-licence-manager/files/database.url create mode 100644 go/testdata/beside/mesh-catalog/modules/claude-licence-manager/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/clipmenud-session create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-histlength create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-max-clips create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-selections create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/i3/50-clipmenu.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/clipmenu/files/systemd/clipmenud-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/confluence/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/desk-channel/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/dnsmasq/files/dnsmasq.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/dnsmasq/files/mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/docker/files/daemon.json create mode 100644 go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.service create mode 100644 go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.timer create mode 100644 go/testdata/beside/mesh-catalog/modules/docker/files/log-rotation.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/dunst/files/dunstrc create mode 100644 go/testdata/beside/mesh-catalog/modules/dunst/files/i3/50-dunst.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/fail2ban create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/iptables-allports-dualchain.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/jail.local create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/mesh-own-addresses.json create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/own-addresses create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/recidive.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/fail2ban/files/sshd.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/feh/files/fehbg create mode 100644 go/testdata/beside/mesh-catalog/modules/feh/files/i3/50-feh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/feh/files/xinitrc-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/fonts/files/50-mesh-fonts.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/agent-teams.json create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/consumers.json create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/origins create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/repositories.json create mode 100644 go/testdata/beside/mesh-catalog/modules/gitea/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/gitlab/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/login create mode 100644 go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/passwd create mode 100644 go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/xinitrc-first create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/admin.secret create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/influxdb-api create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/mesh-influxdb.yaml create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/oidc-client.secret create mode 100644 go/testdata/beside/mesh-catalog/modules/grafana/files/oidc.env create mode 100644 go/testdata/beside/mesh-catalog/modules/hello-web/files/index.html create mode 100644 go/testdata/beside/mesh-catalog/modules/home-assistant/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/home-assistant/files/provisions.env create mode 100644 go/testdata/beside/mesh-catalog/modules/hostname/files/hostname create mode 100644 go/testdata/beside/mesh-catalog/modules/hostname/files/hosts create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/files/config create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-current-desktop create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-session-desktop create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 create mode 100644 go/testdata/beside/mesh-catalog/modules/i3/files/xinitrc-last create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bottom-bar.toml create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/i3/60-i3status-rust.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/icons/custom-icons.toml create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/top-bar.toml create mode 100644 go/testdata/beside/mesh-catalog/modules/i3status-rust/files/xinitrc-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/icecast/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/icecast/files/icecast.xml create mode 100644 go/testdata/beside/mesh-catalog/modules/influxdb/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/influxdb/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/invoicing/files/api.env create mode 100644 go/testdata/beside/mesh-catalog/modules/jira/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/kernel/files/huge-pages.sh create mode 100644 go/testdata/beside/mesh-catalog/modules/kernel/files/mesh-kernel-huge-pages.service create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/files/admin.env create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/files/consumers.json create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/files/database.env create mode 100644 go/testdata/beside/mesh-catalog/modules/keycloak/files/hostname.env create mode 100644 go/testdata/beside/mesh-catalog/modules/lab/files/lab.env create mode 100644 go/testdata/beside/mesh-catalog/modules/lemurs/files/config.toml create mode 100644 go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver create mode 100644 go/testdata/beside/mesh-catalog/modules/letta/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/letta/files/pgpass create mode 100644 go/testdata/beside/mesh-catalog/modules/letta/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/letta/files/start.sh create mode 100644 go/testdata/beside/mesh-catalog/modules/local-model-consumer/files/openai.env create mode 100644 go/testdata/beside/mesh-catalog/modules/localization/files/locale.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/localization/files/vconsole.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/logrotate/files/logrotate.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/files/admin.env create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/files/database.env create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/files/mailu.env create mode 100644 go/testdata/beside/mesh-catalog/modules/mailu/files/secret.env diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/cursor-index.theme b/go/testdata/beside/mesh-catalog/modules/adwaita/files/cursor-index.theme new file mode 100644 index 0000000..d848240 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/cursor-index.theme @@ -0,0 +1,5 @@ +# Written by the mesh (module adwaita, novox/hq ADR 0208): the default cursor theme, for programs that +# read neither XCURSOR_THEME nor the X resources. +[Icon Theme] +Name=Default +Inherits=Adwaita diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk-theme b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk-theme new file mode 100644 index 0000000..b98c809 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk-theme @@ -0,0 +1 @@ +Adwaita:dark diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk2-rc-files b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk2-rc-files new file mode 100644 index 0000000..8a782fb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-gtk2-rc-files @@ -0,0 +1 @@ +/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-qpa-platformtheme b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-qpa-platformtheme new file mode 100644 index 0000000..349e5d6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-qpa-platformtheme @@ -0,0 +1 @@ +qt6ct diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-select b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-select new file mode 100644 index 0000000..1e8b314 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-select @@ -0,0 +1 @@ +6 diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-style-override b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-style-override new file mode 100644 index 0000000..a0da7db --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-qt-style-override @@ -0,0 +1 @@ +Fusion diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-size b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-size new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-size @@ -0,0 +1 @@ +24 diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-theme b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-theme new file mode 100644 index 0000000..cb9676b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/env-xcursor-theme @@ -0,0 +1 @@ +Adwaita diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/gtk-settings.ini b/go/testdata/beside/mesh-catalog/modules/adwaita/files/gtk-settings.ini new file mode 100644 index 0000000..a535ba4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/gtk-settings.ini @@ -0,0 +1,9 @@ +# Written by the mesh (module adwaita, novox/hq ADR 0208), for GTK 3 and GTK 4 alike. Replaced at +# every push; adwaita_appearance switches dark and light for the running session. +[Settings] +gtk-theme-name=Adwaita +gtk-icon-theme-name=Adwaita +gtk-cursor-theme-name=Adwaita +gtk-cursor-theme-size=24 +gtk-font-name=Inter 11 +gtk-application-prefer-dark-theme=1 diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/portals.conf b/go/testdata/beside/mesh-catalog/modules/adwaita/files/portals.conf new file mode 100644 index 0000000..d2e813f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/portals.conf @@ -0,0 +1,11 @@ +# Written by the mesh (module adwaita, novox/hq ADR 0208). Replaced at every push. +# +# Which portal backend answers each interface. i3 is not a desktop xdg-desktop-portal knows, so with +# no preference it uses whichever backend happens to be installed: fine while gtk is the only one, +# wrong the day another arrives as somebody else's dependency. Named instead. gtk also serves +# org.freedesktop.appearance (dark or light) from GSettings, which the session's start sets. +[preferred] +default=gtk +# Secrets for sandboxed programs come from the keyring's backend. Without this line no backend answers +# the interface: gtk does not implement it, and gnome-keyring's names only GNOME as its desktop. +org.freedesktop.impl.portal.Secret=gnome-keyring diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/qt6ct.conf b/go/testdata/beside/mesh-catalog/modules/adwaita/files/qt6ct.conf new file mode 100644 index 0000000..7f396d2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/qt6ct.conf @@ -0,0 +1,29 @@ +[Appearance] +color_scheme_path=/usr/share/qt6ct/colors/darker.conf +custom_palette=true +icon_theme=Adwaita +standard_dialogs=default +style=Fusion + +[Fonts] +fixed="JetBrainsMono Nerd Font,11,-1,5,50,0,0,0,0,0" +general="Inter,11,-1,5,50,0,0,0,0,0" + +[Interface] +activate_item_on_single_click=1 +buttonbox_layout=0 +cursor_flash_time=1000 +dialog_buttons_have_icons=1 +double_click_interval=400 +gui_effects=@Invalid() +keyboard_scheme=2 +menus_have_icons=true +show_shortcuts_in_context_menus=true +stylesheets=@Invalid() +toolbutton_style=4 +underline_shortcut=1 +wheel_scroll_lines=3 + +[Troubleshooting] +force_raster_widgets=1 +ignored_applications=@Invalid() diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/xinitrc-normal b/go/testdata/beside/mesh-catalog/modules/adwaita/files/xinitrc-normal new file mode 100644 index 0000000..864b3cf --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/xinitrc-normal @@ -0,0 +1,10 @@ +# The appearance (module adwaita): GSettings is where the portal reads dark or light, and the portal is +# the only way it reaches Electron, Chromium, Firefox and flatpaks. Set at every session start to the +# module's default; adwaita_appearance switches it for a session. +gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true +gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita' || true +gsettings set org.gnome.desktop.interface icon-theme 'Adwaita' || true +gsettings set org.gnome.desktop.interface cursor-theme 'Adwaita' || true +gsettings set org.gnome.desktop.interface cursor-size 24 || true +gsettings set org.gnome.desktop.interface font-name 'Inter 11' || true +gsettings set org.gnome.desktop.interface monospace-font-name 'JetBrainsMono Nerd Font 11' || true diff --git a/go/testdata/beside/mesh-catalog/modules/adwaita/files/xresources-normal b/go/testdata/beside/mesh-catalog/modules/adwaita/files/xresources-normal new file mode 100644 index 0000000..02e7ec3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/adwaita/files/xresources-normal @@ -0,0 +1,3 @@ +! adwaita: the cursor, for X programs that take it from the resources. +Xcursor.theme: Adwaita +Xcursor.size: 24 diff --git a/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/files/store-names b/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/files/store-names new file mode 100644 index 0000000..29cab31 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/artifact-store-tools/files/store-names @@ -0,0 +1,4 @@ +# The names the artifact store had before, from the setting older-store-names, written by the mesh. +# artifact_store_references reads every definition under these too (novox/hq issue 323). +# Replaced on every push; change the setting, never this file. +older=${config:older-store-names} diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight new file mode 100644 index 0000000..1a3ec31 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight @@ -0,0 +1,33 @@ +#!/bin/bash +# zephyrus-backlight + | - | PERCENT — step or set the internal panel's backlight, never below 1 %. +# Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# The panel is the backlight beneath the eDP connector, not a name: in hybrid mode this model also +# registers the discrete GPU's backlight (nvidia_0), which moves nothing. Writable by the video group +# through the module's udev rule, so the vendor-key trigger needs no root. +set -u +STEP=5 +dev="" +for d in /sys/class/backlight/*; do + [ -e "$d" ] || continue + case "$(readlink -f "$d")" in *-eDP-*) dev=$d; break ;; esac +done +if [ -z "$dev" ]; then + for d in /sys/class/backlight/*; do [ -e "$d" ] && { dev=$d; break; }; done +fi +[ -n "$dev" ] || { echo "zephyrus-backlight: no backlight" >&2; exit 1; } +cur=$(cat "$dev/brightness") +max=$(cat "$dev/max_brightness") +pct=$(( cur * 100 / max )) +case "${1:-}" in + +|up|Up) pct=$(( pct + STEP )) ;; + -|down|Down) pct=$(( pct - STEP )) ;; + ''|*[!0-9]*) echo "usage: zephyrus-backlight + | - | PERCENT" >&2; exit 2 ;; + *) pct=$1 ;; +esac +(( pct < 1 )) && pct=1 +(( pct > 100 )) && pct=100 +new=$(( max * pct / 100 )) +(( new < 1 )) && new=1 +printf '%s' "$new" >"$dev/brightness" || exit 1 +exec "$(dirname "$0")/zephyrus-notify" 5555 "Brightness: ${pct}%" diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display new file mode 100644 index 0000000..0b7d21b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display @@ -0,0 +1,41 @@ +#!/bin/bash +# zephyrus-display primary | order — the laptop's internal panel among the outputs. +# Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# primary makes the internal panel X's primary output, where the bars' tray goes. +# order the display key (Fn+F9, which the firmware sends as Super+P): odd workspaces to the +# internal panel, even ones to the first external output, or all to the panel when it +# is alone. The predecessor's orden-workspaces. +# +# The panel is found, not named: the connected output whose name starts with eDP. The predecessor +# wrote eDP-1, which is what this model calls it today and not a promise. +set -u +here=$(dirname "$0") +panel=$(xrandr --query 2>/dev/null | awk '$2 == "connected" && $1 ~ /^eDP/ { print $1; exit }') +[ -n "$panel" ] || { echo "zephyrus-display: no internal panel connected" >&2; exit 1; } +case "${1:-}" in +primary) + exec xrandr --output "$panel" --primary + ;; +order) + external=$(xrandr --listmonitors 2>/dev/null | awk -v p="$panel" 'NR > 1 && $NF != p { print $NF; exit }') + [ -n "$external" ] || external=$panel + # i3's workspace list, one object per workspace; num and output read from each. No jq: the + # fields are flat strings and numbers, and rect, the only nested value, holds neither name. + i3-msg -t get_workspaces 2>/dev/null | sed 's/},{"id"/}\n{"id"/g' | + while read -r ws; do + num=$(printf '%s' "$ws" | grep -o '"num":-\?[0-9]*' | cut -d: -f2) + out=$(printf '%s' "$ws" | grep -o '"output":"[^"]*"' | cut -d'"' -f4) + [ -n "$num" ] && [ "$num" -ge 0 ] || continue + if [ $((num % 2)) -eq 0 ]; then dest=$external; else dest=$panel; fi + [ "$out" = "$dest" ] && continue + i3-msg "workspace number $num; move workspace to output $dest" >/dev/null + done + if [ "$external" = "$panel" ]; then + "$here/zephyrus-notify" 7780 "Workspaces: all on the panel" + else + "$here/zephyrus-notify" 7780 "Workspaces: odd on the panel, even on $external" + fi + ;; +*) echo "usage: zephyrus-display primary | order" >&2; exit 2 ;; +esac diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify new file mode 100644 index 0000000..aee878b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify @@ -0,0 +1,20 @@ +#!/bin/bash +# zephyrus-kbd-notify — shows the keyboard backlight's level when it changes. The level itself is set +# by the firmware and asusd (Fn+F2/F3), which tell UPower; this only listens and notifies. Started once +# per session from the module's i3 fragment. Shipped by the mesh's asus-zephyrus-g14 module. +# +# One instance per session: i3 runs its `exec` lines again on an in-place restart, and the +# predecessor's listener ran twice after one, showing every change twice. +set -u +here=$(dirname "$0") +lock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/zephyrus-kbd-notify.lock" +exec 9>"$lock" +flock -n 9 || exit 0 +levels=(Off Low Med High) +dbus-monitor --system "type='signal',interface='org.freedesktop.UPower.KbdBacklight',member='BrightnessChanged'" 2>/dev/null | + while read -r line; do + if [[ $line =~ int32\ ([0-9]+) ]]; then + v=${BASH_REMATCH[1]} + "$here/zephyrus-notify" 5556 "Keyboard: ${levels[$v]:-$v}" + fi + done diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media new file mode 100644 index 0000000..8b77d79 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media @@ -0,0 +1,27 @@ +#!/bin/bash +# zephyrus-media play-pause | next | previous — the media keys, through MPRIS (playerctl), with a short +# notification of what happened. Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# From the predecessor's media-control, kept: the lock against a double fire (the vendor keys can +# repeat), and the notification. Dropped: its fallback to a media server's local API, which needed a +# token from a file of secrets (novox/hq research 027 question 2). A player that speaks MPRIS is +# reached; one that does not says so. +set -u +here=$(dirname "$0") +action=${1:-play-pause} +case "$action" in play-pause | next | previous) ;; *) echo "usage: zephyrus-media play-pause | next | previous" >&2; exit 2 ;; esac +lock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/zephyrus-media.lock" +exec 9>"$lock" +flock -n 9 || exit 0 +if ! playerctl status >/dev/null 2>&1; then + "$here/zephyrus-notify" 7777 "Media: no player" + exit 0 +fi +playerctl "$action" 2>/dev/null +if [ "$action" = play-pause ]; then + sleep 0.2 + [ "$(playerctl status 2>/dev/null)" = Playing ] && label=Playing || label=Paused +else + label=${action^} +fi +"$here/zephyrus-notify" 7777 "Media: $label" diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify new file mode 100644 index 0000000..1e8273f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify @@ -0,0 +1,12 @@ +#!/bin/bash +# zephyrus-notify ID SUMMARY — a short desktop notification that replaces the previous one with the +# same ID, through the session's notification service on its bus. busctl is the service manager's +# own client, so nothing is installed for it. Shipped by the mesh's asus-zephyrus-g14 module. +set -u +id=${1:-0} +summary=${2:-} +uid=$(id -u) +DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" \ + busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications \ + org.freedesktop.Notifications Notify susssasa{sv}i \ + asus-zephyrus-g14 "$id" "" "$summary" "" 0 1 urgency y 0 1500 >/dev/null 2>&1 || true diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session new file mode 100644 index 0000000..0f21f91 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session @@ -0,0 +1,46 @@ +#!/bin/bash +# zephyrus-session COMMAND [ARG...] — run a command in the operator's graphical session from outside +# it: from a vendor-key trigger, which triggerhappy runs as the operator's account but with none of +# the session's environment. Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# What it replaces: the predecessor's `as-user`, which triggerhappy ran as root and which `su`-ed to +# a named person with a hard-coded user id and display, and sourced a file of secrets on the way. +# Here the account is whoever runs it, the bus is that account's, and the display is the one the +# account's own session uses. Nothing is sourced. +set -u +uid=$(id -u) +export XDG_RUNTIME_DIR="/run/user/${uid}" +export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" +home=$(getent passwd "$uid" | cut -d: -f6) +[ -n "$home" ] && export HOME="$home" +# The session's own window manager says best which display and authority the session uses: read +# them from its environment, as the desktop modules' session finder does. Then logind, then any +# process of this account that has a display. +from_environ() { + env=$(tr '\0' '\n' <"/proc/$1/environ" 2>/dev/null) || return 1 + d=$(printf '%s\n' "$env" | sed -n 's/^DISPLAY=//p' | head -n1) + [ -n "$d" ] || return 1 + export DISPLAY="$d" + a=$(printf '%s\n' "$env" | sed -n 's/^XAUTHORITY=//p' | head -n1) + [ -n "$a" ] && export XAUTHORITY="$a" + return 0 +} +if [ -z "${DISPLAY:-}" ]; then + for pid in $(pgrep -xu "$uid" i3 2>/dev/null); do + from_environ "$pid" && break + done +fi +if [ -z "${DISPLAY:-}" ]; then + for s in $(loginctl list-sessions --no-legend 2>/dev/null | awk -v u="$uid" '$2 == u { print $1 }'); do + d=$(loginctl show-session "$s" -p Display --value 2>/dev/null) + if [ -n "$d" ]; then export DISPLAY="$d"; break; fi + done +fi +if [ -z "${DISPLAY:-}" ]; then + for pid in $(pgrep -u "$uid" 2>/dev/null); do + from_environ "$pid" && break + done +fi +: "${XAUTHORITY:=${HOME}/.Xauthority}" +export XAUTHORITY +exec "$@" diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad new file mode 100644 index 0000000..637be36 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad @@ -0,0 +1,29 @@ +#!/bin/bash +# zephyrus-touchpad reset | toggle — apply the touchpad's settings again, or switch it on or off. +# Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# The settings themselves are an X input class (/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf), +# which X applies every time the device appears — after a resume too, which is what the predecessor's +# sleep hook existed for. This is the manual form, bound to the touchpad key. +set -u +here=$(dirname "$0") +name=$("$here/zephyrus-session" xinput list --name-only 2>/dev/null | grep -m1 -i 'touchpad') +[ -n "$name" ] || { echo "zephyrus-touchpad: no touchpad in this session" >&2; exit 1; } +x() { "$here/zephyrus-session" xinput "$@"; } +case "${1:-reset}" in + reset) + x set-prop "$name" "libinput Tapping Enabled" 1 + x set-prop "$name" "libinput Natural Scrolling Enabled" 1 + x set-prop "$name" "libinput Accel Speed" 0.15 + x enable "$name" + "$here/zephyrus-session" "$here/zephyrus-notify" 7779 "Touchpad: reset" + ;; + toggle) + if x list-props "$name" | grep -q 'Device Enabled ([0-9]*):[[:space:]]*1'; then + x disable "$name"; "$here/zephyrus-session" "$here/zephyrus-notify" 7779 "Touchpad: off" + else + x enable "$name"; "$here/zephyrus-session" "$here/zephyrus-notify" 7779 "Touchpad: on" + fi + ;; + *) echo "usage: zephyrus-touchpad reset | toggle" >&2; exit 2 ;; +esac diff --git a/go/testdata/beside/mesh-catalog/modules/baserow/files/config.json b/go/testdata/beside/mesh-catalog/modules/baserow/files/config.json new file mode 100644 index 0000000..955ac24 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/baserow/files/config.json @@ -0,0 +1,4 @@ +{ + "password": "${secret:admin}", + "host": "${bound:route:name}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/baserow/files/server.env b/go/testdata/beside/mesh-catalog/modules/baserow/files/server.env new file mode 100644 index 0000000..7b716b4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/baserow/files/server.env @@ -0,0 +1,7 @@ +DATABASE_HOST=${bound:postgres-database:at} +DATABASE_PORT=${bound:postgres-database:port} +DATABASE_NAME=${bound:postgres-database:as} +DATABASE_USER=${bound:postgres-database:as} +DATABASE_PASSWORD_FILE=/run/secrets/database +DISABLE_EMBEDDED_PSQL=true +BASEROW_PUBLIC_URL=https://${bound:route:name} diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/accounts.json b/go/testdata/beside/mesh-catalog/modules/betterbird/files/accounts.json new file mode 100644 index 0000000..e182cc3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/accounts.json @@ -0,0 +1,3 @@ +{ + "accounts": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird b/go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird new file mode 100644 index 0000000..3ce088a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird @@ -0,0 +1,10 @@ +#!/bin/sh +# Betterbird's start, written by the mesh (module betterbird, novox/hq ADR 0316). Replaced at every push. +# +# Before Betterbird starts, the module's tools place its user.js in the profile (the settings prefs, accounts, identities, calendars, +# address-books and tags) and write what Betterbird rewrites itself: the account lists and each declared account's filters. They do +# it only while no Betterbird of this account runs, because Betterbird rewrites its saved preferences when it quits; a +# second start while one runs only hands over to it. What the preparation could not do it says here, and +# betterbird_check says it too. Betterbird starts either way: its configuration never holds the mail back. +/usr/local/lib/mesh-betterbird/betterbird-tools prepare || echo "betterbird: the mesh's preparation did not complete; betterbird_check says why" >&2 +exec /opt/betterbird/betterbird "$@" diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/configuration.json b/go/testdata/beside/mesh-catalog/modules/betterbird/files/configuration.json new file mode 100644 index 0000000..7020446 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/configuration.json @@ -0,0 +1,6 @@ +{ + "identities": {}, + "calendars": {}, + "address-books": {}, + "filters": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/eu.betterbird.Betterbird.desktop b/go/testdata/beside/mesh-catalog/modules/betterbird/files/eu.betterbird.Betterbird.desktop new file mode 100644 index 0000000..e409fcf --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/eu.betterbird.Betterbird.desktop @@ -0,0 +1,25 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Betterbird +GenericName=Mail Client +Comment=Send and receive mail with Betterbird +Exec=/usr/local/bin/betterbird %u +Icon=/opt/betterbird/chrome/icons/default/default128.png +Terminal=false +Categories=Network;Email; +MimeType=message/rfc822;x-scheme-handler/mailto;application/x-xpinstall;application/x-extension-ics;text/calendar;text/vcard;text/x-vcard;x-scheme-handler/webcal;x-scheme-handler/webcals;x-scheme-handler/mid; +StartupNotify=true +StartupWMClass=eu.betterbird.Betterbird +Keywords=Email;E-mail;Calendar;Thunderbird; +Actions=ComposeMessage;OpenAddressBook; +# Written by the mesh (module betterbird, novox/hq ADR 0316 §3), in place of the community package's entry: +# it starts Betterbird through the module's start, and names the icon in the installation, with no link. + +[Desktop Action ComposeMessage] +Name=Write new message +Exec=/usr/local/bin/betterbird -compose + +[Desktop Action OpenAddressBook] +Name=Open address book +Exec=/usr/local/bin/betterbird -addressbook diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/prefs.json b/go/testdata/beside/mesh-catalog/modules/betterbird/files/prefs.json new file mode 100644 index 0000000..45fd973 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/prefs.json @@ -0,0 +1,3 @@ +{ + "prefs": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/tags.json b/go/testdata/beside/mesh-catalog/modules/betterbird/files/tags.json new file mode 100644 index 0000000..4772f09 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/betterbird/files/tags.json @@ -0,0 +1,3 @@ +{ + "tags": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor b/go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor new file mode 100644 index 0000000..72576d2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor @@ -0,0 +1,58 @@ +#!/bin/sh +# The mesh's internal certificate authority, trusted by this machine. +# +# Written by the mesh from the ca-trust module's manifest (novox/hq ADR 0147). +# Editing it here lasts until the next apply. +# +# There is no prior trust to verify the fetch against — this is the thing that +# establishes it — so it is made over the mesh's own private network, which is +# what authenticates it (novox/hq ADR 0098, the same reasoning that lets the +# route proxy fetch this root for itself). What comes back is checked here: a +# body that is not a certificate is refused now, rather than believed and then +# failed by whatever reads the trust store next. +set -eu + +ROOTS='https://${bound:internal-acme-ca:at}:${bound:internal-acme-ca:port}${bound:internal-acme-ca:roots}' +ANCHORS=/etc/ca-certificates/trust-source/anchors +ANCHOR="$ANCHORS/mesh-internal-ca.crt" + +# Arch's layout, said out loud rather than assumed: a machine that keeps its +# anchors elsewhere fails here, visibly, instead of writing a file nothing +# reads. That failure is the signal that this belongs in the host, where one +# operating system's difference lives (novox/hq ADR 0147, option 2). +[ -d "$ANCHORS" ] || { + echo "this machine keeps no trust anchors in $ANCHORS; ca-trust is written for that layout" >&2 + exit 1 +} + +case "${1:-}" in +install) + tmp=$(mktemp) + trap 'rm -f "$tmp"' EXIT + # The authority may still be starting, or this machine may have come up + # before it: two minutes of asking, then an honest failure. + n=0 + while [ "$n" -lt 60 ]; do + if curl --fail --silent --show-error --insecure --max-time 10 \ + --output "$tmp" "$ROOTS" && + grep -q 'BEGIN CERTIFICATE' "$tmp"; then + install -m 0644 "$tmp" "$ANCHOR" + update-ca-trust + exit 0 + fi + n=$((n + 1)) + sleep 2 + done + echo "the authority at $ROOTS did not serve a certificate within two minutes" >&2 + exit 1 + ;; +remove) + # What stopping the unit does, and therefore what being unassigned does. + rm -f "$ANCHOR" + update-ca-trust + ;; +*) + echo "usage: $(basename "$0") install|remove" >&2 + exit 2 + ;; +esac diff --git a/go/testdata/beside/mesh-catalog/modules/ca-trust/files/mesh-ca-trust.service b/go/testdata/beside/mesh-catalog/modules/ca-trust/files/mesh-ca-trust.service new file mode 100644 index 0000000..2411560 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ca-trust/files/mesh-ca-trust.service @@ -0,0 +1,16 @@ +[Unit] +Description=The mesh's internal certificate authority, trusted by this machine +# novox/hq ADR 0147. Starting this unit places the mesh's root among this +# machine's trust anchors; stopping it takes the root away again, which is what +# the host does when the module is no longer assigned here. +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=${dir:state}/anchor install +ExecStop=${dir:state}/anchor remove + +[Install] +WantedBy=multi-user.target diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/agent-account b/go/testdata/beside/mesh-catalog/modules/claude-code/files/agent-account new file mode 100644 index 0000000..b081a6c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/files/agent-account @@ -0,0 +1 @@ +${machine:agent-account} diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent b/go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent new file mode 100644 index 0000000..e3d330f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent @@ -0,0 +1,13 @@ +#!/bin/sh +# The mesh's (module claude-code, novox/hq ADR 0266): start the agent as the account agents run as on this +# machine. Written whole at every push: an edit here is overwritten. +agent='${machine:agent-account}' +if [ "$(id -un)" = "$agent" ]; then + exec claude "$@" +fi +# Another account, the operator's, becomes the agent's through its own sudo: the person is the authority. The +# operator's override of the guard travels only when it is set in this shell, as it would reach claude. +if [ -n "$MESH_GUARD_OVERRIDE" ]; then + exec sudo -iu "$agent" env MESH_GUARD_OVERRIDE="$MESH_GUARD_OVERRIDE" claude "$@" +fi +exec sudo -iu "$agent" claude "$@" diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/facts.json b/go/testdata/beside/mesh-catalog/modules/claude-code/files/facts.json new file mode 100644 index 0000000..7fd72bd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/files/facts.json @@ -0,0 +1,4 @@ +{ + "node": "${machine:name}", + "console": "http://127.0.0.1:${bound:mcp-endpoint:port}/mcp" +} diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/settings.json b/go/testdata/beside/mesh-catalog/modules/claude-code/files/settings.json new file mode 100644 index 0000000..8284e66 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/files/settings.json @@ -0,0 +1,10 @@ +{ + "role": "", + "mcp_servers": {}, + "managed_settings": {}, + "instructions": {}, + "output_styles": {}, + "skills": {}, + "commands": {}, + "agents": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/zsh-normal b/go/testdata/beside/mesh-catalog/modules/claude-code/files/zsh-normal new file mode 100644 index 0000000..b90b85f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/claude-code/files/zsh-normal @@ -0,0 +1,12 @@ +# The agent's session (module claude-code, novox/hq ADR 0266): where this machine names an account of the +# agents' own, claude in an interactive zsh is claude-agent, which starts the session as that account. Where +# agents run as the operator's account the file names that account, and nothing is added. claude-agent runs +# the real claude: exec, sudo and its sh see no alias. +if [[ -r /etc/claude-code/agent-account ]]; then + () { + local agent=$(/dev/null | grep -E '^(DISPLAY|XAUTHORITY)='); do + export "$pair" + done + [ -n "$DISPLAY" ] && break + set -- /tmp/.X11-unix/X* + if [ $# -eq 1 ] && [ -S "$1" ]; then + DISPLAY=":${1##*/X}" + export DISPLAY + else + sleep 1 + fi +done +if [ -z "$XAUTHORITY" ] && [ -r "$HOME/.Xauthority" ]; then + XAUTHORITY="$HOME/.Xauthority" + export XAUTHORITY +fi +# clipmenud allows one daemon per account: it takes an exclusive flock on its session_lock file and exits +# with status 2 when that is held. After a delivery without a new login, the session's older clipmenud (from +# its xinitrc) still holds it, and a start would run the unit round its restart loop. So this waits, quietly, +# until the lock is free: it looks at the same file clipmenud locks (the directory is clipmenud's own rule) +# and lets go at once. Exit status 1 is "held" for util-linux and BusyBox flock alike (no -E: BusyBox lacks it); any other outcome starts clipmenud, which says its own +# error. Another daemon winning the gap between look and start costs one restart, not a loop. +lock="${CM_DIR:-${XDG_RUNTIME_DIR-${TMPDIR-/tmp}}}/clipmenu.6.$USER/session_lock" +while [ -e "$lock" ]; do + flock -n "$lock" true + [ $? -eq 1 ] || break + sleep 2 +done +PATH="/usr/local/lib/mesh-clipmenu:$PATH" +export PATH +exec clipmenud diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel new file mode 100644 index 0000000..10f9bbd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel @@ -0,0 +1,20 @@ +#!/bin/sh +# xsel as clipmenud sees it, written by the mesh (module clipmenu). Replaced at every push. +# +# clipmenud records text, and reads a selection with `timeout 1 xsel -o`. A selection holding an +# image (a screenshot copied as image/png) is sent in pieces; one second is too short for megabytes, +# timeout kills xsel half way, and the program owning the image waits for ever for a reader that is +# gone. From then on nothing can ask the clipboard anything: pastes hang, and an Electron app that +# asks on its main thread freezes. So a read goes ahead only when the selection offers text. +case " $* " in +*" -o "* | *" --output "*) + selection=clipboard + case " $* " in + *" --primary "* | *" -p "*) selection=primary ;; + *" --secondary "* | *" -s "*) selection=secondary ;; + esac + targets=$(timeout 1 xclip -selection "$selection" -t TARGETS -o 2>/dev/null) || exit 1 + printf '%s\n' "$targets" | grep -qxE 'UTF8_STRING|STRING|TEXT|text/plain(;charset=utf-8)?' || exit 1 + ;; +esac +exec /usr/bin/xsel "$@" diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-histlength b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-histlength new file mode 100644 index 0000000..60d3b2f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-histlength @@ -0,0 +1 @@ +15 diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-max-clips b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-max-clips new file mode 100644 index 0000000..1b79f38 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-max-clips @@ -0,0 +1 @@ +500 diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-selections b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-selections new file mode 100644 index 0000000..613d81a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/env-cm-selections @@ -0,0 +1 @@ +clipboard diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/i3/50-clipmenu.conf b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/i3/50-clipmenu.conf new file mode 100644 index 0000000..41835e6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/i3/50-clipmenu.conf @@ -0,0 +1,5 @@ +# The clipboard's history key (module clipmenu, novox/hq ADR 0208). Owned by the mesh: replaced at +# every push. clipmenu shows the history through `dmenu`, the node's dmenu-compatible command, which +# the holder of node-launcher answers (rofi on the workstations); the chosen entry is put back on the +# clipboard. +bindsym $mod+period exec --no-startup-id clipmenu -p Clipboard diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/systemd/clipmenud-mesh.conf b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/systemd/clipmenud-mesh.conf new file mode 100644 index 0000000..799e22d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/systemd/clipmenud-mesh.conf @@ -0,0 +1,14 @@ +# clipmenud in the operator's account (module clipmenu, novox/hq ADR 0208). Owned by the mesh: replaced at every push. +# The package's unit starts clipmenud with no DISPLAY; the module's launcher finds the display and puts the +# text-only xsel first on the PATH. The settings repeat the module's environment, which the account's +# service manager does not read. +[Unit] +StartLimitIntervalSec=0 + +[Service] +RestartSec=2s +Environment=CM_SELECTIONS=clipboard +Environment=CM_MAX_CLIPS=500 +Environment=CM_HISTLENGTH=15 +ExecStart= +ExecStart=/usr/local/lib/mesh-clipmenu/clipmenud-session diff --git a/go/testdata/beside/mesh-catalog/modules/confluence/files/config.json b/go/testdata/beside/mesh-catalog/modules/confluence/files/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/confluence/files/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/desk-channel/files/settings.json b/go/testdata/beside/mesh-catalog/modules/desk-channel/files/settings.json new file mode 100644 index 0000000..1ef16fd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/desk-channel/files/settings.json @@ -0,0 +1,3 @@ +{ + "desktop-machines": [] +} diff --git a/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/dnsmasq.conf b/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/dnsmasq.conf new file mode 100644 index 0000000..10f3926 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/dnsmasq.conf @@ -0,0 +1,87 @@ +# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than +# patched, because this module owns the file and a patch would leave whatever +# was there before to be discovered later. +# +# This is one of the mesh's resolvers (novox/hq ADR 0194, 0223): it holds the +# `mesh-dns-resolver` seat, which more than one machine may hold, each answering +# the same names from the same roster. Every node and every container lists every +# holder and no public resolver, so whichever answers first gives the one answer. +# It holds the mesh's own names and nothing else (ADR 0191) — the roster is the +# mesh's, rendered into each holder; no other copy lives on any machine. + +# What the mesh computed: one wildcard per machine — its name and everything +# under it — and the mesh's own suffix as a local domain, so a name under it is +# answered here or not at all and is never asked upstream. Rewritten whenever a +# machine joins or leaves, which is why the service below restarts on it: a +# reload makes dnsmasq re-read hosts files, not its configuration, and a +# wildcard is configuration. +conf-file=/etc/mesh-resolver/nodes.conf + +# Every zone a module answers itself (ADR 0199): one forwarding line per zone, to +# the private address of the machine its module runs on and the port its +# answerer is published on. Nothing in a zone is answered here; what names a zone +# holds is the module's. Rewritten whenever a zone is declared or moves, and the +# service restarts on it for the same reason as above. +conf-file=/etc/mesh-resolver/zones.conf + +# Where it answers: this machine's private address, so every node — and every +# container on every node, which copies its machine's resolvers — can ask; and +# loopback, for this machine's own use. Never a LAN address: a device that is +# not a member cannot reach what the mesh's names point at, and a LAN's resolver +# is its router's (ADR 0194). +# +# Named as an ADDRESS and not as the interface that carries it, on purpose. A +# container's query is addressed to this address but arrives on the runtime's +# bridge, and dnsmasq checks every query against what it was told to answer on: +# an `interface=` line admits queries by the interface they arrive on, a +# `listen-address=` line by the address they are sent to. With `interface=mesh0` +# alone, a query from a container was received and dropped without a word +# (novox/hq 04-ISSUES/110). The address admits it whatever bridge it comes in on. +# +# bind-interfaces, and never bind-dynamic (novox/hq issue 348). bind-dynamic +# re-reads the machine's addresses on every change of any link and closes each +# listener whose interface that reading did not find; a reading made while the +# container runtime deletes a bridge's virtual links fails ("index gone"), and +# then every listener is closed until a later change reads cleanly. On +# 2026-10-09 the private address was left unbound for twenty minutes, the daemon +# running and silent. bind-interfaces binds each address once, at start, and +# never closes it. It cannot bind an address that is not there yet, so the unit +# waits for it (the drop-in below); a new private address restarts the unit, as +# this file changing always did. +bind-interfaces +listen-address=${machine:address} +listen-address=127.0.0.1 + +# **It must never read resolv.conf to find out where to forward.** This +# machine's resolv.conf names this resolver — so a resolver that read it for +# upstreams would find itself, and every query it could not answer locally would +# loop until its receive queue filled. That is not theoretical: it filled with +# 15KB of queries and every lookup on the machine hung. no-resolv makes that loop +# impossible: the upstreams are the two lines below, and nothing on the machine +# can redirect them. The mesh's own names never reach them: the local= line in +# the file above stops them here, answered or refused. +no-resolv +server=1.1.1.1 +server=8.8.8.8 + +# Both upstreams validate DNSSEC and say so with the Authenticated Data bit, +# and this passes that bit down rather than dropping it, which dnsmasq does +# unless told. It does not validate itself: an answer's trust is the upstream's +# and the path to it, which is what a forwarding resolver's trust always was. +# Without it a program that refuses to run behind a resolver that does not +# validate — a mail system's admin does exactly that check at start — cannot +# use this resolver (novox/hq 04-ISSUES/171). +proxy-dnssec + +# A name without a dot is never forwarded, and reverse lookups of private ranges +# are answered here rather than asking the world who 10.x is. +domain-needed +bogus-priv + +# **No hosts file, and no operator's files.** This machine's /etc/hosts is its +# own — the hosts module's block and the operator's lines (ADR 0199) — and the +# mesh's resolver answers every node, so a line written for one machine's own +# programs must not become an answer for all of them. Every per-node resolver +# went wrong exactly here: a hosts file read once at start, an operator's old +# line beside the mesh's, a drop-in read from a directory nobody owned. +no-hosts diff --git a/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/mesh.conf b/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/mesh.conf new file mode 100644 index 0000000..96d8d7d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dnsmasq/files/mesh.conf @@ -0,0 +1,14 @@ +# Written by the mesh (module dnsmasq, novox/hq issue 348). Replaced on every push. +# +# The resolver binds its addresses once, at start (bind-interfaces), so it starts only once this +# machine's private address is there: it waits up to 80 seconds for it, and a start that found it +# missing is tried again every ten seconds rather than left failed. +[Unit] +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=0 + +[Service] +ExecStartPre=/usr/bin/sh -c 'for i in $(seq 1 80); do ip -4 -o addr show to ${machine:address} | grep -q . && exit 0; sleep 1; done; echo "${machine:address} is not on this machine" >&2; exit 1' +Restart=on-failure +RestartSec=10s diff --git a/go/testdata/beside/mesh-catalog/modules/docker/files/daemon.json b/go/testdata/beside/mesh-catalog/modules/docker/files/daemon.json new file mode 100644 index 0000000..83750f4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker/files/daemon.json @@ -0,0 +1 @@ +{"live-restore": true, "insecure-registries": ["${seat:mesh-artifact-store:reach}"], "log-driver": "json-file", "log-opts": {"max-size": "${config:log-max-size}", "max-file": "${config:log-max-file}"}} diff --git a/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.service b/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.service new file mode 100644 index 0000000..b9b92c1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.service @@ -0,0 +1,15 @@ +# Generated by the mesh. Do not edit — module docker writes this file and replaces it at every push. +[Unit] +Description=Prune dangling images and unused build cache (the mesh's docker module) +# Never volumes, never a container, never an image a container uses: dangling +# images and build cache nothing refers to, unused for a week. What a person +# prunes beyond that is docker_prune's, by hand. +After=docker.service +ConditionPathExists=/run/docker.sock + +[Service] +Type=oneshot +Nice=19 +IOSchedulingClass=idle +ExecStart=/usr/bin/docker image prune --force --filter until=168h +ExecStart=/usr/bin/docker builder prune --force --filter until=168h diff --git a/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.timer b/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.timer new file mode 100644 index 0000000..82befd1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker/files/docker-prune.timer @@ -0,0 +1,11 @@ +# Generated by the mesh. Do not edit — module docker writes this file and replaces it at every push. +[Unit] +Description=Weekly prune of dangling images and unused build cache (the mesh's docker module) + +[Timer] +OnCalendar=weekly +RandomizedDelaySec=1h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/go/testdata/beside/mesh-catalog/modules/docker/files/log-rotation.conf b/go/testdata/beside/mesh-catalog/modules/docker/files/log-rotation.conf new file mode 100644 index 0000000..3919036 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/docker/files/log-rotation.conf @@ -0,0 +1,5 @@ +# The log rotation daemon.json takes, written by the mesh from this machine's settings log-max-size and +# log-max-file, and judged by the step log-rotation-check before daemon.json is written (novox/hq issue 452). +# Replaced on every push; change the settings, never this file. +size=${config:log-max-size} +files=${config:log-max-file} diff --git a/go/testdata/beside/mesh-catalog/modules/dunst/files/dunstrc b/go/testdata/beside/mesh-catalog/modules/dunst/files/dunstrc new file mode 100644 index 0000000..afba676 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dunst/files/dunstrc @@ -0,0 +1,110 @@ +# dunst, the notifier (module dunst, novox/hq ADR 0208). Owned by the mesh: this file is replaced +# at every push. Adopted from the laptop's file of 2026-10-04 (the two workstations differed in +# position, transparency and corner radius; the laptop's square, opaque, top-right one matches the +# rest of the desktop). Only what differs from dunst's defaults, and what the desktop relies on. +# +# Other modules' rules go in ~/.config/dunst/dunstrc.d/*.conf, which dunst reads after this file, +# so a drop-in outranks it. dunst is started by D-Bus on the first notification: nothing starts it. +# When the mesh rewrites this file (a font-size or width setting changed), it tells a running dunst +# to read it again (its unit's reload, dunstctl reload); what is on screen stays. + +[global] + monitor = 0 + follow = none + + # Geometry + width = ${config:width} + height = (0, 300) + origin = top-right + offset = (10, 50) + notification_limit = 20 + + progress_bar = true + progress_bar_height = 10 + progress_bar_frame_width = 1 + progress_bar_min_width = 150 + progress_bar_max_width = 300 + + indicate_hidden = yes + transparency = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + text_icon_padding = 0 + frame_width = 3 + frame_color = "#de5200" + gap_size = 0 + separator_color = frame + sort = yes + corner_radius = 0 + + # Text: the interface face (research 026/04) + font = Inter ${config:font-size} + line_height = 0 + markup = full + format = "%s\n%b" + alignment = left + vertical_alignment = center + show_age_threshold = 60 + ellipsize = middle + ignore_newline = no + stack_duplicates = true + hide_duplicate_count = false + show_indicators = yes + + # Icons, from the desktop's icon theme + enable_recursive_icon_lookup = true + icon_theme = Adwaita + icon_position = left + min_icon_size = 32 + max_icon_size = 128 + + # History + sticky_history = yes + history_length = 20 + + # The context menu is rofi (installed by the rofi module); -no-custom keeps typed text from becoming a choice. Not `dmenu`, the node-launcher's command in + # ~/.local/bin: dunst runs in the account's service manager, whose PATH does not hold that + # directory (seen on the laptop on 2026-10-10), so the menu never opened. Links open in the + # desktop's default browser. + dmenu = rofi -dmenu -no-custom -p dunst + browser = /usr/bin/xdg-open + always_run_script = true + + title = Dunst + class = Dunst + ignore_dbusclose = false + + # A tap answers (the touchpads have no middle button). do_action runs the notification's only + # action, or opens the context menu when it has several; without actions it opens its one link, + # if it has one. The notification then closes; a menu dismissed with Escape leaves it shown. + mouse_left_click = do_action, close_current + mouse_middle_click = do_action, close_current + mouse_right_click = close_all + +[urgency_low] + background = "#000000" + foreground = "#ffffff" + timeout = 10 + +[urgency_normal] + background = "#000000" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#000000" + foreground = "#ffffff" + frame_color = "#ff0000" + timeout = 0 + +# What is shown once is never kept (the review of 2026-10-09): a notification marked secret (a link's +# code, category mesh.secret) and any transient one leave no entry in the history, which +# node-notifier.history serves to every caller of the mesh's verbs. +[mesh-secret] + category = "mesh.secret" + history_ignore = yes + +[transient-history-ignore] + match_transient = yes + history_ignore = yes diff --git a/go/testdata/beside/mesh-catalog/modules/dunst/files/i3/50-dunst.conf b/go/testdata/beside/mesh-catalog/modules/dunst/files/i3/50-dunst.conf new file mode 100644 index 0000000..fec1f8d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/dunst/files/i3/50-dunst.conf @@ -0,0 +1,4 @@ +# The notifications' key (module dunst, novox/hq ADR 0208). Owned by the mesh: replaced at every push. +# Super+N opens the menu (rofi) of the actions and links of every notification on screen, so one is +# chosen from the keyboard as a tap chooses it on the notification. +bindsym $mod+n exec --no-startup-id dunstctl context diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/fail2ban b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/fail2ban new file mode 100644 index 0000000..b5395db --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/fail2ban @@ -0,0 +1,7 @@ +/var/log/fail2ban.log { + missingok + notifempty + postrotate + /usr/bin/fail2ban-client flushlogs >/dev/null || true + endscript +} diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/iptables-allports-dualchain.conf b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/iptables-allports-dualchain.conf new file mode 100644 index 0000000..2c9fc73 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/iptables-allports-dualchain.conf @@ -0,0 +1,40 @@ +# Fail2Ban action: ban in both INPUT and DOCKER-USER chains +# Used by recidive to block repeat offenders from both host and Docker services + +[INCLUDES] + +before = iptables.conf + +[Definition] + +type = allports + +actionstart = { -C f2b- -j >/dev/null 2>&1; } || { -N f2b- || true; -A f2b- -j ; } + { -C INPUT -p -j f2b- >/dev/null 2>&1; } || { -I INPUT -p -j f2b-; } + { -C DOCKER-USER -p -j f2b- >/dev/null 2>&1; } || { -I DOCKER-USER -p -j f2b-; } + +actionstop = -D INPUT -p -j f2b- 2>/dev/null || true + -D DOCKER-USER -p -j f2b- 2>/dev/null || true + -F f2b- + -X f2b- + +actioncheck = -n -L f2b- >/dev/null + +actionban = -I f2b- 1 -s -j + +actionunban = -D f2b- -s -j + +[Init] + +chain = INPUT +name = default +protocol = tcp +blocktype = REJECT --reject-with icmp-port-unreachable +returntype = RETURN +lockingopt = -w +iptables = iptables + +[Init?family=inet6] + +blocktype = REJECT --reject-with icmp6-port-unreachable +iptables = ip6tables diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/jail.local b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/jail.local new file mode 100644 index 0000000..1fa0ffe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/jail.local @@ -0,0 +1,48 @@ +[INCLUDES] + +before = paths-arch.conf + +[DEFAULT] + +# Never act on the machine itself or on a tunnel peer: the mesh's private range is +# ${machine:mesh-range}, named here rather than written as a value the module cannot +# know (novox/hq ADR 0112). Without this, fail2ban could ban the mesh's own nodes. +# **A ban list never holds a neighbour.** The mesh's own range is named rather than written +# (novox/hq ADR 0112), and every private range beside it: a source on one is somebody's own +# network, not the internet. On a machine behind a router that reflects local traffic, every +# client in the house arrives as the gateway's address — so one mistyped local request banned +# 192.168.1.1 on the home server and would have cut the whole house off from it (ADR 0186). +# +# **Nor the operator's own addresses** (novox/hq ADR 0339, issue 238): the public addresses the operator names +# as theirs, in the mesh-wide setting own-addresses. An agent polling the forge from the operator's home banned +# that home for four weeks on 2026-10-04, and nothing in the house reached the control-node until it was found +# among 400 bans. The file is written by this module's watch from the setting, one entry per line; fail2ban +# reads it again within a second of a change, so a new value needs no restart. Empty until the operator +# fills the setting. A jail that sets its own ignoreip leaves all of this out; the watch says so. +ignoreip = 127.0.0.1/8 ::1 ${machine:mesh-range} 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 fc00::/7 fe80::/10 file:/var/lib/mesh-fail2ban/own-addresses + +# Three failures in a day ban for a day (novox/hq ADR 0179). The attackers this mesh sees pace +# themselves at one try every ten minutes, under any ten-minute window; a day's window counts +# them, and a day's ban costs a person who mistyped three times once, from one address, while +# the mesh's own range is never banned at all. +bantime = 1d +findtime = 1d +maxretry = 3 + +# Ban through iptables, not through a firewall front-end the machine may not have. ufw is +# installed on two of this mesh's machines and absent on the other two, and fail2ban finds out +# only at ban time: the service reports healthy, the jail counts the attempt, the ban command +# exits 127, and nothing is blocked. Proven on 2026-09-28 -- 'ufw: command not found' on a +# machine the mesh reported as protected. +# +# The action below is this module's own, already used by the recidive jail on every machine +# here, and it bans in DOCKER-USER as well as INPUT, so a container's published port is +# covered too. +banaction = iptables-allports-dualchain +banaction_allports = iptables-allports-dualchain + +[sshd] +enabled = true +port = ssh +logpath = %(sshd_log)s +backend = %(sshd_backend)s diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/mesh-own-addresses.json b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/mesh-own-addresses.json new file mode 100644 index 0000000..0eb9604 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/mesh-own-addresses.json @@ -0,0 +1,3 @@ +{ + "own-addresses": [] +} diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/own-addresses b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/own-addresses new file mode 100644 index 0000000..e69de29 diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/recidive.conf b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/recidive.conf new file mode 100644 index 0000000..1f8786f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/recidive.conf @@ -0,0 +1,9 @@ +[recidive] +enabled = true +logpath = /var/log/fail2ban.log +# Ban in both INPUT (host services like SSH) and DOCKER-USER (container services) +banaction = iptables-allports-dualchain +# Banned twice in two weeks, by any jail, is banned for four (novox/hq ADR 0179). +bantime = 4w +findtime = 2w +maxretry = 2 diff --git a/go/testdata/beside/mesh-catalog/modules/fail2ban/files/sshd.conf b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/sshd.conf new file mode 100644 index 0000000..c8a30f2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fail2ban/files/sshd.conf @@ -0,0 +1,8 @@ +[sshd] +enabled = true +port = ssh +logpath = %(sshd_log)s +backend = %(sshd_backend)s +maxretry = 3 +findtime = 1d +bantime = 1d diff --git a/go/testdata/beside/mesh-catalog/modules/feh/files/fehbg b/go/testdata/beside/mesh-catalog/modules/feh/files/fehbg new file mode 100644 index 0000000..a3159bb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/feh/files/fehbg @@ -0,0 +1,6 @@ +#!/bin/sh +# The wallpaper (module feh, novox/hq ADR 0208). Owned by the mesh: replaced at every push. The +# session's start runs it, and so may anything that wants the declared wallpaper back. The image is +# the module's own, in ~/.local/share/feh/wallpapers. feh_set changes the wallpaper for a session +# without touching this file. +feh --no-fehbg --bg-fill "$HOME/.local/share/feh/wallpapers/default.jpg" diff --git a/go/testdata/beside/mesh-catalog/modules/feh/files/i3/50-feh.conf b/go/testdata/beside/mesh-catalog/modules/feh/files/i3/50-feh.conf new file mode 100644 index 0000000..40ac7da --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/feh/files/i3/50-feh.conf @@ -0,0 +1,3 @@ +# The wallpaper's key (module feh, novox/hq ADR 0208). Owned by the mesh: replaced at every push. +# It puts the declared wallpaper back, after a monitor change or a wallpaper set for the session. +bindsym $mod+Shift+b exec --no-startup-id ~/.fehbg diff --git a/go/testdata/beside/mesh-catalog/modules/feh/files/xinitrc-normal b/go/testdata/beside/mesh-catalog/modules/feh/files/xinitrc-normal new file mode 100644 index 0000000..8e91e82 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/feh/files/xinitrc-normal @@ -0,0 +1,2 @@ +# The wallpaper (module feh, novox/hq ADR 0208): the declared one, set once per session. +"$HOME/.fehbg" diff --git a/go/testdata/beside/mesh-catalog/modules/fonts/files/50-mesh-fonts.conf b/go/testdata/beside/mesh-catalog/modules/fonts/files/50-mesh-fonts.conf new file mode 100644 index 0000000..76d0c87 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/fonts/files/50-mesh-fonts.conf @@ -0,0 +1,40 @@ + + + + + The mesh: the faces monospace, sans-serif, serif and emoji mean + + + Hack Nerd Fontmonospace + MesloLGS NFmonospace + Iosevka Nerd Fontmonospace + JetBrains Mono Nerd FontJetBrainsMono Nerd Font + + + monospaceJetBrainsMono Nerd Font + sans-serifInterNoto Sans + system-uiInter + serifNoto Serif + emojiNoto Color Emoji + + + + Symbols Nerd Font + Noto Color Emoji + + diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/agent-teams.json b/go/testdata/beside/mesh-catalog/modules/gitea/files/agent-teams.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/agent-teams.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/config.json b/go/testdata/beside/mesh-catalog/modules/gitea/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/consumers.json b/go/testdata/beside/mesh-catalog/modules/gitea/files/consumers.json new file mode 100644 index 0000000..790b51f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/consumers.json @@ -0,0 +1 @@ +${consumers:forge-account} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/origins b/go/testdata/beside/mesh-catalog/modules/gitea/files/origins new file mode 100644 index 0000000..6bbfb35 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/origins @@ -0,0 +1 @@ +https://${bound:route:name-web} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/repositories.json b/go/testdata/beside/mesh-catalog/modules/gitea/files/repositories.json new file mode 100644 index 0000000..36626cc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/repositories.json @@ -0,0 +1 @@ +{"repositories": ${config:repositories}} diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/files/server.env b/go/testdata/beside/mesh-catalog/modules/gitea/files/server.env new file mode 100644 index 0000000..8cb88e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitea/files/server.env @@ -0,0 +1,7 @@ +GITEA__security__INTERNAL_TOKEN=${secret:internal-token} +GITEA__database__DB_TYPE=postgres +GITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port} +GITEA__database__NAME=${bound:postgres-database:as} +GITEA__database__USER=${bound:postgres-database:as} +GITEA__database__PASSWD=${secret:postgres-database} +GITEA__webhook__ALLOWED_HOST_LIST=172.17.0.1 diff --git a/go/testdata/beside/mesh-catalog/modules/gitlab/files/config.json b/go/testdata/beside/mesh-catalog/modules/gitlab/files/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gitlab/files/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/login b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/login new file mode 100644 index 0000000..47833d9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/login @@ -0,0 +1,4 @@ +# The login keyring (module gnome-keyring, novox/hq ADR 0208, ADR 0102): the password typed at the +# login screen unlocks the keyring, and the login session starts the keyring daemon with it. +auth optional pam_gnome_keyring.so +session optional pam_gnome_keyring.so auto_start diff --git a/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/passwd b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/passwd new file mode 100644 index 0000000..dd43bb8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/pam/passwd @@ -0,0 +1,3 @@ +# The login keyring (module gnome-keyring, novox/hq ADR 0208, ADR 0102): changing the account's +# password changes the login keyring's with it, so the next login still unlocks it. +password optional pam_gnome_keyring.so diff --git a/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/xinitrc-first b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/xinitrc-first new file mode 100644 index 0000000..5fb92b8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/gnome-keyring/files/xinitrc-first @@ -0,0 +1,5 @@ +# The ssh agent (module gnome-keyring, novox/hq ADR 0208): gcr's, which the account's service manager +# starts on first use from its socket. Named here, for the session and every terminal it starts, until +# the account's environment can say a path under the runtime directory (see the module's README). +SSH_AUTH_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gcr/ssh" +export SSH_AUTH_SOCK diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/admin.secret b/go/testdata/beside/mesh-catalog/modules/grafana/files/admin.secret new file mode 100644 index 0000000..51bf424 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/admin.secret @@ -0,0 +1 @@ +${secret:admin} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/config.json b/go/testdata/beside/mesh-catalog/modules/grafana/files/config.json new file mode 100644 index 0000000..567df9a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/config.json @@ -0,0 +1,4 @@ +{ + "user": "admin", + "password": "${secret:admin}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/influxdb-api b/go/testdata/beside/mesh-catalog/modules/grafana/files/influxdb-api new file mode 100644 index 0000000..8329ab7 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/influxdb-api @@ -0,0 +1 @@ +${secret:influxdb-api} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/mesh-influxdb.yaml b/go/testdata/beside/mesh-catalog/modules/grafana/files/mesh-influxdb.yaml new file mode 100644 index 0000000..e341005 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/mesh-influxdb.yaml @@ -0,0 +1,19 @@ +apiVersion: 1 +# Written by the mesh from grafana's influxdb-api binding, only while it is bound (novox/hq ADR 0330): +# without influxdb, grafana runs with no mesh data source. grafana reads it at start. Its own name and +# uid, so a data source somebody made in the UI is never overwritten, and read-only in the UI because +# the mesh resets it. The password is read from the file the mesh delivers, never written here. +datasources: + - name: InfluxDB (mesh) + uid: mesh-influxdb-api + type: influxdb + access: proxy + url: ${bound:influxdb-api:scheme}://${bound:influxdb-api:at}:${bound:influxdb-api:port} + user: ${bound:influxdb-api:as} + isDefault: false + editable: false + jsonData: + dbName: ${bound:influxdb-api:bucket} + httpMode: POST + secureJsonData: + password: $__file{/run/secrets/influxdb/influxdb-api} diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc-client.secret b/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc-client.secret new file mode 100644 index 0000000..9146dc1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc-client.secret @@ -0,0 +1 @@ +${secret:oidc-client} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc.env b/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc.env new file mode 100644 index 0000000..bcc1e9c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/grafana/files/oidc.env @@ -0,0 +1,13 @@ +GF_SERVER_ROOT_URL=https://${bound:route:name} +GF_AUTH_GENERIC_OAUTH_ENABLED=true +GF_AUTH_GENERIC_OAUTH_NAME=Keycloak +GF_AUTH_GENERIC_OAUTH_CLIENT_ID=${bound:oidc-client:as} +GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE=/run/secrets/oidc-client +GF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile roles +GF_AUTH_GENERIC_OAUTH_AUTH_URL=${bound:oidc-client:issuer}${bound:oidc-client:authorization-path} +GF_AUTH_GENERIC_OAUTH_TOKEN_URL=${bound:oidc-client:issuer}${bound:oidc-client:token-path} +GF_AUTH_GENERIC_OAUTH_API_URL=${bound:oidc-client:issuer}${bound:oidc-client:userinfo-path} +GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(roles[*], 'admin') && 'Admin' || contains(realm_access.roles[*], 'admin') && 'Admin' || 'Viewer' +GF_AUTH_GENERIC_OAUTH_USE_PKCE=true +GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true +GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN=true diff --git a/go/testdata/beside/mesh-catalog/modules/hello-web/files/index.html b/go/testdata/beside/mesh-catalog/modules/hello-web/files/index.html new file mode 100644 index 0000000..0d513fd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/hello-web/files/index.html @@ -0,0 +1 @@ +hello from hello-web, routed by the mesh diff --git a/go/testdata/beside/mesh-catalog/modules/home-assistant/files/config.json b/go/testdata/beside/mesh-catalog/modules/home-assistant/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/home-assistant/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/home-assistant/files/provisions.env b/go/testdata/beside/mesh-catalog/modules/home-assistant/files/provisions.env new file mode 100644 index 0000000..93aba0c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/home-assistant/files/provisions.env @@ -0,0 +1,4 @@ +MESH_HOMEASSISTANT_URL=http://127.0.0.1:${port:8123} +MESH_HOMEASSISTANT_TOKEN_FILE=${dir:mesh-state}/token +MESH_PROVISIONS_DIR=${dir:state} +MESH_WRITTEN_DIR=${dir:written} diff --git a/go/testdata/beside/mesh-catalog/modules/hostname/files/hostname b/go/testdata/beside/mesh-catalog/modules/hostname/files/hostname new file mode 100644 index 0000000..34c12a4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/hostname/files/hostname @@ -0,0 +1 @@ +${config:hostname} diff --git a/go/testdata/beside/mesh-catalog/modules/hostname/files/hosts b/go/testdata/beside/mesh-catalog/modules/hostname/files/hosts new file mode 100644 index 0000000..7c9c073 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/hostname/files/hosts @@ -0,0 +1,7 @@ +# The machine's own names (module hostname, novox/hq ADR 0199, ADR 0223). Every line outside this +# block is the operator's: kept across every push, changed through the node-hostname verbs add and +# remove, and given back when this module goes. The mesh's names are not here: the mesh's resolver +# answers them. +127.0.0.1 localhost +::1 localhost +127.0.1.1 ${machine:name} diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/config b/go/testdata/beside/mesh-catalog/modules/i3/files/config new file mode 100644 index 0000000..c49398c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/files/config @@ -0,0 +1,201 @@ +# i3 config file (v4), written by the mesh (module i3, novox/hq ADR 0208). Replaced at every push; +# change the module instead. i3's user guide is the reference. +# +# Other modules add to this configuration as contributions to node-display-session (novox/hq ADR +# 0212): the mesh places their lines near the end, each module's under a line naming it, where every +# variable set here ($mod, $ws1 … $ws10) is in scope. A file of yours in ~/.config/i3/config.d/ is read +# after them, by the include at the very end, and is yours. +# +# The reload watcher of this module reloads i3 when this file or a drop-in changes, after checking the +# result with i3 -C; it never reloads into a configuration with errors. + +# Font for window titles, and the bars below: the mesh's monospace face (research 026/04). +font pango:JetBrainsMono Nerd Font 11 + +# XDG autostart entries (~/.config/autostart, /etc/xdg/autostart), started once at login. +exec --no-startup-id dex --autostart --environment i3 + +######################################### +###### Keys #### +######################################### +# To find key symbols: xmodmap -pke / xmodmap -pm +set $mod Mod4 +set $alt Mod1 +set $shift Shift +set $ctrl Control + +# use these keys for focus, movement, and resize directions when reaching for +# the arrows is not convenient +set $left h +set $down j +set $up k +set $right l + +# use Mouse+$mod to drag floating windows to their wanted position +floating_modifier $mod + +# move tiling windows via drag & drop by left-clicking into the title bar, +# or left-clicking anywhere into the window while holding the floating modifier. +tiling_drag modifier titlebar + +# start a terminal: whichever the terminal module names in $TERMINAL +bindsym $mod+Return exec i3-sensible-terminal + +# kill focused window +bindsym $mod+$shift+q kill + +# change focus +bindsym $mod+$left focus left +bindsym $mod+$down focus down +bindsym $mod+$up focus up +bindsym $mod+$right focus right + +bindsym $mod+Left focus left +bindsym $mod+Down focus down +bindsym $mod+Up focus up +bindsym $mod+Right focus right + +# move focused window +bindsym $mod+$shift+$left move left +bindsym $mod+$shift+$down move down +bindsym $mod+$shift+$up move up +bindsym $mod+$shift+$right move right + +bindsym $mod+$shift+Left move left +bindsym $mod+$shift+Down move down +bindsym $mod+$shift+Up move up +bindsym $mod+$shift+Right move right + +# split in horizontal orientation +bindsym $mod+c split h +# split in vertical orientation +bindsym $mod+v split v + +# enter fullscreen mode for the focused container +bindsym $mod+f fullscreen toggle + +# change container layout (stacked, tabbed, toggle split) +bindsym $mod+s layout stacking +bindsym $mod+w layout tabbed +bindsym $mod+e layout toggle split + +# toggle tiling / floating +bindsym $mod+$shift+space floating toggle + +# change focus between tiling / floating windows +bindsym $mod+space focus mode_toggle + +# focus the parent container +bindsym $mod+a focus parent + +# alt-tab functionality +bindsym $mod+Tab workspace back_and_forth + +######################################### +###### Workspace mgmt #### +######################################### +set $ws1 "1" +set $ws2 "2" +set $ws3 "3" +set $ws4 "4" +set $ws5 "5" +set $ws6 "6" +set $ws7 "7" +set $ws8 "8" +set $ws9 "9" +set $ws10 "10" + +bindsym $mod+1 workspace number $ws1 +bindsym $mod+2 workspace number $ws2 +bindsym $mod+3 workspace number $ws3 +bindsym $mod+4 workspace number $ws4 +bindsym $mod+5 workspace number $ws5 +bindsym $mod+6 workspace number $ws6 +bindsym $mod+7 workspace number $ws7 +bindsym $mod+8 workspace number $ws8 +bindsym $mod+9 workspace number $ws9 +bindsym $mod+0 workspace number $ws10 + +bindsym $mod+$shift+1 move container to workspace number $ws1 +bindsym $mod+$shift+2 move container to workspace number $ws2 +bindsym $mod+$shift+3 move container to workspace number $ws3 +bindsym $mod+$shift+4 move container to workspace number $ws4 +bindsym $mod+$shift+5 move container to workspace number $ws5 +bindsym $mod+$shift+6 move container to workspace number $ws6 +bindsym $mod+$shift+7 move container to workspace number $ws7 +bindsym $mod+$shift+8 move container to workspace number $ws8 +bindsym $mod+$shift+9 move container to workspace number $ws9 +bindsym $mod+$shift+0 move container to workspace number $ws10 + +######################################### +###### Window mgmt #### +######################################### +set $resize_px 10 px +bindsym $mod+$ctrl+$left resize shrink width $resize_px +bindsym $mod+$ctrl+$down resize grow height $resize_px +bindsym $mod+$ctrl+$up resize shrink height $resize_px +bindsym $mod+$ctrl+$right resize grow width $resize_px + +######################################### +###### Session mgmt #### +######################################### +bindsym $mod+$shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -B 'Yes, exit i3' 'i3-msg exit'" + +# Lock the screen through logind, so the one locker the lock screen's module runs handles it (and +# suspend and lid close too). +bindsym $mod+Delete exec --no-startup-id loginctl lock-session + +# reload the configuration file +bindsym $mod+$shift+c reload + +# restart i3 inplace (preserves your layout/session, can be used to upgrade i3) +bindsym $mod+$shift+r restart + +######################################### +###### Borders #### +######################################### +default_border pixel 1 +smart_borders on + +######################################### +###### Gaps #### +######################################### +gaps inner 0 +gaps outer 0 + +# Only the accent-bearing slots are themed; background and text keep i3's own defaults. Borders are +# `pixel`, so no title bar shows: the colour says which window has focus. +# border background text indicator child_border +client.focused #de5200 #de5200 #1E2127 #de5200 #de5200 +client.urgent #900000 #900000 #ffffff #900000 #900000 + +######################################### +###### Until their modules carry them ## +######################################### +# Each line below belongs to something other than i3, named on its line. When that module is written +# it contributes the line to node-display-session, and the line goes from here in the same change. +# The launcher, the clipboard, the wallpaper, the bars, a machine model's keys and the peripherals' +# tray already contribute theirs (rofi, clipmenu, feh, i3status-rust, asus-zephyrus-g14, +# polychromatic). + +# the operator's scripts: volume, games volume, sessions, screenshot +bindsym XF86AudioRaiseVolume exec --no-startup-id volume-notify up +bindsym XF86AudioLowerVolume exec --no-startup-id volume-notify down +bindsym XF86AudioMute exec --no-startup-id volume-notify mute +bindsym XF86AudioMicMute exec --no-startup-id mic-notify +bindsym $ctrl+XF86AudioRaiseVolume exec --no-startup-id set-games-volume 5 +bindsym $ctrl+XF86AudioLowerVolume exec --no-startup-id set-games-volume -5 +bindsym $mod+$shift+Return exec --no-startup-id ~/scripts/i3-sessions/launcher.sh +bindsym --release $ctrl+$shift+x exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh + +######################################### +###### Other modules' lines #### +######################################### +# Placed by the mesh from every other module's contribution (novox/hq ADR 0212): the launcher, the +# clipboard, the wallpaper, the bars, a machine model's keys, the peripherals' tray. Each module's +# under a line naming it. +${contribution:node-display-session:config} +######################################### +###### Your own files #### +######################################### +include ~/.config/i3/config.d/*.conf diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-current-desktop b/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-current-desktop new file mode 100644 index 0000000..04ad29f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-current-desktop @@ -0,0 +1 @@ +i3 diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-session-desktop b/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-session-desktop new file mode 100644 index 0000000..04ad29f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/files/env-xdg-session-desktop @@ -0,0 +1 @@ +i3 diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 b/go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 new file mode 100644 index 0000000..4e269ad --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 @@ -0,0 +1,9 @@ +#!/bin/sh +# The i3 session (module i3, novox/hq ADR 0208), offered by the login manager. It runs the session's +# start, ~/.xinitrc, which the display server's module writes and which ends by starting i3. +# Replaced at every push. +# +# The login manager hands the session a stdout and stderr whose reading end nobody holds, so a +# program that writes to them dies of EPIPE (an Electron tray app shows it as an error dialog). +# The session's output goes to the journal instead, under `journalctl -t x-session`. +exec systemd-cat -t x-session /bin/sh "$HOME/.xinitrc" diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/xinitrc-last b/go/testdata/beside/mesh-catalog/modules/i3/files/xinitrc-last new file mode 100644 index 0000000..e84ff3d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3/files/xinitrc-last @@ -0,0 +1,2 @@ +# The session: i3, with 25 MiB of debug log kept in memory for i3-dump-log. +exec i3 --shmlog-size=26214400 diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog new file mode 100644 index 0000000..63b2b02 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# i3bar-watchdog [session-pid] (module i3status-rust, novox/hq ADR 0208): respawns a bar that died. +# +# i3 starts one i3bar per `bar { }` block and never restarts one that exits, and a reload does not +# either. Changing the monitor setup reliably kills the bar that owns the tray. This brings back the +# one missing i3bar, without restarting i3, and logs the outputs at that moment: the evidence for the +# cause, which is i3bar's. +# +# Started once per session from the session's start, with the session's own pid; it ends when that +# process does. Adopted from the predecessor's i3-bar-watchdog user unit of 2026-10-04. +set -uo pipefail + +session_pid="${1:-}" +interval="${I3_BAR_WATCHDOG_INTERVAL:-5}" +# Two misses in a row before acting: an i3 restart tears every bar down and starts them again. +confirm="${I3_BAR_WATCHDOG_CONFIRM:-2}" +# Never respawn the same bar more often than this, so a bar that dies at once is not a tight loop. +cooldown="${I3_BAR_WATCHDOG_COOLDOWN:-30}" + +log() { printf 'i3bar-watchdog: %s\n' "$*" >&2; } + +for tool in i3-msg i3bar pgrep; do + command -v "$tool" >/dev/null 2>&1 || { + log "$tool is missing; not watching" + exit 1 + } +done + +declare -A missed=() fixed=() + +bar_ids() { i3-msg -t get_bar_config 2>/dev/null | grep -oE '"[^"]+"' | tr -d '"'; } +outputs() { xrandr --listmonitors 2>/dev/null | tail -n +2 | awk '{print $2" "$3}' | tr '\n' ' '; } +session_alive() { [ -z "$session_pid" ] || kill -0 "$session_pid" 2>/dev/null; } + +while session_alive; do + sleep "$interval" + socket="$(i3 --get-socketpath 2>/dev/null)" + [ -n "$socket" ] && [ -S "$socket" ] || continue + ids="$(bar_ids)" + [ -n "$ids" ] || continue + while read -r id; do + [ -n "$id" ] || continue + if pgrep -u "$EUID" -f -- "i3bar --bar_id=$id" >/dev/null 2>&1; then + missed[$id]=0 + continue + fi + missed[$id]=$((${missed[$id]:-0} + 1)) + [ "${missed[$id]}" -ge "$confirm" ] || continue + now="$(date +%s)" + if [ $((now - ${fixed[$id]:-0})) -lt "$cooldown" ]; then + continue + fi + log "$id is gone; respawning it. Outputs now: $(outputs)" + nohup i3bar --bar_id="$id" --socket="$socket" >/dev/null 2>&1 & + disown 2>/dev/null || true + fixed[$id]="$now" + sleep 2 + if pgrep -u "$EUID" -f -- "i3bar --bar_id=$id" >/dev/null 2>&1; then + missed[$id]=0 + else + log "$id exited within 2s of respawning; check its status_command" + fi + done <<<"$ids" +done diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates new file mode 100644 index 0000000..1a70cf9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# i3status-updates [threshold] (module i3status-rust, novox/hq ADR 0208): the pending updates, for +# the bottom bar's custom block (json = true). Emits one JSON line. +# +# Adopted from the operator's pkg-updates of 2026-10-04. The native `packages` block showed a raw +# red error on every transient network failure, which on a roaming laptop is often. This keeps the +# last good result through a failure and shows an error only after `threshold` failures in a row +# (default 10). Official packages through checkupdates (pacman-contrib, a temporary database, no +# root); AUR packages through paru when it is installed, and none counted when it is not. +set -uo pipefail + +threshold="${1:-10}" +icon="update" +state_dir="${XDG_RUNTIME_DIR:-/tmp}/i3status-updates" +mkdir -p "$state_dir" +last="$state_dir/last_good.json" +failures="$state_dir/failures" + +json() { printf '{"icon":"%s","state":"%s","text":"%s","short_text":"%s"}' "$icon" "$1" "$2" "$3"; } +count() { [ -n "$1" ] && printf '%s\n' "$1" | grep -c . || echo 0; } + +official="$(checkupdates 2>/dev/null)" +official_rc=$? +aur="" aur_rc=0 +if command -v paru >/dev/null 2>&1; then + aur="$(paru -Qua 2>/dev/null)" + aur_rc=$? +fi + +# checkupdates: 0 updates listed, 2 none pending; anything else is a failure. +if { [ "$official_rc" -eq 0 ] || [ "$official_rc" -eq 2 ]; } && [ "$aur_rc" -eq 0 ]; then + o="$(count "$official")" + a="$(count "$aur")" + total=$((o + a)) + if [ "$total" -eq 0 ]; then + payload="$(json Good "up to date" "")" + else + # A kernel update wants a reboot: the one worth not putting off. + state=Info + printf '%s\n' "$official" | grep -qE '^(linux|linux-lts|linux-zen) ' && state=Warning + label="$o + $a = $total updates" + [ "$a" -eq 0 ] && label="$total updates" + [ "$total" -eq 1 ] && label="1 update" + payload="$(json "$state" "$label" "$total")" + fi + printf '%s' "$payload" >"$last" + printf '0' >"$failures" + printf '%s\n' "$payload" + exit 0 +fi + +n=$(($(cat "$failures" 2>/dev/null || echo 0) + 1)) +printf '%s' "$n" >"$failures" +if [ "$n" -ge "$threshold" ]; then + json Critical "update check failing (${n}x)" "!" + echo +elif [ -s "$last" ]; then + cat "$last" + echo +else + json Idle "" "" + echo +fi diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value new file mode 100644 index 0000000..8fea95e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value @@ -0,0 +1,13 @@ +#!/bin/sh +# i3status-value (module i3status-rust, novox/hq ADR 0260): one value another module keeps +# on the bus, for a block it contributed. +# +# This module's own tools bundle watches the state on the bus (the mesh grants it the read) and leaves +# the value's text in this module's own directory; this prints it. A value not refreshed for thirty +# seconds is one its module stopped giving, and prints nothing, so the block hides rather than show an +# old number as if it were now. +dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/i3status-rust/values" +f="$dir/$1@$2" +if [ -r "$f" ] && [ $(( $(date +%s) - $(stat -c %Y "$f") )) -le 30 ]; then + head -n 1 "$f" +fi diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bottom-bar.toml b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bottom-bar.toml new file mode 100644 index 0000000..12e6805 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bottom-bar.toml @@ -0,0 +1,109 @@ +# The bottom bar (module i3status-rust, novox/hq ADR 0208). Owned by the mesh: replaced at every +# push. Adopted from the workstations of 2026-10-04 with what follows a machine's hardware or one +# person's devices left out: a block that depends on the hardware is contributed by the module that +# knows it (novox/hq ADR 0255), and a headset's address is not the bar's to know. Every block written +# here works the same on any machine. +icons_format = "{icon}" + +[theme] +theme = "plain" +[theme.overrides] +idle_bg = "#000000" +idle_fg = "#f1f1f1" +info_bg = "#000000" +info_fg = "#f1f1f1" +good_bg = "#000000" +good_fg = "#859900" +warning_bg = "#000000" +warning_fg = "#de5200" +critical_bg = "#000000" +critical_fg = "#ff0000" +separator = " | " +separator_fg = "#ffffff" +separator_bg = "#000000" + +[icons] +icons = "custom-icons" + +[[block]] +block = "tea_timer" +format = "$icon{ $minutes:$seconds|}" +done_cmd = "notify-send 'Timer finished'" + +# Pending updates: official and, where an AUR helper is installed, AUR. The script keeps the last good +# count through a network gap and shows an error only after ten failed checks in a row. Click: the +# list, in the launcher's menu. +[[block]] +block = "custom" +command = "~/.local/bin/i3status-updates 10" +json = true +interval = 1800 +[[block.click]] +button = "left" +cmd = "checkupdates | dmenu -l 20 -p Updates" + +[[block]] +block = "cpu" + +[[block]] +block = "disk_space" +path = "/" +info_type = "used" +alert_unit = "GB" +interval = 20 +warning = 850 +alert = 920 +format = "$icon / $used.eng(w:2) ($percentage.eng(w:2))" + +[[block]] +block = "disk_space" +path = "/home" +info_type = "used" +alert_unit = "GB" +interval = 20 +warning = 850 +alert = 920 +format = "$icon /home $used.eng(w:2) ($percentage.eng(w:2))" + +[[block]] +block = "memory" +# What programs hold, without the cache the kernel gives back when asked: the figure the memory-pressure +# module judges by. Counting the cache as used read near full on a machine with room to spare. +format = "$icon $mem_used.eng(w:2) ($mem_used_percents.eng(w:2))" +format_alt = "$icon_swap $swap_used.eng(w:2) ($swap_used_percents.eng(w:2))" + +# Blocks other modules contribute beside the machine's resources (novox/hq ADR 0255), rendered from +# what they show by this module's template, and only on a machine that has what they show. +${collected:contribution:node-bar:block:bar=bottom,place=resources} +[[block]] +block = "docker" +interval = 2 +format = "$icon $running/$total" + +[[block]] +block = "sound" +[[block.click]] +button = "left" +cmd = "pavucontrol" + +# Blocks other modules contribute beside the sound (novox/hq ADR 0255): the power module's battery and +# power draw, where the machine has them. +${collected:contribution:node-bar:block:bar=bottom,place=status} +# The weather from the Norwegian Meteorological Institute, which needs no key, where the machine is. +[[block]] +block = "weather" +format = "$icon $weather_verbose ($location) $temp" +autolocate = true +autolocate_interval = 600 +[block.service] +name = "metno" + +[[block]] +block = "notify" +driver = "dunst" +format = " $icon {($notification_count.eng(w:1))|}" + +[[block]] +block = "time" +interval = 1 +format = "$timestamp.datetime(f:'%a %d/%m %H:%M:%S')" diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/i3/60-i3status-rust.conf b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/i3/60-i3status-rust.conf new file mode 100644 index 0000000..20acbb1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/i3/60-i3status-rust.conf @@ -0,0 +1,34 @@ +# The bars (module i3status-rust, novox/hq ADR 0208). Owned by the mesh: replaced at every push. +# i3 reads this file through its configuration's `include ~/.config/i3/config.d/*.conf`. The bottom +# bar shows the machine; the top bar the focused window and the tray, on the primary output. The +# face is the monospace one every desktop module names. +bar { + font pango:JetBrainsMono Nerd Font 11 + position bottom + status_command i3status-rs ~/.config/i3status-rust/bottom-bar.toml + tray_output none + colors { + separator #ffffff + background #000000 + statusline #ffffff + # The text on an accent-coloured button is dark: light text on the accent was barely + # legible. + focused_workspace #de5200 #de5200 #000000 + active_workspace #de5200 #de5200 #000000 + inactive_workspace #000000 #000000 #ffffff + urgent_workspace #2f343a #900000 #ffffff + } +} + +bar { + font pango:JetBrainsMono Nerd Font 11 + position top + status_command i3status-rs ~/.config/i3status-rust/top-bar.toml + workspace_buttons no + tray_output primary + colors { + separator #ffffff + background #000000 + statusline #ffffff + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/icons/custom-icons.toml b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/icons/custom-icons.toml new file mode 100644 index 0000000..9636a82 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/icons/custom-icons.toml @@ -0,0 +1,135 @@ +# Icons for the bars (module i3status-rust, novox/hq ADR 0208), from the JetBrains Mono Nerd Font. +# Owned by the mesh: replaced at every push. Adopted unchanged from the workstations of 2026-10-04. +# Material from NerdFont +# Codepoints from the Nerd Fonts cheat sheet +backlight = [ + "\ue38d", # nf-weather-moon_new + "\ue3d4", # nf-weather-moon_alt_waxing_gibbous_6 + "\ue3d3", # nf-weather-moon_alt_waxing_gibbous_5 + "\ue3d2", # nf-weather-moon_alt_waxing_gibbous_4 + "\ue3d1", # nf-weather-moon_alt_waxing_gibbous_3 + "\ue3d0", # nf-weather-moon_alt_waxing_gibbous_2 + "\ue3cf", # nf-weather-moon_alt_waxing_gibbous_1 + "\ue3ce", # nf-weather-moon_alt_first_quarter + "\ue3cd", # nf-weather-moon_alt_waxing_crescent_6 + "\ue3cc", # nf-weather-moon_alt_waxing_crescent_5 + "\ue3cb", # nf-weather-moon_alt_waxing_crescent_4 + "\ue3ca", # nf-weather-moon_alt_waxing_crescent_3 + "\ue3c9", # nf-weather-moon_alt_waxing_crescent_2 + "\ue3c8", # nf-weather-moon_alt_waxing_crescent_1 + "\ue39b", # nf-weather-moon_full +] +bat_charging = "\U000f0084" # nf-md-battery_charging +bat_not_available = "\U000f0091" # nf-md-battery_unknown +bat = [ + "\U000f007a", # nf-md-battery_10 + "\U000f007b", # nf-md-battery_20 + "\U000f007c", # nf-md-battery_30 + "\U000f007d", # nf-md-battery_40 + "\U000f007e", # nf-md-battery_50 + "\U000f007f", # nf-md-battery_60 + "\U000f0080", # nf-md-battery_70 + "\U000f0081", # nf-md-battery_80 + "\U000f0082", # nf-md-battery_90 + "\U000f0079", # nf-md-battery +] +bell = "\U000f009c" # nf-md-bell_outline +bell-slash = "\U000f009b" # nf-md-bell_off +blackberry = "\uf307" # nf-md-blackberry +bluetooth = "\U000f00af" # nf-md-bluetooth +calendar = "\U000f00ed" # nf-md-calendar +cogs = "\U000f0493" # nf-md-cog +cpu = [ + "\U000F0F86", # nf-md-speedometer_slow + "\U000F0F85", # nf-md-speedometer_medium + "\U000F04C5", # nf-md-speedometer +] +cpu_boost_on = "\U000f0521" # nf-md-toggle_switch +cpu_boost_off = "\U000f0a19" # nf-md-toggle_switch_off_outline +disk_drive = "\U000f02ca" # nf-md-harddisk +docker = "\uf308" # nf-linux-docker +github = "\U000f02a4" # nf-md-github +gpu = "\U000f0379" # nf-md-monitor +headphones = "\U000f02cb" # nf-md-headphones +joystick = "\U000f0297" # nf-md-gamepad_variant +keyboard = "\U000f030c" # nf-md-keyboard +mail = "\U000f01ee" # nf-md-email +memory_mem = "\U000f035b" # nf-md-memory +memory_swap = "\U000f02ca" # nf-md-harddisk +mouse = "\U000f037d" # nf-md-mouse +music = "\U000f075a" # nf-md-music +music_next = "\U000f04ad" # nf-md-skip_next +music_pause = "\U000f03e4" # nf-md-pause +music_play = "\U000f040a" # nf-md-play +music_prev = "\U000f04ae" # nf-md-skip_previous +net_bridge = "\U000f04aa" # nf-md-sitemap +net_down = "\U000f01da" # nf-md-download +net_loopback = "\U000f006f" # nf-md-backup_restore +net_modem = "\U000f03f2" # nf-md-phone +net_cellular = [ + "\U000F08FD", # nf-md-network_strength_off_outline + "\U000F08FE", # nf-md-network_strength_outline + "\U000F08F4", # nf-md-network_strength_1 + "\U000F08F6", # nf-md-network_strength_2 + "\U000F08F8", # nf-md-network_strength_3 + "\U000F08FA", # nf-md-network_strength_4 +] +net_up = "\U000f0552" # nf-md-upload +net_vpn = "\U000f0582" # nf-md-vpn +net_wired = "\U000f0200" # nf-md-ethernet +net_wireless = [ + "\U000F092F", # nf-md-wifi_strength_outline + "\U000F091F", # nf-md-wifi_strength_1 + "\U000F0922", # nf-md-wifi_strength_2 + "\U000F0925", # nf-md-wifi_strength_3 + "\U000F0928", # nf-md-wifi_strength_4 +] +notification = "\U000f009c" # nf-md-bell_outline +phone = "\U000f03f2" # nf-md-phone +phone_disconnected = "\U000f0658" # nf-md-phone_minus +ping = "\U000f051f" # nf-md-timer_sand +pomodoro = "\ue001" # nf-pom-pomodoro_done +pomodoro_break = "\U000f0176" # nf-md-coffee +pomodoro_paused = "\U000f03e4" # nf-md-pause +pomodoro_started = "\U000f040a" # nf-md-play +pomodoro_stopped = "\U000f04db" # nf-md-stop +resolution = "\U000f0293" # nf-md-fullscreen +tasks = "\U000f05c7" # nf-md-playlist_check +tea = "\U000f0d9e" # nf-md-tea +thermometer = [ + "\U000f10c3", # nf-md-thermometer_low + "\U000f050f", # nf-md-thermometer + "\U000f10c2", # nf-md-thermometer_high +] +time = "\U000f0150" # nf-md-clock_outline +toggle_off = "\U000f0a19" # nf-md-toggle_switch_off_outline +toggle_on = "\U000f0521" # nf-md-toggle_switch +unknown = "\U000f0186" # nf-md-comment_question_outline | TODO: Make default? +update = "\U000f03d5" # nf-md-package_up +uptime = "\U000f0153" # nf-md-clock_in +volume_muted = "\U000f075f" # nf-md-volume_mute +volume = [ + "\U000f057f", # nf-md-volume_low + "\U000f0580", # nf-md-volume_medium + "\U000f057e", # nf-md-volume_high +] +microphone_muted = "\U000f036d" # nf-md-microphone_off +microphone = [ + "\U000f036e", # nf-md-microphone_outline + "\U000f036c", # nf-md-microphone + "\U000f036c", # nf-md-microphone +] +xrandr = "\U000f037a" # nf-md-monitor_multiple + +# Weather icons (the names the weather block documents) +weather_sun = "\ue30d" # nf-weather-day_sunny (when weather is reported as “Clear” during the day) +weather_moon = "\ue32b" # nf-weather-night_clear (when weather is reported as “Clear” at night) +weather_clouds = "\ue312" # nf-weather-cloudy (when weather is reported as “Clouds” during the day) +weather_clouds_night = "\ue37e" # nf-weather-night_alt_cloudy (when weather is reported as “Clouds” at night) +weather_fog = "\ue313" # nf-weather-fog (when weather is reported as “Fog” or “Mist” during the day) +weather_fog_night = "\ue346" # nf-weather-night_fog (when weather is reported as “Fog” or “Mist” at night) +weather_rain = "\ue318" # nf-weather-rain (when weather is reported as “Rain” or “Drizzle” during the day) +weather_rain_night = "\ue325" # nf-weather-night_alt_rain (when weather is reported as “Rain” or “Drizzle” at night) +weather_snow = "\ue31a" # nf-weather-snow (when weather is reported as “Snow”) +weather_thunder = "\ue31d" # nf-weather-thunderstorm (when weather is reported as “Thunderstorm” during the day) +weather_thunder_night = "\ue32a" # nf-weather-night_alt_thunderstorm (when weather is reported as “Thunderstorm” at night) diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/top-bar.toml b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/top-bar.toml new file mode 100644 index 0000000..be198e8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/top-bar.toml @@ -0,0 +1,37 @@ +# The top bar (module i3status-rust, novox/hq ADR 0208): the focused window's title and the uptime. +# Owned by the mesh: replaced at every push. Adopted unchanged from the workstations of 2026-10-04. +icons_format = "{icon}" + +[theme] +theme = "plain" +[theme.overrides] +idle_bg = "#000000" +idle_fg = "#f1f1f1" +info_bg = "#000000" +info_fg = "#f1f1f1" +good_bg = "#000000" +good_fg = "#859900" +warning_bg = "#000000" +warning_fg = "#de5200" +critical_bg = "#000000" +critical_fg = "#ff0000" +separator = " | " +separator_fg = "#ffffff" +separator_bg = "#000000" + +[icons] +icons = "custom-icons" + +[[block]] +block = "focused_window" +[block.format] +full = " $title.str(max_w:50) |" +short = " $title.str(max_w:100) |" + +[[block]] +block = "uptime" +interval = 60 + +# Blocks other modules contribute to the top bar (novox/hq ADR 0255). None do yet; both places are kept +# so that one that does is never dropped. +${collected:contribution:node-bar:block:bar=top,place=resources}${collected:contribution:node-bar:block:bar=top,place=status} diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/xinitrc-normal b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/xinitrc-normal new file mode 100644 index 0000000..888acdb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/xinitrc-normal @@ -0,0 +1,3 @@ +# The bar watchdog (module i3status-rust, novox/hq ADR 0208): i3 never restarts a bar that dies; this +# brings it back. Once per session, ending with the session's own process ($$). +"$HOME/.local/bin/i3bar-watchdog" "$$" & diff --git a/go/testdata/beside/mesh-catalog/modules/icecast/files/config.json b/go/testdata/beside/mesh-catalog/modules/icecast/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/icecast/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/icecast/files/icecast.xml b/go/testdata/beside/mesh-catalog/modules/icecast/files/icecast.xml new file mode 100644 index 0000000..de4dd79 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/icecast/files/icecast.xml @@ -0,0 +1,56 @@ + + + Earth + icemaster@localhost + + 100 + 2 + 524288 + 30 + 15 + 10 + 1 + 65535 + + + ${secret:source} + ${secret:relay} + admin + ${secret:admin} + + + localhost + + 8000 + + +
+ + 1 + + /usr/share/icecast + /var/log/icecast + /usr/share/icecast/web + /usr/share/icecast/admin + + + + access.log + error.log + 3 + 10000 + + + 0 + + + icecast + icecast + + + diff --git a/go/testdata/beside/mesh-catalog/modules/influxdb/files/config.json b/go/testdata/beside/mesh-catalog/modules/influxdb/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/influxdb/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/influxdb/files/server.env b/go/testdata/beside/mesh-catalog/modules/influxdb/files/server.env new file mode 100644 index 0000000..d2552e0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/influxdb/files/server.env @@ -0,0 +1,6 @@ +DOCKER_INFLUXDB_INIT_MODE=setup +DOCKER_INFLUXDB_INIT_USERNAME=admin +DOCKER_INFLUXDB_INIT_PASSWORD_FILE=/run/secrets/admin +DOCKER_INFLUXDB_INIT_ADMIN_TOKEN_FILE=/run/secrets/admin-token +DOCKER_INFLUXDB_INIT_ORG=mesh +DOCKER_INFLUXDB_INIT_BUCKET=default diff --git a/go/testdata/beside/mesh-catalog/modules/invoicing/files/api.env b/go/testdata/beside/mesh-catalog/modules/invoicing/files/api.env new file mode 100644 index 0000000..8eaee6f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/invoicing/files/api.env @@ -0,0 +1,9 @@ +NODE_ENV=production +PORT=9000 +MONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as} +MONGO_DB=${bound:mongodb-database:as} +MINIO_BUCKET=${bound:s3-bucket:bucket} +MINIO_ENDPOINT=${bound:s3-bucket:at} +MINIO_PORT=${bound:s3-bucket:port} +MINIO_ACCESSKEY=${bound:s3-bucket:as} +MINIO_SECRET=${secret:s3-bucket} diff --git a/go/testdata/beside/mesh-catalog/modules/jira/files/config.json b/go/testdata/beside/mesh-catalog/modules/jira/files/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/jira/files/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/kernel/files/huge-pages.sh b/go/testdata/beside/mesh-catalog/modules/kernel/files/huge-pages.sh new file mode 100644 index 0000000..e0a9815 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/kernel/files/huge-pages.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# Transparent huge pages as the mesh sets them on this machine (module kernel, novox/hq issue 746). +# Written by the mesh from this machine's setting transparent-huge-pages and run by +# mesh-kernel-huge-pages.service: `apply` at boot and whenever the setting changes, `restore` when the +# unit stops (the module unassigned, or the machine shutting down). Replaced on every push; change the +# setting, never this file. A change of the setting reloads the unit, which runs `apply` again: the mode +# moves straight to the new value and never passes back through the one the machine was found with. +# +# huge-pages apply|restore [ []] +# +# The two directories are arguments only so the module's tests can run this against a stand-in tree. +set -eu + +want='${config:transparent-huge-pages}' +sys=/sys/kernel/mm/transparent_hugepage +state='${dir:state}' +[ $# -ge 2 ] && sys=$2 +[ $# -ge 3 ] && state=$3 + +case "$want" in +always | madvise | never) ;; +*) + echo "the setting transparent-huge-pages is '$want'; the kernel takes always, madvise or never" >&2 + exit 2 + ;; +esac +[ -f "$sys/enabled" ] || { + echo "this kernel has no $sys/enabled: it was built without transparent huge pages, so there is nothing to set" >&2 + exit 1 +} + +# The mode in force is the word the kernel brackets: "always [madvise] never". A file holding one bare +# word (the stand-in tree of the tests) is read as that word. +mode() { sed -n -e 's/.*\[\([a-z]*\)\].*/\1/p' -e t -e 's/^ *\([a-z]*\) *$/\1/p' "$sys/enabled"; } + +[ $# -ge 1 ] || set -- usage +case "$1" in +apply) + now=$(mode) + # What the machine had before the mesh first set it, kept once, so `restore` gives it back. + [ -s "$state/found" ] || printf '%s\n' "$now" >"$state/found" + if [ "$now" = "$want" ]; then + echo "transparent huge pages: $want, as set" + exit 0 + fi + printf '%s\n' "$want" >"$sys/enabled" + got=$(mode) + [ "$got" = "$want" ] || { + echo "wrote $want to $sys/enabled and the kernel says $got" >&2 + exit 1 + } + echo "transparent huge pages: $now -> $want" + # A file folio already in the page cache keeps its size: the active journal files, mapped by journald + # and by every reader, stay in 2 MiB folios until they are archived (issue 746). Rotating starts new + # files that are mapped under the new mode. A failed rotation is said and does not fail the setting. + # At boot nothing is in huge folios yet, and the persistent journal opens only after this unit: no rotation + # while the service manager is still bringing the machine up. + booting=$(systemctl is-system-running 2>/dev/null || true) + case "$booting" in + initializing | starting) echo "the machine is still starting: nothing to rotate yet" ;; + *) + if [ "$want" != always ] && command -v journalctl >/dev/null 2>&1; then + journalctl --rotate || echo "the journal could not be rotated: its active files keep their huge pages until they are archived" >&2 + fi + ;; + esac + ;; +restore) + found=$(cat "$state/found" 2>/dev/null || true) + case "$found" in + always | madvise | never) + printf '%s\n' "$found" >"$sys/enabled" + echo "transparent huge pages: back to $found, as the machine had them" + ;; + *) echo "transparent huge pages: nothing recorded as found, left at $(mode)" ;; + esac + # Unassigned or shut down: nothing of the module is kept; the next start records what it finds again. + rm -f "$state/found" + ;; +*) + echo "usage: huge-pages apply|restore [ []]" >&2 + exit 2 + ;; +esac diff --git a/go/testdata/beside/mesh-catalog/modules/kernel/files/mesh-kernel-huge-pages.service b/go/testdata/beside/mesh-catalog/modules/kernel/files/mesh-kernel-huge-pages.service new file mode 100644 index 0000000..77a9059 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/kernel/files/mesh-kernel-huge-pages.service @@ -0,0 +1,25 @@ +[Unit] +Description=Transparent huge pages as the mesh sets them on this machine +# Module kernel, novox/hq issue 746. Starting this unit sets the mode from the setting +# transparent-huge-pages; reloading it (the setting changed) sets the new mode; stopping it gives back the +# mode the machine had before. It runs early in boot, before the services that fill the page cache. +DefaultDependencies=no +After=local-fs.target +Before=sysinit.target shutdown.target +Conflicts=shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=${dir:state}/huge-pages apply +ExecReload=${dir:state}/huge-pages apply +ExecStop=${dir:state}/huge-pages restore +# It writes one file under /sys and its own state, and asks the journal daemon to rotate over its socket. +ProtectSystem=strict +ReadWritePaths=${dir:state} +ProtectHome=yes +PrivateNetwork=yes +NoNewPrivileges=yes + +[Install] +WantedBy=sysinit.target diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/files/admin.env b/go/testdata/beside/mesh-catalog/modules/keycloak/files/admin.env new file mode 100644 index 0000000..3e52ae5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/files/admin.env @@ -0,0 +1,2 @@ +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=${secret:admin} diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/files/config.json b/go/testdata/beside/mesh-catalog/modules/keycloak/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/files/consumers.json b/go/testdata/beside/mesh-catalog/modules/keycloak/files/consumers.json new file mode 100644 index 0000000..b0af957 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/files/consumers.json @@ -0,0 +1 @@ +${consumers:oidc-client} diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/files/database.env b/go/testdata/beside/mesh-catalog/modules/keycloak/files/database.env new file mode 100644 index 0000000..454ee99 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/files/database.env @@ -0,0 +1,3 @@ +KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} +KC_DB_USERNAME=${bound:postgres-database:as} +KC_DB_PASSWORD=${secret:postgres-database} diff --git a/go/testdata/beside/mesh-catalog/modules/keycloak/files/hostname.env b/go/testdata/beside/mesh-catalog/modules/keycloak/files/hostname.env new file mode 100644 index 0000000..92f061a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/keycloak/files/hostname.env @@ -0,0 +1 @@ +KC_HOSTNAME=https://${bound:route:name} diff --git a/go/testdata/beside/mesh-catalog/modules/lab/files/lab.env b/go/testdata/beside/mesh-catalog/modules/lab/files/lab.env new file mode 100644 index 0000000..35aa1a2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/lab/files/lab.env @@ -0,0 +1,3 @@ +MESH_LAB_FORGE=${config:forge} +MESH_LAB_FORGE_BINDING=${dir:state}/package-registry.json +MESH_LAB_FORGE_SECRET=${dir:state}/package-registry.secret diff --git a/go/testdata/beside/mesh-catalog/modules/lemurs/files/config.toml b/go/testdata/beside/mesh-catalog/modules/lemurs/files/config.toml new file mode 100644 index 0000000..6e5667d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/lemurs/files/config.toml @@ -0,0 +1,369 @@ +# The login manager's configuration, written by the mesh (module lemurs, novox/hq ADR 0208). Replaced +# at every push; change the module instead. The structure is lemurs 0.4's own (the shipped file, with +# every option, as lemurs requires); what the mesh changes from it is marked "mesh:". +# +# The sessions offered are the executable files other modules place in the two scripts directories +# below (/etc/lemurs/wms for X, /etc/lemurs/wayland for Wayland), one per session module. A file there +# is named as the session is offered. Desktop entries that packages install (/usr/share/xsessions) are +# not offered: they start the window manager bare, skipping the session's start (~/.xinitrc), which is +# where the account's environment, the X resources and every module's session lines are. +# +# Lemurs configuration file. +# Contains all the customization options of lemurs. +# +# Note: that as of now you need to have all options in the selected +# configuration file. Otherwise Lemurs will not work. +# +# Colors: +# --------- +# There is a list of predefined colors. These include: +# - black +# - white +# - (dark) gray +# - (light) red +# - (light) blue +# - (light) green +# - (light) magenta +# - (light) cyan +# - (light) yellow +# - orange +# +# You can also utilize custom colors with hex color codes. +# "#87CEEB" will create a Sky Blue color. +# +# Note: If the color wasn't recognized, it will default to white. +# --------- +# +# Modifiers: +# --------- +# There is a number of modifiers you can use. These can be combined by +# delimiting them with a comma (e.g. "bold,italic"). The modifiers are: +# - bold +# - dim +# - italic +# - underlined +# - reverse +# - crossed out +# - hidden +# --------- +# + +# The tty which contains lemurs. This has to be mirrored in the lemurs.service +tty = 2 + +# Where to log the main lemurs control flow. +main_log_path = "/var/log/lemurs.log" + +# Where to log to for the client. The Client is the Desktop Environment or +# Window Manager for Xorg, the Compositor for Wayland and the Shell for TTY. +client_log_path = "/var/log/lemurs.client.log" + +# At which point to point the cache. If you want to disable the cache globally +# you can use `/dev/null`. +cache_path = "/var/cache/lemurs" + +# Disable all logging. This is overwritten by the `--no-log` flag. +do_log = true + +# The PAM service that should be used to login +pam_service = "lemurs" + +# Path to system shell that gets used to execute linux commands. In almost all +# cases, this should refer to a bash shell. +system_shell = "/bin/sh" + +# Initial state of the `PATH` environment variable. +initial_path = "/usr/local/sbin:/usr/local/bin:/usr/bin" + +# The type flag that will be appended to the shell that calls the session +# environment. This may depend on your shell. Options: +# - 'none'. Disables calling a login shell +# - 'short'. Produces the `-l` flag. Supported by most shells. +# - 'long'. This produces the `--login` flag and is suited for bash and zsh. +shell_login_flag = "short" + +# Focus behaviour of fields when Lemurs is initially started +# +# Possible values: +# - default: Initially focus on first non-cached value +# - no-focus: No initial focus +# - environment: Initially focus on the environment selector +# - username: Initially focus on the username field +# - password: Initially focus on the password field +focus_behaviour = "default" + +# General settings for background style +[background] + +# Control whether to render background widget or not +show_background = false + +[background.style] +# Allow to set the default background color for the login shell +color = "black" +# Settings for the background block's borders +show_border = true +border_color = "white" + +[power_controls] +# The margin between hints +hint_margin = 2 + +# There are no additional entries by default +entries = [] + +# Example +# Reboot to another os option +#[[power_controls.entries]] +## The text in the top-left to display how to reboot. +#hint = "Reboot to OS" +# +## The color and modifiers of the hint in the top-left corner +#hint_color = "dark gray" +#hint_modifiers = "" +# +## The key used to reboot. Possibilities are F1 to F12. +#key = "F3" +## The command that is executed when the key is pressed +#cmd = "efibootmgr -n0 && systemctl reboot -l" + + +# If you want to remove the base_entries +# base_entries = [] + +# Shutdown option +[[power_controls.base_entries]] +# The text in the top-left to display how to shutdown. +hint = "Shutdown" + +# The color and modifiers of the hint in the top-left corner +hint_color = "dark gray" +hint_modifiers = "" + +# The key used to shutdown. Possibilities are F1 to F12. +key = "F1" +# The command that is executed when the key is pressed +cmd = "systemctl poweroff -l" + +# Reboot option +[[power_controls.base_entries]] +# The text in the top-left to display how to reboot. +hint = "Reboot" + +# The color and modifiers of the hint in the top-left corner +hint_color = "dark gray" +hint_modifiers = "" + +# The key used to reboot. Possibilities are F1 to F12. +key = "F2" +# The command that is executed when the key is pressed +cmd = "systemctl reboot -l" + +# Setting for the selector of the desktop environment you are using. +[environment_switcher] +# Terms: +# --------- +# Movers: indicators which show which direction one can move whilst selecting +# the desktop environment +# Selected: The currently selected desktop environment. +# Neighbours: The adjacent desktop environment to the one current selected +# +# Visualisation: +# +# < i3 bspwm awesome > +# +# ^ ^ ^ ^ ^ +# | | | | | +# mover | selected | mover +# | | +# neighbour neighbour +# --------- +# + +# Control the visibility of the switcher +# Options: +# - "visible" - Always show the switcher [default] +# - "hidden" - Always hide the switcher +# - [key] - F1-F12 to be able to toggle the visibility +# mesh: hidden, as both workstations had it, but F3 shows it, so a second session can be chosen. +switcher_visibility = "F3" + +# The text in the top-left to display how to toggle the switcher. The text +# '%key%' will be replaced with the switcher_visibility key. This is not shown +# if switcher_visibility is set to "visible" or "hidden". +toggle_hint = "Switcher %key%" + +# The color and modifiers of the hint in the top-left corner +toggle_hint_color = "dark gray" +toggle_hint_modifiers = "" + + +# Show an option for the TTY shell when logging in as one of the environments. +# NOTE: it is always shown when no viable options are found. +include_tty_shell = false + +# Remember the selected environment after logging in for the next time +remember = true + +# Enables showing the movers +show_movers = true + +# Mover's color and modifiers whilst the selector is unfocused +mover_color = "dark gray" +mover_modifiers = "" + +# Mover's color and modifiers whilst the selector is focused +mover_color_focused = "orange" +mover_modifiers_focused = "bold" + +# The characters used to display the movers. Suggestions are: +# - "<" ">" +# - "<-" "->" +# - "<<" ">>" +# - "[" "]" +left_mover = "<" +right_mover = ">" + +# The margin between the movers and the neighbours or selected (depending on +# `show_neighbours`) +mover_margin = 1 + +# Enables showing the neighbours +show_neighbours = true + +# Neighbours' color and modifiers whilst the selector is unfocused +neighbour_color = "dark gray" +neighbour_modifiers = "" + +# Neighbours' color and modifiers whilst the selector is focused +neighbour_color_focused = "gray" +neighbour_modifiers_focused = "" + +# Margin between neighbours and selected +neighbour_margin = 1 + +# Selected's color and modifiers whilst the selector is unfocused +selected_color = "gray" +selected_modifiers = "underlined" + +# Selected's color and modifiers whilst the selector is focused +selected_color_focused = "white" +selected_modifiers_focused = "bold" + +# The length of the name of the desktop environment which is displayed. +max_display_length = 8 + +# The text used when no desktop environments are available +no_envs_text = "No environments..." + +# The color and modifiers of the 'no desktop environments available text' +# whilst the selector is unfocused +no_envs_color = "white" +no_envs_modifiers = "" + +# The color and modifiers of the 'no desktop environments available text' +# whilst the selector is focused +no_envs_color_focused = "red" +no_envs_modifiers_focused = "" + +[username_field] + +# Remember the username for the next time after a successful login attempt. +remember = true + +[username_field.style] +# Enables showing a title +show_title = true +# The text used within the title +title = "Login" + +# The title's color and modifiers whilst the username field is unfocused +title_color = "white" +content_color = "white" + +# The title's color and modifiers whilst the username field is focused +title_color_focused = "orange" +content_color_focused = "orange" + +# Enables showing the borders +show_border = true +# The borders' color and modifiers whilst the username field is unfocused +border_color = "white" +# The borders' color and modifiers whilst the username field is focused +border_color_focused = "orange" + +# Constrain the width of the username field +use_max_width = true +# The constraint of the username field's width +max_width = 48 + +[password_field] + +# The character used for replacement when typing a password. Leave empty for no +# feedback. +# Note: Only one character is accepted. +content_replacement_character = "*" + +[password_field.style] +# Enables showing a title +show_title = true +# The text used within the title +title = "Password" + +# The title's color and modifiers whilst the password field is unfocused +title_color = "white" +content_color = "white" + +# The title's color and modifiers whilst the password field is focused +title_color_focused = "orange" +content_color_focused = "orange" + +# Enables showing the borders +show_border = true +# The borders' color and modifiers whilst the password field is unfocused +border_color = "white" +# The borders' color and modifiers whilst the password field is focused +border_color_focused = "orange" + +# Constrain the width of the password field +use_max_width = true +# The constraint of the password field's width +max_width = 48 + +[x11] +# Where to log to for the XServer. +xserver_log_path = "/var/log/lemurs.xorg.log" + +# The value of the `DISPLAY` environment variable for X11 sessions +x11_display = ":1" + +# How many seconds to give the X server to start. To make it infinitely, put it +# to 0. +xserver_timeout_secs = 60 + +# Where to find the X11 server binary +# mesh: the server through the wrapper below, which adds -noreset. +xserver_path = "/etc/lemurs/xserver" + +# Where to find the X11 xauth binary +xauth_path = "/usr/bin/xauth" + +# Path to the directory where the startup scripts for the X11 sessions are found +scripts_path = "/etc/lemurs/wms" + +# Path to the xsetup script that is needed for the environment setup of the +# window manager. +xsetup_path = "/etc/lemurs/xsetup.sh" + +# The directory to use for desktop entries X11 sessions. +# mesh: an empty directory of the module's own, so no package's desktop entry is offered. +xsessions_path = "/etc/lemurs/xsessions" + +[wayland] +# Path to the directory where the startup scripts for the Wayland sessions are +# found +scripts_path = "/etc/lemurs/wayland" + +# The directory to use for desktop entries wayland sessions. +# mesh: likewise for Wayland. +wayland_sessions_path = "/etc/lemurs/wayland-sessions" diff --git a/go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver b/go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver new file mode 100644 index 0000000..5908f22 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver @@ -0,0 +1,8 @@ +#!/bin/sh +# The X server as lemurs starts it, written by the mesh (module lemurs). Replaced at every push. +# +# -noreset: without it, X resets whenever its last client disconnects, and a reset empties the +# resource database. The session's start merges the X resources with xrdb before any long-lived +# client is connected, so the server reset twice in the four seconds after a login and threw the +# resources away: xterm fell back to the bitmap "fixed" font, without the mesh's face and colours. +exec /usr/bin/X "$@" -noreset diff --git a/go/testdata/beside/mesh-catalog/modules/letta/files/config.json b/go/testdata/beside/mesh-catalog/modules/letta/files/config.json new file mode 100644 index 0000000..8ed994d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/letta/files/config.json @@ -0,0 +1,3 @@ +{ + "password": "${secret:server-password}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/letta/files/pgpass b/go/testdata/beside/mesh-catalog/modules/letta/files/pgpass new file mode 100644 index 0000000..cd41b66 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/letta/files/pgpass @@ -0,0 +1 @@ +*:*:*:${bound:postgres-database:as}:${secret:postgres-database} diff --git a/go/testdata/beside/mesh-catalog/modules/letta/files/server.env b/go/testdata/beside/mesh-catalog/modules/letta/files/server.env new file mode 100644 index 0000000..fe2acb9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/letta/files/server.env @@ -0,0 +1,6 @@ +LETTA_PG_URI=postgresql://${bound:postgres-database:as}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} +PGPASSFILE=/run/secrets/pgpass +LETTA_SERVER_PASSWORD=${secret:server-password} +OPENAI_API_KEY=${secret:openai-api-key} +SECURE=true +TZ=Europe/Brussels diff --git a/go/testdata/beside/mesh-catalog/modules/letta/files/start.sh b/go/testdata/beside/mesh-catalog/modules/letta/files/start.sh new file mode 100644 index 0000000..9955592 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/letta/files/start.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Generated by the mesh. Do not edit: module letta writes this file and replaces it at every push. +# +# letta 0.6.8 prints secrets it is given to its log (novox/hq issue 268): +# letta/server/rest_api/app.py prints its server password when it starts in secure mode; +# startup.sh, alembic/env.py and letta/server/server.py print LETTA_PG_URI whole. +# The URI carries no password (libpq reads it from PGPASSFILE), and the one print of the server +# password is rewritten before the server starts. Either one failing refuses the start: a letta +# that does not start says why here, and one that leaks says nothing. +set -e +app=/app/letta/server/rest_api/app.py +sed -i 's/Using secure mode with password: {random_password}/Using secure mode (the password is not printed)/' "$app" +if grep -q 'print(.*random_password' "$app"; then + echo "letta: $app still prints the server password; not starting (novox/hq issue 268)" >&2 + exit 1 +fi +case "$LETTA_PG_URI" in + *://*:*@*) + echo "letta: LETTA_PG_URI carries a password, and letta prints that URI; not starting (novox/hq issue 268)" >&2 + exit 1 + ;; +esac +exec ./letta/server/startup.sh diff --git a/go/testdata/beside/mesh-catalog/modules/local-model-consumer/files/openai.env b/go/testdata/beside/mesh-catalog/modules/local-model-consumer/files/openai.env new file mode 100644 index 0000000..9aca396 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/local-model-consumer/files/openai.env @@ -0,0 +1,3 @@ +OPENAI_BASE_URL=http://${bound:model-access:at}:${bound:model-access:port}/v1 +OPENAI_MODEL=${bound:model-access:model} +OPENAI_API_KEY=local diff --git a/go/testdata/beside/mesh-catalog/modules/localization/files/locale.conf b/go/testdata/beside/mesh-catalog/modules/localization/files/locale.conf new file mode 100644 index 0000000..fe405b5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/localization/files/locale.conf @@ -0,0 +1,3 @@ +# The mesh's (module localization, novox/hq to-be 42): the system locale. Written whole at every +# push; an edit here is overwritten. Read at the next login. +LANG=en_US.UTF-8 diff --git a/go/testdata/beside/mesh-catalog/modules/localization/files/vconsole.conf b/go/testdata/beside/mesh-catalog/modules/localization/files/vconsole.conf new file mode 100644 index 0000000..88d96f6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/localization/files/vconsole.conf @@ -0,0 +1,3 @@ +# The mesh's (module localization, novox/hq to-be 42): the console keymap. Written whole at every +# push; an edit here is overwritten. Read at the next boot. +KEYMAP=us diff --git a/go/testdata/beside/mesh-catalog/modules/logrotate/files/logrotate.conf b/go/testdata/beside/mesh-catalog/modules/logrotate/files/logrotate.conf new file mode 100644 index 0000000..d646137 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/logrotate/files/logrotate.conf @@ -0,0 +1,32 @@ +# The mesh's (module logrotate, novox/hq to-be 42): the base configuration every rotation inherits. +# Written whole at every push; an edit here is overwritten. Each package's own rules are in +# /etc/logrotate.d and stay the packages'. + +# Weekly, four weeks kept, a new empty log created after each rotation. +weekly +rotate 4 +create + +# Rotated logs are compressed, one rotation late, so a program still writing to the file it had open +# loses nothing to the compression. +compress +delaycompress + +# A package's replaced configuration is never read as a rule. +tabooext + .pacorig .pacnew .pacsave + +include /etc/logrotate.d + +/var/log/wtmp { + monthly + create 0664 root utmp + minsize 1M + rotate 1 +} + +/var/log/btmp { + missingok + monthly + create 0600 root utmp + rotate 1 +} diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/files/admin.env b/go/testdata/beside/mesh-catalog/modules/mailu/files/admin.env new file mode 100644 index 0000000..f066bb5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/files/admin.env @@ -0,0 +1,2 @@ +INITIAL_ADMIN_PW=${secret:admin} +API_TOKEN=${secret:api-token} diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/files/config.json b/go/testdata/beside/mesh-catalog/modules/mailu/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/files/database.env b/go/testdata/beside/mesh-catalog/modules/mailu/files/database.env new file mode 100644 index 0000000..ba70d3d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/files/database.env @@ -0,0 +1,5 @@ +DB_FLAVOR=postgresql +DB_HOST=${bound:postgres-database:at}:${bound:postgres-database:port} +DB_USER=${bound:postgres-database:as} +DB_NAME=${bound:postgres-database:as} +DB_PW=${secret:postgres-database} diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/files/mailu.env b/go/testdata/beside/mesh-catalog/modules/mailu/files/mailu.env new file mode 100644 index 0000000..2c977d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/files/mailu.env @@ -0,0 +1,78 @@ +ADMIN_ADDRESS=mailu-admin +ANTISPAM_ADDRESS=mailu-antispam +ANTIVIRUS_ADDRESS=mailu-antivirus +IMAP_ADDRESS=mailu-imap +SMTP_ADDRESS=mailu-smtp +FRONT_ADDRESS=mailu-front +WEBMAIL_ADDRESS=mailu-webmail +WEBDAV_ADDRESS=mailu-webdav +REDIS_ADDRESS=mailu-redis +PORTS=25,80,443,465,993,995,4190,110,143,587 +DOMAIN=${config:domain} +HOSTNAMES=${bound:route:name-web} +POSTMASTER=admin +SITENAME=${config:sitename} +WEBSITE=${config:website} +TLS_FLAVOR=letsencrypt +SUBNET=192.168.203.0/24 +COMPOSE_PROJECT_NAME=mailu +ANTIVIRUS=clamav +WEBMAIL=roundcube +WEBDAV=radicale +FETCHMAIL_ENABLED=True +FETCHMAIL_DELAY=600 +ADMIN=true +WEB_ADMIN=/admin +WEB_WEBMAIL=/webmail +WEBROOT_REDIRECT=/webmail +API=true +WEB_API=/api +AUTH_RATELIMIT_IP=6000/hour +AUTH_RATELIMIT_USER=1000/day +CREDENTIAL_ROUNDS=12 +PASSWORD_SCHEME=PBKDF2 +DISABLE_STATISTICS=True +MESSAGE_SIZE_LIMIT=50000000 +MESSAGE_RATELIMIT=200/day +RECIPIENT_DELIMITER=+ +POSTFIX_MYNETWORKS=127.0.0.0/8 [::1]/128 +RELAYNETS= +RELAYHOST= +REJECT_UNLISTED_RECIPIENT= +DB_FLAVOR=postgresql +INITIAL_ADMIN_ACCOUNT=admin +INITIAL_ADMIN_DOMAIN=${config:domain} +INITIAL_ADMIN_MODE=ifmissing +SMTP_PORT=25 +SMTPS_PORT=465 +SUBMISSION_PORT=587 +POP3_PORT=110 +POP3S_PORT=995 +IMAP_PORT=143 +IMAPS_PORT=993 +HTTP_PORT=7080 +HTTPS_PORT=7443 +AUTOMX_PORT=4243 +AMX_SMTP_ADDRESS=${bound:route:name-web} +AMX_SMTP_PORT=587 +AMX_IMAP_ADDRESS=${bound:route:name-web} +AMX_IMAP_PORT=143 +AMX_MAIL_DOMAINS=${config:domain} +DMARC_RUA=admin +DMARC_RUF=admin +LETSENCRYPT_SHORTCHAIN=True +TZ=Etc/UTC +LOG_LEVEL=INFO +WELCOME=false +REAL_IP_HEADER=X-Real-IP +REAL_IP_FROM=${config:proxy-address} +COMPRESSION= +COMPRESS_LEVEL= +COMPRESSION_LEVEL= +BIND_ADDRESS4=127.0.0.1 +BIND_ADDRESS6=::1 +MAILU_VERSION=1.9 +DOCKER_ORG=mailu +DOCKER_PREFIX= +WELCOME_SUBJECT=Welcome to your new email account +WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly! diff --git a/go/testdata/beside/mesh-catalog/modules/mailu/files/secret.env b/go/testdata/beside/mesh-catalog/modules/mailu/files/secret.env new file mode 100644 index 0000000..c6f6b66 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mailu/files/secret.env @@ -0,0 +1 @@ +SECRET_KEY=${secret:secret-key} From 781ba6aeb4467dbe1b23d75a6dba251dd0c7744d Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 08:20:05 +0000 Subject: [PATCH 4/9] Capture the catalogue modules' remaining files directories and templates at mesh-catalog bb0fe93b for the moved catalogue tests (issue 1379) Part 4 of 4 of go/testdata/beside. --- .../UPower.conf.d/50-asus-zephyrus-g14.conf | 11 + .../30-asus-zephyrus-g14-touchpad.conf | 12 + .../asus-zephyrus-g14/templates/after-wake | 7 + .../templates/display-session | 30 ++ .../templates/hotkeys-triggers | 10 + .../modprobe.d/g14-nvidia-power.conf | 16 ++ .../modprobe.d/video-brightness-switch.conf | 6 + .../asus-zephyrus-g14.conf | 7 + .../asus-zephyrus-g14-nvidia.conf | 7 + .../asus-zephyrus-g14-nvidia.conf | 7 + .../asus-zephyrus-g14-nvidia.conf | 7 + .../templates/udev/rules.d/90-backlight.rules | 5 + .../modules/matrix/files/conduit.toml | 15 + .../modules/matrix/files/element.json | 53 ++++ .../modules/memory-pressure/files/oomd.conf | 8 + .../memory-pressure/files/root-slice.conf | 5 + .../modules/memory-pressure/files/sysctl.conf | 9 + .../memory-pressure/files/user-manager.conf | 7 + .../memory-pressure/files/zram-generator.conf | 11 + .../modules/mesh-catalog/files/database.url | 1 + .../modules/mesh-cli/files/zsh-normal | 3 + .../modules/mesh-issues/files/settings.json | 147 ++++++++++ .../modules/mesh-knowledge/files/database.url | 1 + .../mesh-knowledge/files/settings.json | 5 + .../modules/mesh-mail-web/files/oidc.json | 5 + .../mesh-mail-web/files/operators.json | 3 + .../modules/mesh-mail/files/accounts.json | 3 + .../modules/mesh-mail/files/database-url | 1 + .../modules/mesh-mail/files/grants.json | 3 + .../modules/mesh-mail/files/store.json | 9 + .../modules/mesh-watcher/files/settings.json | 3 + .../modules/messenger/files/settings.json | 4 + .../mesh-catalog/modules/minio/files/root.env | 2 + .../modules/model-usage/files/database.url | 1 + .../modules/mongodb/files/server-root.secret | 1 + .../modules/mosquitto/files/bootstrap.env | 7 + .../modules/mosquitto/files/mosquitto.conf | 20 ++ .../modules/mounts/files/mounts.conf | 11 + .../modules/mssql/files/backup.sql | 14 + .../mesh-catalog/modules/mssql/files/sa.env | 2 + .../modules/n8n/files/n8n-database.secret | 1 + .../mesh-catalog/modules/n8n/files/server.env | 12 + .../mesh-catalog/modules/nats/files/nats.conf | 46 +++ .../modules/networkmanager/files/50-mesh.conf | 63 +++++ .../modules/nextcloud/files/config.json | 1 + .../modules/nextcloud/files/server.env | 14 + .../modules/nfs-server/files/50-mesh.conf | 12 + .../modules/nfs-server/files/shares.conf | 10 + .../nftables/files/mesh-filter.service | 14 + .../modules/nftables/files/mesh.conf | 5 + .../modules/node-env/files/50-mesh.conf | 4 + .../modules/node-env/files/environment.sh | 5 + .../modules/nodered/files/config.json | 3 + .../modules/nodered/files/mqtt.env | 4 + .../modules/nodered/files/settings.js | 48 ++++ .../modules/nodered/files/settings.json | 14 + .../modules/only-office/files/server.env | 5 + .../modules/pacman/files/pacman.conf | 29 ++ .../modules/pacman/files/reflector.conf | 8 + .../modules/picom/files/picom.conf | 38 +++ .../polychromatic/files/display-session | 6 + .../modules/postgres/files/consumers.json | 1 + .../modules/power/files/bin/power-moment | 90 ++++++ .../modules/power/templates/bar-battery | 9 + .../modules/power/templates/bar-power-draw | 6 + .../templates/mesh-power/moments/after-boot | 4 + .../templates/mesh-power/moments/after-wake | 4 + .../mesh-power/moments/before-shutdown | 4 + .../templates/mesh-power/moments/before-sleep | 4 + .../templates/mesh-power/moments/on-battery | 4 + .../templates/mesh-power/moments/on-mains | 4 + .../systemd/logind.conf.d/power.conf | 8 + .../system/hibernate.target.d/mesh-power.conf | 3 + .../hybrid-sleep.target.d/mesh-power.conf | 3 + .../system/mesh-power-after-boot.service | 13 + .../system/mesh-power-after-wake.service | 11 + .../system/mesh-power-before-shutdown.service | 18 ++ .../system/mesh-power-before-sleep.service | 11 + .../systemd/system/mesh-power-supply.service | 10 + .../system/sleep.target.d/mesh-power.conf | 3 + .../mesh-power.conf | 3 + .../system/suspend.target.d/mesh-power.conf | 3 + .../udev/rules.d/90-mesh-power.rules | 5 + .../modules/powerlevel10k/files/zsh-normal | 3 + .../modules/records/files/config.json | 1 + .../mesh-catalog/modules/records/files/origin | 1 + .../modules/redis/files/redis.conf | 3 + .../modules/restic/files/backups.conf | 2 + .../modules/restic/files/data.conf | 2 + .../modules/restic/files/retention.conf | 6 + .../mesh-catalog/modules/rofi/files/bin/dmenu | 26 ++ .../modules/rofi/files/bin/rofi-launch | 39 +++ .../modules/rofi/files/bin/rofi-powermenu | 54 ++++ .../modules/rofi/files/config.rasi | 54 ++++ .../modules/rofi/files/i3/50-rofi.conf | 8 + .../rofi/files/themes/mesh-powermenu.rasi | 172 ++++++++++++ .../modules/rofi/files/themes/mesh.rasi | 263 ++++++++++++++++++ .../modules/route-adapter/files/config.json | 6 + .../modules/screen-lock/files/bin/screen-lock | 57 ++++ .../modules/screen-lock/files/xinitrc-normal | 8 + .../modules/searxng/files/config.json | 1 + .../modules/searxng/files/settings.yml | 16 ++ .../modules/showcase/files/README.md | 6 + .../modules/showcase/files/marker.txt | 1 + .../modules/showcase/templates/showcase.env | 5 + .../modules/slack/files/display-session | 11 + .../modules/ssh-client/files/config | 6 + .../modules/ssh-client/files/forge | 7 + .../modules/ssh-client/files/gitconfig | 2 + .../modules/ssh-client/files/known_hosts | 1 + .../modules/sshd/files/10-mesh.conf | 10 + .../modules/step-ca/files/config.json | 1 + .../modules/step-ca/files/init.env | 2 + .../modules/sudo/files/10-mesh-operator | 9 + .../modules/supabase/files/analytics-env.env | 14 + .../supabase/files/analytics-reconcile.exs | 86 ++++++ .../modules/supabase/files/analytics-start.sh | 41 +++ .../modules/supabase/files/db-init/jwt.sql | 2 + .../modules/supabase/files/db-init/logs.sql | 6 + .../modules/supabase/files/db-init/pooler.sql | 6 + .../supabase/files/db-init/realtime.sql | 4 + .../modules/supabase/files/db-init/roles.sql | 7 + .../supabase/files/db-init/supabase.sql | 3 + .../supabase/files/db-init/webhooks.sql | 208 ++++++++++++++ .../modules/supabase/files/functions-env.env | 6 + .../supabase/files/functions-main/index.ts | 94 +++++++ .../modules/supabase/files/gotrue.env | 28 ++ .../modules/supabase/files/kong.yml | 242 ++++++++++++++++ .../modules/supabase/files/meta-env.env | 6 + .../modules/supabase/files/pooler-env.env | 14 + .../modules/supabase/files/pooler.exs | 43 +++ .../modules/supabase/files/postgrest.conf | 7 + .../modules/supabase/files/realtime-env.env | 16 ++ .../modules/supabase/files/settings.json | 7 + .../modules/supabase/files/storage-env.env | 13 + .../modules/supabase/files/studio-env.env | 18 ++ .../modules/supabase/files/vector.yml | 258 +++++++++++++++++ .../systemd-networkd/files/00-mesh0.network | 35 +++ .../modules/time-sync/files/50-mesh.conf | 6 + .../modules/triggerhappy/files/service.conf | 8 + .../modules/triggerhappy/files/triggers.conf | 4 + .../modules/umami/files/server.env | 3 + .../modules/unifi/files/config.json | 4 + .../modules/xdg/files/mimeapps.list | 29 ++ .../modules/xdg/files/user-dirs.conf | 9 + .../modules/xdg/files/user-dirs.dirs | 17 ++ .../modules/xdg/files/user-dirs.locale | 1 + .../mesh-catalog/modules/xorg/files/xinitrc | 51 ++++ .../modules/xorg/files/xresources | 13 + .../modules/xterm/files/env-terminal | 1 + .../modules/xterm/files/xresources-normal | 31 +++ .../mesh-catalog/modules/zfs/files/scrub.conf | 6 + .../zsh-autosuggestions/files/zsh-normal | 2 + .../zsh-syntax-highlighting/files/zsh-last | 3 + .../mesh-catalog/modules/zsh/files/env-editor | 1 + .../modules/zsh/files/env-path-local-bin | 1 + .../modules/zsh/files/env-path-scripts | 1 + .../modules/zsh/files/env-path-scripts-bin | 1 + .../mesh-catalog/modules/zsh/files/env-visual | 1 + .../modules/zsh/files/env-xdg-config-home | 1 + .../mesh-catalog/modules/zsh/files/tools.conf | 3 + .../mesh-catalog/modules/zsh/files/zshenv | 9 + .../mesh-catalog/modules/zsh/files/zshrc | 60 ++++ 163 files changed, 3255 insertions(+) create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/after-wake create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/display-session create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/hotkeys-triggers create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/g14-nvidia-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/video-brightness-switch.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/udev/rules.d/90-backlight.rules create mode 100644 go/testdata/beside/mesh-catalog/modules/matrix/files/conduit.toml create mode 100644 go/testdata/beside/mesh-catalog/modules/matrix/files/element.json create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/files/oomd.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/files/root-slice.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/files/sysctl.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/files/user-manager.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/memory-pressure/files/zram-generator.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-catalog/files/database.url create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-cli/files/zsh-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-issues/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/database.url create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/oidc.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/operators.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail/files/accounts.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail/files/database-url create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail/files/grants.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-mail/files/store.json create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-watcher/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/minio/files/root.env create mode 100644 go/testdata/beside/mesh-catalog/modules/model-usage/files/database.url create mode 100644 go/testdata/beside/mesh-catalog/modules/mongodb/files/server-root.secret create mode 100644 go/testdata/beside/mesh-catalog/modules/mosquitto/files/bootstrap.env create mode 100644 go/testdata/beside/mesh-catalog/modules/mosquitto/files/mosquitto.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/mounts/files/mounts.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/mssql/files/backup.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/mssql/files/sa.env create mode 100644 go/testdata/beside/mesh-catalog/modules/n8n/files/n8n-database.secret create mode 100644 go/testdata/beside/mesh-catalog/modules/n8n/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/nats/files/nats.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/networkmanager/files/50-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/nextcloud/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/nextcloud/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/nfs-server/files/50-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/nfs-server/files/shares.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/nftables/files/mesh-filter.service create mode 100644 go/testdata/beside/mesh-catalog/modules/nftables/files/mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/node-env/files/50-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/node-env/files/environment.sh create mode 100644 go/testdata/beside/mesh-catalog/modules/nodered/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/nodered/files/mqtt.env create mode 100644 go/testdata/beside/mesh-catalog/modules/nodered/files/settings.js create mode 100644 go/testdata/beside/mesh-catalog/modules/nodered/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/only-office/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/pacman/files/pacman.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/pacman/files/reflector.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/picom/files/picom.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/polychromatic/files/display-session create mode 100644 go/testdata/beside/mesh-catalog/modules/postgres/files/consumers.json create mode 100644 go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/bar-battery create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/bar-power-draw create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-boot create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-wake create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-shutdown create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-sleep create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-battery create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-mains create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/logind.conf.d/power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hibernate.target.d/mesh-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hybrid-sleep.target.d/mesh-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-boot.service create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-wake.service create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-shutdown.service create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-sleep.service create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-supply.service create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/sleep.target.d/mesh-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend.target.d/mesh-power.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/power/templates/udev/rules.d/90-mesh-power.rules create mode 100644 go/testdata/beside/mesh-catalog/modules/powerlevel10k/files/zsh-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/records/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/records/files/origin create mode 100644 go/testdata/beside/mesh-catalog/modules/redis/files/redis.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/restic/files/backups.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/restic/files/data.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/restic/files/retention.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/config.rasi create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/i3/50-rofi.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh-powermenu.rasi create mode 100644 go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh.rasi create mode 100644 go/testdata/beside/mesh-catalog/modules/route-adapter/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock create mode 100644 go/testdata/beside/mesh-catalog/modules/screen-lock/files/xinitrc-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/searxng/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/searxng/files/settings.yml create mode 100644 go/testdata/beside/mesh-catalog/modules/showcase/files/README.md create mode 100644 go/testdata/beside/mesh-catalog/modules/showcase/files/marker.txt create mode 100644 go/testdata/beside/mesh-catalog/modules/showcase/templates/showcase.env create mode 100644 go/testdata/beside/mesh-catalog/modules/slack/files/display-session create mode 100644 go/testdata/beside/mesh-catalog/modules/ssh-client/files/config create mode 100644 go/testdata/beside/mesh-catalog/modules/ssh-client/files/forge create mode 100644 go/testdata/beside/mesh-catalog/modules/ssh-client/files/gitconfig create mode 100644 go/testdata/beside/mesh-catalog/modules/ssh-client/files/known_hosts create mode 100644 go/testdata/beside/mesh-catalog/modules/sshd/files/10-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/step-ca/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/step-ca/files/init.env create mode 100644 go/testdata/beside/mesh-catalog/modules/sudo/files/10-mesh-operator create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-reconcile.exs create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-start.sh create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/jwt.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/logs.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/pooler.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/realtime.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/roles.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/supabase.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/webhooks.sql create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/functions-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/functions-main/index.ts create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/gotrue.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/kong.yml create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/meta-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/pooler-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/pooler.exs create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/postgrest.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/realtime-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/settings.json create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/storage-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/studio-env.env create mode 100644 go/testdata/beside/mesh-catalog/modules/supabase/files/vector.yml create mode 100644 go/testdata/beside/mesh-catalog/modules/systemd-networkd/files/00-mesh0.network create mode 100644 go/testdata/beside/mesh-catalog/modules/time-sync/files/50-mesh.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/triggerhappy/files/service.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/triggerhappy/files/triggers.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/umami/files/server.env create mode 100644 go/testdata/beside/mesh-catalog/modules/unifi/files/config.json create mode 100644 go/testdata/beside/mesh-catalog/modules/xdg/files/mimeapps.list create mode 100644 go/testdata/beside/mesh-catalog/modules/xdg/files/user-dirs.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/xdg/files/user-dirs.dirs create mode 100644 go/testdata/beside/mesh-catalog/modules/xdg/files/user-dirs.locale create mode 100644 go/testdata/beside/mesh-catalog/modules/xorg/files/xinitrc create mode 100644 go/testdata/beside/mesh-catalog/modules/xorg/files/xresources create mode 100644 go/testdata/beside/mesh-catalog/modules/xterm/files/env-terminal create mode 100644 go/testdata/beside/mesh-catalog/modules/xterm/files/xresources-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/zfs/files/scrub.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh-autosuggestions/files/zsh-normal create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh-syntax-highlighting/files/zsh-last create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-editor create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-path-local-bin create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-path-scripts create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-path-scripts-bin create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-visual create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/env-xdg-config-home create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/tools.conf create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/zshenv create mode 100644 go/testdata/beside/mesh-catalog/modules/zsh/files/zshrc diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf new file mode 100644 index 0000000..f3d6bef --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf @@ -0,0 +1,11 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# On low battery the machine suspends rather than powering off, at 7 % — s2idle still draws a little, +# so it leaves headroom. A drop-in over the package's own UPower.conf, which stays the package's. +[UPower] +UsePercentageForPolicy=true +PercentageLow=15.0 +PercentageCritical=10.0 +PercentageAction=7.0 +CriticalPowerAction=Suspend +AllowRiskyCriticalPowerAction=true diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf new file mode 100644 index 0000000..f30830c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf @@ -0,0 +1,12 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The touchpad's settings, applied by X every time the device appears — at login and after every +# resume, when the device is initialised again. This replaces the predecessor's sleep hook, which ran +# xinput after a resume as a named person on a guessed display. +Section "InputClass" + Identifier "asus-zephyrus-g14 touchpad" + MatchIsTouchpad "on" + Option "Tapping" "on" + Option "NaturalScrolling" "true" + Option "AccelSpeed" "0.15" +EndSection diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/after-wake b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/after-wake new file mode 100644 index 0000000..af60a9d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/after-wake @@ -0,0 +1,7 @@ +# The touchpad's settings again after waking, in the operator's session: X applies the module's +# input class when the device appears, and this covers a wake that does not initialise it again. +# Runs as root from the power module; the reset itself runs as the session's owner. +sleep 2 +owner=$(ps -o user= -C i3 | head -n 1) +[ -n "$owner" ] && runuser -u "$owner" -- /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset +true diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/display-session b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/display-session new file mode 100644 index 0000000..38f9e5b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/display-session @@ -0,0 +1,30 @@ +# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the +# mesh: replaced at every push. Once the controller places contributions to node-display-session +# (ADR 0210), these become the module's contribution instead of a file in i3's directory. +# +# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are +# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for +# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's. + +# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the +# screenshot grabs the pointer to select a region, which fails while the key is still held. +bindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh + +# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the +# external output. +bindsym $mod+p exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display order + +# The keyboard backlight's level, shown when it changes. +exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-kbd-notify +# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the +# mesh: replaced at every push. Once the controller places contributions to node-display-session +# (ADR 0210), these become the module's contribution instead of a file in i3's directory. +# +# The model's panel and touchpad. + +# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where +# is the display server's (autorandr, run at every session start). +exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display primary + +# The touchpad's settings again, by hand. X applies them itself whenever the device appears. +bindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/hotkeys-triggers b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/hotkeys-triggers new file mode 100644 index 0000000..5e4d165 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/hotkeys-triggers @@ -0,0 +1,10 @@ +# The ROG Zephyrus G14's vendor keys, which reach no X client: media (the M-keys), panel brightness, +# and the touchpad key. Each runs this module's own script, as the operator's account. +KEY_PROG1 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media play-pause +KEY_PROG3 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media previous +KEY_PROG4 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media next +KEY_BRIGHTNESSDOWN 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight - +KEY_BRIGHTNESSDOWN 2 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight - +KEY_BRIGHTNESSUP 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight + +KEY_BRIGHTNESSUP 2 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight + +KEY_F21 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/g14-nvidia-power.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/g14-nvidia-power.conf new file mode 100644 index 0000000..7894102 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/g14-nvidia-power.conf @@ -0,0 +1,16 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The discrete GPU's driver options on the ROG Zephyrus G14 (GA403, RTX 40 series, hybrid graphics). +# +# NVreg_DynamicPowerManagement=0x00 turns runtime D3 off. With it on, a change of power source sends +# the driver an ACPI notification it fails to handle on this model ("RmHandleDNotifierEvent: Failed to +# handle ACPI D-Notifier event, status=0x62"), and the GPU stops making progress until the machine is +# powered off. Off costs a few idle watts in hybrid mode and keeps the machine up. +# +# NVreg_PreserveVideoMemoryAllocations=1 saves video memory across suspend, so what used the GPU still +# works after waking. It needs nvidia-suspend, -hibernate and -resume to run around a sleep, which this +# module's drop-ins on the sleep services ask for. +# +# A change here applies when the driver next loads: at the next boot. +options nvidia NVreg_PreserveVideoMemoryAllocations=1 +options nvidia NVreg_DynamicPowerManagement=0x00 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/video-brightness-switch.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/video-brightness-switch.conf new file mode 100644 index 0000000..5b2de1d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/modprobe.d/video-brightness-switch.conf @@ -0,0 +1,6 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The ACPI video driver does not change the backlight itself on the brightness keys: on this model it +# moves the wrong one. The keys are triggerhappy's (see /etc/triggerhappy/triggers.d/asus-g14.conf). +# Applies when the module next loads: at the next boot. +options video brightness_switch_enabled=0 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf new file mode 100644 index 0000000..4466dd0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf @@ -0,0 +1,7 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# nvidia-powerd (Dynamic Boost) was the first error in the chain that hung this model's GPU on a change +# of power source, and asusd starts it on mains. It runs only when the kernel command line says +# zephyrus.nvidia-powerd — an explicit opt-in, at boot. +[Unit] +ConditionKernelCommandLine=zephyrus.nvidia-powerd diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf new file mode 100644 index 0000000..1f56e3b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf @@ -0,0 +1,7 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as +# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service +# shape must not start these units by hand, which would put the GPU to sleep with the machine awake. +[Unit] +Wants=nvidia-hibernate.service nvidia-resume.service diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf new file mode 100644 index 0000000..5813188 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf @@ -0,0 +1,7 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as +# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service +# shape must not start these units by hand, which would put the GPU to sleep with the machine awake. +[Unit] +Wants=nvidia-suspend-then-hibernate.service nvidia-resume.service diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf new file mode 100644 index 0000000..92a0111 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf @@ -0,0 +1,7 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as +# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service +# shape must not start these units by hand, which would put the GPU to sleep with the machine awake. +[Unit] +Wants=nvidia-suspend.service nvidia-resume.service diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/udev/rules.d/90-backlight.rules b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/udev/rules.d/90-backlight.rules new file mode 100644 index 0000000..0a9a894 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/templates/udev/rules.d/90-backlight.rules @@ -0,0 +1,5 @@ +# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead. +# +# The backlights are writable by the video group, so the brightness keys and the module's brightness tool +# move the panel without root. +ACTION=="add", SUBSYSTEM=="backlight", RUN+="/usr/bin/chgrp video /sys/class/backlight/%k/brightness", RUN+="/usr/bin/chmod g+w /sys/class/backlight/%k/brightness" diff --git a/go/testdata/beside/mesh-catalog/modules/matrix/files/conduit.toml b/go/testdata/beside/mesh-catalog/modules/matrix/files/conduit.toml new file mode 100644 index 0000000..9b72401 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/matrix/files/conduit.toml @@ -0,0 +1,15 @@ +# Written by the mesh (modules/matrix). Conduit reads this file (CONDUIT_CONFIG); nothing comes +# from the environment. server_name is the homeserver's permanent identity: every user id, room id +# and signature in the database carries it, so it is the name this module is served under +# (${bound:route:name-homeserver}) and never changes once a database exists. +[global] +server_name = "${bound:route:name-homeserver}" +database_backend = "rocksdb" +database_path = "/var/lib/matrix-conduit/" +address = "0.0.0.0" +port = 6167 +max_request_size = 20000000 +allow_registration = false +allow_federation = true +allow_check_for_updates = true +trusted_servers = ["matrix.org"] diff --git a/go/testdata/beside/mesh-catalog/modules/matrix/files/element.json b/go/testdata/beside/mesh-catalog/modules/matrix/files/element.json new file mode 100644 index 0000000..305a89f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/matrix/files/element.json @@ -0,0 +1,53 @@ +{ + "default_server_name": "${bound:route:name-homeserver}", + "default_server_config": { + "m.homeserver": { + "base_url": "https://${bound:route:name-homeserver}" + }, + "m.identity_server": { + "base_url": "https://vector.im" + } + }, + "brand": "Element", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "integrations_widgets_urls": [ + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ], + "bug_report_endpoint_url": "https://element.io/bugreports/submit", + "uisi_autorageshake_app": "element-auto-uisi", + "show_labs_settings": true, + "room_directory": { + "servers": [ + "${bound:route:name-homeserver}", + "matrix.org", + "gitter.im", + "libera.chat" + ] + }, + "enable_presence_by_hs_url": { + "https://matrix.org": false, + "https://matrix-client.matrix.org": false + }, + "terms_and_conditions_links": [ + { + "url": "https://element.io/privacy", + "text": "Privacy Policy" + }, + { + "url": "https://element.io/cookie-policy", + "text": "Cookie Policy" + } + ], + "features": { + "feature_video_rooms": true, + "feature_rust_crypto": true + }, + "element_call": { + "url": "https://call.element.dev" + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/oomd.conf b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/oomd.conf new file mode 100644 index 0000000..39af6de --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/oomd.conf @@ -0,0 +1,8 @@ +# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead. +# +# systemd-oomd kills the worst unit before the kernel's OOM killer freezes the machine: when swap is +# 90 % used, or when a watched unit stalls on memory for 20 s above its limit. +[OOM] +SwapUsedLimit=90% +DefaultMemoryPressureLimit=60% +DefaultMemoryPressureDurationSec=20s diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/root-slice.conf b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/root-slice.conf new file mode 100644 index 0000000..edae3de --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/root-slice.conf @@ -0,0 +1,5 @@ +# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead. +# +# systemd-oomd may act on swap exhaustion across the whole machine. +[Slice] +ManagedOOMSwap=kill diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/sysctl.conf b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/sysctl.conf new file mode 100644 index 0000000..678251c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/sysctl.conf @@ -0,0 +1,9 @@ +# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead. +# +# Swap-in reads one page, not eight: read-ahead exists to amortise a disk's seek, and compressed swap in +# RAM has none — the speculation only costs decompression and memory. +vm.page-cluster = 0 +# +# vm.swappiness is deliberately left alone. The usual zram advice (150-180) holds only while the +# compressed swap has room; once it is full, a higher swappiness moves pages to the disk swap, the thing +# being avoided. Raise it only together with zram-size. diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/user-manager.conf b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/user-manager.conf new file mode 100644 index 0000000..8ef300c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/user-manager.conf @@ -0,0 +1,7 @@ +# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead. +# +# systemd-oomd may kill the worst unit in a person's service manager when its memory pressure stays +# above 80 % — instead of the kernel freezing the machine. +[Service] +ManagedOOMMemoryPressure=kill +ManagedOOMMemoryPressureLimit=80% diff --git a/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/zram-generator.conf b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/zram-generator.conf new file mode 100644 index 0000000..cccfb05 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/memory-pressure/files/zram-generator.conf @@ -0,0 +1,11 @@ +# Managed by the mesh (module memory-pressure). Replaced on every push; edit the catalogue instead. +# +# Compressed swap in RAM: fast, at a higher priority than any swap on disk, so it takes the everyday +# pressure before anything spills to a disk. Half the RAM, at most 16 GiB, compressed with zstd. +# The swap on disk, its size and whether one exists at all are the machine's, not this module's. +# +# Read by the generator at boot: a change applies at the next boot. +[zram0] +zram-size = min(ram / 2, 16384) +compression-algorithm = zstd +swap-priority = 100 diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-catalog/files/database.url b/go/testdata/beside/mesh-catalog/modules/mesh-catalog/files/database.url new file mode 100644 index 0000000..dc59da9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-catalog/files/database.url @@ -0,0 +1 @@ +postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-cli/files/zsh-normal b/go/testdata/beside/mesh-catalog/modules/mesh-cli/files/zsh-normal new file mode 100644 index 0000000..117f2a5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-cli/files/zsh-normal @@ -0,0 +1,3 @@ +# The operator's command (module mesh-cli, novox/hq ADR 0272): nox is mesh-cli, in interactive zsh only. +# A script, sudo and a document meant to work everywhere say mesh-cli. +alias nox=mesh-cli diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-issues/files/settings.json b/go/testdata/beside/mesh-catalog/modules/mesh-issues/files/settings.json new file mode 100644 index 0000000..66abd38 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-issues/files/settings.json @@ -0,0 +1,147 @@ +{ + "first-number": 365, + "owners": [ + "hq", + "mesh-catalog", + "mesh-catalog modules/adwaita", + "mesh-catalog modules/artifact-store-tools", + "mesh-catalog modules/asus-zephyrus-g14", + "mesh-catalog modules/audit-logger", + "mesh-catalog modules/avahi", + "mesh-catalog modules/baserow", + "mesh-catalog modules/betterbird", + "mesh-catalog modules/blueman", + "mesh-catalog modules/bluetooth", + "mesh-catalog modules/ca-trust", + "mesh-catalog modules/claude-code", + "mesh-catalog modules/claude-licence-manager", + "mesh-catalog modules/clipmenu", + "mesh-catalog modules/confluence", + "mesh-catalog modules/cups", + "mesh-catalog modules/dbus", + "mesh-catalog modules/desk-channel", + "mesh-catalog modules/disk-load", + "mesh-catalog modules/distribution", + "mesh-catalog modules/dmenu", + "mesh-catalog modules/dnsmasq", + "mesh-catalog modules/docker", + "mesh-catalog modules/docker-compose", + "mesh-catalog modules/dunst", + "mesh-catalog modules/fail2ban", + "mesh-catalog modules/feh", + "mesh-catalog modules/flatpak", + "mesh-catalog modules/fonts", + "mesh-catalog modules/forticlient", + "mesh-catalog modules/gitea", + "mesh-catalog modules/gitlab", + "mesh-catalog modules/gnome-keyring", + "mesh-catalog modules/grafana", + "mesh-catalog modules/hello-web", + "mesh-catalog modules/home-assistant", + "mesh-catalog modules/hostname", + "mesh-catalog modules/i3", + "mesh-catalog modules/i3status-rust", + "mesh-catalog modules/icecast", + "mesh-catalog modules/influxdb", + "mesh-catalog modules/invoicing", + "mesh-catalog modules/jira", + "mesh-catalog modules/keycloak", + "mesh-catalog modules/lab", + "mesh-catalog modules/lemurs", + "mesh-catalog modules/letta", + "mesh-catalog modules/local-model-consumer", + "mesh-catalog modules/localization", + "mesh-catalog modules/logrotate", + "mesh-catalog modules/mailu", + "mesh-catalog modules/marrytts", + "mesh-catalog modules/matrix", + "mesh-catalog modules/memory-pressure", + "mesh-catalog modules/mesh-catalog", + "mesh-catalog modules/mesh-cli", + "mesh-catalog modules/mesh-delivery", + "mesh-catalog modules/mesh-delivery-gate", + "mesh-catalog modules/mesh-issues", + "mesh-catalog modules/mesh-vault", + "mesh-catalog modules/mesh-watcher", + "mesh-catalog modules/messenger", + "mesh-catalog modules/minio", + "mesh-catalog modules/model-usage", + "mesh-catalog modules/mongodb", + "mesh-catalog modules/mosquitto", + "mesh-catalog modules/mounts", + "mesh-catalog modules/mssql", + "mesh-catalog modules/n8n", + "mesh-catalog modules/nats", + "mesh-catalog modules/netcheck", + "mesh-catalog modules/networkmanager", + "mesh-catalog modules/nextcloud", + "mesh-catalog modules/nextcloud-client", + "mesh-catalog modules/nfs-server", + "mesh-catalog modules/nftables", + "mesh-catalog modules/nm-applet", + "mesh-catalog modules/node-env", + "mesh-catalog modules/nodered", + "mesh-catalog modules/ollama", + "mesh-catalog modules/only-office", + "mesh-catalog modules/openai-consumer", + "mesh-catalog modules/openrazer", + "mesh-catalog modules/pacman", + "mesh-catalog modules/photos-eef", + "mesh-catalog modules/photos-filip", + "mesh-catalog modules/picom", + "mesh-catalog modules/polychromatic", + "mesh-catalog modules/postgres", + "mesh-catalog modules/power", + "mesh-catalog modules/powerlevel10k", + "mesh-catalog modules/records", + "mesh-catalog modules/redis", + "mesh-catalog modules/restic", + "mesh-catalog modules/rofi", + "mesh-catalog modules/route-adapter", + "mesh-catalog modules/screen-lock", + "mesh-catalog modules/searxng", + "mesh-catalog modules/sensors", + "mesh-catalog modules/showcase", + "mesh-catalog modules/slack", + "mesh-catalog modules/snapd", + "mesh-catalog modules/ssh-client", + "mesh-catalog modules/sshd", + "mesh-catalog modules/step-ca", + "mesh-catalog modules/sudo", + "mesh-catalog modules/supabase", + "mesh-catalog modules/systemd", + "mesh-catalog modules/systemd-networkd", + "mesh-catalog modules/systemd-resolved", + "mesh-catalog modules/telegram", + "mesh-catalog modules/time-sync", + "mesh-catalog modules/triggerhappy", + "mesh-catalog modules/umami", + "mesh-catalog modules/unifi", + "mesh-catalog modules/website", + "mesh-catalog modules/xclip", + "mesh-catalog modules/xdg", + "mesh-catalog modules/xorg", + "mesh-catalog modules/xterm", + "mesh-catalog modules/zfs", + "mesh-catalog modules/zsh", + "mesh-catalog modules/zsh-autosuggestions", + "mesh-catalog modules/zsh-syntax-highlighting", + "mesh-controller", + "mesh-controller modules/build-agent", + "mesh-controller modules/route-proxy", + "mesh-delivery", + "mesh-host", + "mesh-lab", + "mesh-media-catalog", + "mesh-sdk", + "mesh-tools" + ], + "duplicate-check": "on", + "judge-model": "sonnet", + "forge-owner": "novox", + "stuck-after": "2h", + "worker-stale-after": "24h", + "delivered-not-resolved-after": "1h", + "decision-floor": 293, + "research-floor": 45 +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/database.url b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/database.url new file mode 100644 index 0000000..dc59da9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/database.url @@ -0,0 +1 @@ +postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/settings.json b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/settings.json new file mode 100644 index 0000000..6f682fe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-knowledge/files/settings.json @@ -0,0 +1,5 @@ +{ + "judge-model": "sonnet", + "drafting": "on", + "reserved-for": "168h" +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/oidc.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/oidc.json new file mode 100644 index 0000000..f864608 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/oidc.json @@ -0,0 +1,5 @@ +{ + "client-id": "${bound:oidc-client:as}", + "client-secret": "${secret:oidc-client}", + "issuer": "${bound:oidc-client:issuer}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/operators.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/operators.json new file mode 100644 index 0000000..dea781c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/files/operators.json @@ -0,0 +1,3 @@ +{ + "operators": [] +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/accounts.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/accounts.json new file mode 100644 index 0000000..e182cc3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/accounts.json @@ -0,0 +1,3 @@ +{ + "accounts": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/database-url b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/database-url new file mode 100644 index 0000000..dc59da9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/database-url @@ -0,0 +1 @@ +postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/grants.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/grants.json new file mode 100644 index 0000000..458eabb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/grants.json @@ -0,0 +1,3 @@ +{ + "grants": {} +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/store.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/store.json new file mode 100644 index 0000000..8c2075d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/files/store.json @@ -0,0 +1,9 @@ +{ + "scheme": "${bound:s3-bucket:scheme}", + "at": "${bound:s3-bucket:at}", + "port": "${bound:s3-bucket:port}", + "bucket": "${bound:s3-bucket:bucket}", + "region": "${bound:s3-bucket:region}", + "key": "${bound:s3-bucket:as}", + "secret": "${secret:s3-bucket}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-watcher/files/settings.json b/go/testdata/beside/mesh-catalog/modules/mesh-watcher/files/settings.json new file mode 100644 index 0000000..84b9ecc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-watcher/files/settings.json @@ -0,0 +1,3 @@ +{ + "telegram-chat-id": "" +} diff --git a/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json b/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json new file mode 100644 index 0000000..ce76334 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json @@ -0,0 +1,4 @@ +{ + "time-zone": "", + "only-what-needs-me": [] +} diff --git a/go/testdata/beside/mesh-catalog/modules/minio/files/root.env b/go/testdata/beside/mesh-catalog/modules/minio/files/root.env new file mode 100644 index 0000000..2255260 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/minio/files/root.env @@ -0,0 +1,2 @@ +MINIO_ROOT_USER=meshroot +MINIO_BROWSER_REDIRECT_URL=https://${bound:route:name-console} diff --git a/go/testdata/beside/mesh-catalog/modules/model-usage/files/database.url b/go/testdata/beside/mesh-catalog/modules/model-usage/files/database.url new file mode 100644 index 0000000..dc59da9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/model-usage/files/database.url @@ -0,0 +1 @@ +postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} diff --git a/go/testdata/beside/mesh-catalog/modules/mongodb/files/server-root.secret b/go/testdata/beside/mesh-catalog/modules/mongodb/files/server-root.secret new file mode 100644 index 0000000..0ccabee --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mongodb/files/server-root.secret @@ -0,0 +1 @@ +${secret:root} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/mosquitto/files/bootstrap.env b/go/testdata/beside/mesh-catalog/modules/mosquitto/files/bootstrap.env new file mode 100644 index 0000000..65b710e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mosquitto/files/bootstrap.env @@ -0,0 +1,7 @@ +MESH_PROVISION_MQTT=127.0.0.1:${port:1883} +MESH_PROVISION_ADMIN_USER=mesh-admin +MESH_PROVISION_PASSWORD_FILE=${dir:mesh-state}/admin +MESH_DYNSEC_FILE=${dir:data}/dynamic-security.json +MESH_MQTT_CTRL_IMAGE=eclipse-mosquitto@sha256:38c0da4f2ef84284d47b3b3eeea1cb3bdeabe81ee10caf0cd5c5ff61ee3ea408 +MESH_MQTT_CTRL_CONTAINER=mosquitto +MESH_MQTT_ADMIN_APPLIED_FILE=${dir:state}/admin.applied diff --git a/go/testdata/beside/mesh-catalog/modules/mosquitto/files/mosquitto.conf b/go/testdata/beside/mesh-catalog/modules/mosquitto/files/mosquitto.conf new file mode 100644 index 0000000..500c2a6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mosquitto/files/mosquitto.conf @@ -0,0 +1,20 @@ +persistence true +persistence_location /mosquitto/data + +log_dest stdout +log_type warning +log_type error +log_type notice + +# Every client authenticates; identities and their per-topic ACLs are managed +# at runtime by the dynamic security plugin, whose store the plugin itself owns. +allow_anonymous false +plugin /usr/lib/mosquitto_dynamic_security.so +plugin_opt_config_file /mosquitto/data/dynamic-security.json + +# MQTT listener +listener 1883 + +# MQTT-over-WebSockets listener +listener 8081 +protocol websockets diff --git a/go/testdata/beside/mesh-catalog/modules/mounts/files/mounts.conf b/go/testdata/beside/mesh-catalog/modules/mounts/files/mounts.conf new file mode 100644 index 0000000..1d5f87d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mounts/files/mounts.conf @@ -0,0 +1,11 @@ +# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings +# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this. +# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry. +shares=${config:shares} +sources=${config:sources} +server=${bound:nfs-share:from} +at=${bound:nfs-share:at} +account=${machine:account} +node=${machine:name} +passwords=${dir:state} +state=${dir:state} diff --git a/go/testdata/beside/mesh-catalog/modules/mssql/files/backup.sql b/go/testdata/beside/mesh-catalog/modules/mssql/files/backup.sql new file mode 100644 index 0000000..fd2ecce --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mssql/files/backup.sql @@ -0,0 +1,14 @@ +SET NOCOUNT ON; +DECLARE @n sysname, @s nvarchar(max); +DECLARE c CURSOR LOCAL FAST_FORWARD FOR + SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL; +OPEN c; +FETCH NEXT FROM c INTO @n; +WHILE @@FETCH_STATUS = 0 +BEGIN + SET @s = N'BACKUP DATABASE ' + QUOTENAME(@n) + N' TO DISK = N''/var/opt/mssql/backup/' + REPLACE(@n, N'''', N'''''') + N'.bak'' WITH INIT, COPY_ONLY, CHECKSUM'; + EXEC (@s); + FETCH NEXT FROM c INTO @n; +END +CLOSE c; +DEALLOCATE c; diff --git a/go/testdata/beside/mesh-catalog/modules/mssql/files/sa.env b/go/testdata/beside/mesh-catalog/modules/mssql/files/sa.env new file mode 100644 index 0000000..e7dd444 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mssql/files/sa.env @@ -0,0 +1,2 @@ +ACCEPT_EULA=Y +MSSQL_SA_PASSWORD=${secret:sa} diff --git a/go/testdata/beside/mesh-catalog/modules/n8n/files/n8n-database.secret b/go/testdata/beside/mesh-catalog/modules/n8n/files/n8n-database.secret new file mode 100644 index 0000000..e811e88 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/n8n/files/n8n-database.secret @@ -0,0 +1 @@ +${secret:postgres-database} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/n8n/files/server.env b/go/testdata/beside/mesh-catalog/modules/n8n/files/server.env new file mode 100644 index 0000000..2f74e0f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/n8n/files/server.env @@ -0,0 +1,12 @@ +N8N_HOST=${bound:route:name} +N8N_PORT=5678 +N8N_PROTOCOL=https +WEBHOOK_URL=https://${bound:route:name}/ +NODE_FUNCTION_ALLOW_BUILTIN=* +NODE_FUNCTION_ALLOW_EXTERNAL=* +DB_TYPE=postgresdb +DB_POSTGRESDB_HOST=${bound:postgres-database:at} +DB_POSTGRESDB_PORT=${bound:postgres-database:port} +DB_POSTGRESDB_DATABASE=${bound:postgres-database:as} +DB_POSTGRESDB_USER=${bound:postgres-database:as} +DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/database diff --git a/go/testdata/beside/mesh-catalog/modules/nats/files/nats.conf b/go/testdata/beside/mesh-catalog/modules/nats/files/nats.conf new file mode 100644 index 0000000..e1d33eb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nats/files/nats.conf @@ -0,0 +1,46 @@ +# The nats module's own server settings. Declared by the module, because a port, a TLS path +# and a store directory are properties of the container this module raises: they live in its +# image and its mounts and change when it does. +# +# The mesh writes accounts.conf beside this one and nothing else. A controller that wrote the +# whole file would have to be kept in step with a Dockerfile it never sees. + +port: 4222 +# Monitoring on every interface *inside* the container, so the publish below can reach it; the publish +# is 127.0.0.1:8222 on the machine, so nothing beyond the machine reaches it, and the module guards 8222 +# too. Bound to the container's own loopback until 2026-10-04, the published port answered nothing +# (connection reset), and the only way in was `docker exec`. +http: 0.0.0.0:8222 + +# The mesh's own broker certificate — the one every machine already pins by fingerprint and the +# controller already trusts (MESH_BROKER_CERTIFICATE). Serving the new bus with it means no +# machine's pin changes when it moves, and no second certificate exists to be wrong about. +tls { + cert_file: "/tls/tls.crt" + key_file: "/tls/tls.key" +} + +# **No `verify`, deliberately, and it was `verify: true` until a probe ran this image.** That +# setting makes the server demand a *client* certificate, and nothing in the mesh presents one: a +# host pins this server's exact certificate and authenticates with the password the mesh minted +# (novox/hq ADR 0004, design 25 §4), and so does a module's runtime. With it on, every connection +# in the mesh is refused at the TLS handshake, before any password is looked at — and the error is +# "client didn't provide a certificate", which reads as a client fault. +# +# TLS is still required: a tls block is what makes it required, and verify only decides whether +# client certificates are checked. What is given up is a second factor the mesh has no machinery +# to issue or rotate — a certificate per module per node — and what is kept is stronger than a +# name check in both directions: an exact pin outward, a per-user password inward. + +jetstream { + store_dir: "/data" +} + +# Every user of the mesh, composed by the controller and rewritten whenever a module is +# assigned, a node enrols or a person's access changes. +# +# **Relative, and in this same directory, because it has to be.** An absolute include path is +# resolved relative to the including file's directory, not from the root: nats-server given +# `include /etc/nats/accounts.conf` from /etc/nats-server/nats.conf looks for +# /etc/nats-server/etc/nats/accounts.conf and refuses to start. Verified against the server. +include accounts.conf diff --git a/go/testdata/beside/mesh-catalog/modules/networkmanager/files/50-mesh.conf b/go/testdata/beside/mesh-catalog/modules/networkmanager/files/50-mesh.conf new file mode 100644 index 0000000..f8c8e8a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/networkmanager/files/50-mesh.conf @@ -0,0 +1,63 @@ +# Managed by the mesh (module networkmanager). Replaced on every push; edit the +# catalogue instead. +# +# This machine's uplink is NetworkManager's, and this file is the whole of what +# the mesh asks of it (novox/hq ADR 0117): leave the resolver file to the mesh, +# and leave the private network's interface alone. Nothing more. The mesh never +# declares a connection profile, an address, a route, a wireless network or its +# credentials — those are joined at the machine, by the person using it, and +# the link they make is the only channel the mesh reaches this machine over. A +# push that got a link wrong could not be undone by the next one. +# +# A drop-in of the mesh's own, beside NetworkManager.conf and whatever else the +# operator keeps in this directory. NetworkManager reads the files here sorted by +# name and a later one wins a key it sets again — so a file of the operator's +# that sorts after this one (any name starting with a letter does) and sets dns= +# or unmanaged-devices= overrides it. That is the operator's to decide, and the +# reason this file sets nothing but the two keys it must. +# +# NetworkManager itself is the machine's: the mesh never starts, stops, enables +# or disables it (its service is declared with no state), because stopping it +# takes every link down, this machine's channel to the mesh included — and a +# module unassigned by mistake must not be able to do that. When this file +# changes, a running NetworkManager is reloaded (its D-Bus Reload call, which +# re-reads its configuration — NetworkManager(8)), never restarted. + +[main] +# The resolver file is the mesh's: this module writes /etc/resolv.conf itself, +# listing the mesh's resolvers (novox/hq ADR 0223). Without this line +# NetworkManager rewrites that file on +# every connectivity change — every network joined, every lease renewed — +# and the mesh's resolver is silently replaced while every surface of the mesh +# still reads green. none: "NetworkManager will not modify resolv.conf. This +# implies rc-manager unmanaged" (NetworkManager.conf(5), 1.58). On an adopted +# machine the predecessor wrote the same line in a file of its own; both say one +# thing, and the predecessor's is retired by hand after the take. +# +# Not NetworkManager's own global DNS ([global-dns-domain-*] with rc-manager=file): +# it writes its own header and composes the options line itself, so it cannot +# write the mesh's file byte for byte, and the three uplink modules would write +# three different files for one fact. dns=none, and the file declared beside it. +dns=none + +[keyfile] +# mesh0 is the private network's interface: the mesh brings it up and the mesh +# alone configures it. A manager that considers every interface its own could +# try to configure it, or tear it down on a profile change. +# +# unmanaged-devices rather than a [device-mesh0] section with managed=0, because +# NetworkManager.conf(5) says a device unmanaged by this key "is strictly +# unmanaged and cannot be overruled by using the API like nmcli device set +# $IFNAME managed yes", while device*.managed "can be overruled at runtime via +# D-Bus". The same page adds that device*.managed "may be a better choice" for +# exactly those reasons — for an interface the operator might want to hand back +# at runtime. For the mesh's own interface, strict is the point. +# +# += rather than =: the same page documents appending to a list-valued key set +# earlier ("plugins+=another-plugin") as an extension of its key file format, +# and unmanaged-devices is a device list. = would replace whatever devices the +# operator already keeps NetworkManager away from; += adds this one to them +# (novox/hq ADR 0102: a list is added to, never replaced). A file of the +# operator's read after this one that sets the key with = replaces it again; +# that is the operator's to decide. +unmanaged-devices+=interface-name:mesh0 diff --git a/go/testdata/beside/mesh-catalog/modules/nextcloud/files/config.json b/go/testdata/beside/mesh-catalog/modules/nextcloud/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nextcloud/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/nextcloud/files/server.env b/go/testdata/beside/mesh-catalog/modules/nextcloud/files/server.env new file mode 100644 index 0000000..df6e20f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nextcloud/files/server.env @@ -0,0 +1,14 @@ +POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port} +POSTGRES_DB=${bound:postgres-database:as} +POSTGRES_USER=${bound:postgres-database:as} +POSTGRES_PASSWORD=${secret:postgres-database} +NEXTCLOUD_ADMIN_USER=mesh-admin +NEXTCLOUD_ADMIN_PASSWORD=${secret:admin} +OBJECTSTORE_S3_HOST=${bound:s3-bucket:at} +OBJECTSTORE_S3_PORT=${bound:s3-bucket:port} +OBJECTSTORE_S3_BUCKET=${bound:s3-bucket:bucket} +OBJECTSTORE_S3_KEY=${bound:s3-bucket:as} +OBJECTSTORE_S3_SECRET=${secret:s3-bucket} +OBJECTSTORE_S3_SSL=false +OBJECTSTORE_S3_USEPATH_STYLE=true +OBJECTSTORE_S3_REGION=${bound:s3-bucket:region} diff --git a/go/testdata/beside/mesh-catalog/modules/nfs-server/files/50-mesh.conf b/go/testdata/beside/mesh-catalog/modules/nfs-server/files/50-mesh.conf new file mode 100644 index 0000000..2640a8b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nfs-server/files/50-mesh.conf @@ -0,0 +1,12 @@ +# Written by the mesh (module nfs-server, novox/hq ADR 0263). Replaced on every push; a drop-in of +# the operator's that sorts after this one overrides it, and is theirs. +# +# NFS version 4 only: a client needs port 2049 and nothing else, so the module opens nothing more +# than that, to the private network. Version 3 needs rpcbind and mountd, on ports the mesh does not open. +[nfsd] +vers2=n +vers3=n +vers4=y +vers4.0=n +vers4.1=y +vers4.2=y diff --git a/go/testdata/beside/mesh-catalog/modules/nfs-server/files/shares.conf b/go/testdata/beside/mesh-catalog/modules/nfs-server/files/shares.conf new file mode 100644 index 0000000..f25f026 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nfs-server/files/shares.conf @@ -0,0 +1,10 @@ +# Written by the mesh (module nfs-server, novox/hq ADR 0263) from this machine's assignment. +# Replaced on every push; change the `shares` setting, never this file. +# +# The shares: name=folder, or name=folder:ro, one share per folder. The grants: share=node:rw|ro[,…], +# which nodes may mount each and how. The module's process exports a share to a node's private address +# only when it is granted here and that node's mounts module wants it, every client mapped to the +# folder's owner, and only to addresses inside the range below. +shares=${config:shares} +grants=${config:grants} +range=${machine:mesh-range} diff --git a/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh-filter.service b/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh-filter.service new file mode 100644 index 0000000..5a6cced --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh-filter.service @@ -0,0 +1,14 @@ +[Unit] +Description=The mesh's packet filter, derived from what is assigned to this node +Wants=network-pre.target +Before=network-pre.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=nft -f /etc/nftables.conf +ExecReload=nft -f /etc/nftables.conf +ExecStop=nft delete table inet mesh + +[Install] +WantedBy=multi-user.target diff --git a/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh.conf b/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh.conf new file mode 100644 index 0000000..f1a7bb0 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nftables/files/mesh.conf @@ -0,0 +1,5 @@ +# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset +# (novox/hq ADR 0100) — a flush would take the container runtime's rules and any firewall with it. +[Service] +ExecStop= +ExecStop=nft delete table inet mesh diff --git a/go/testdata/beside/mesh-catalog/modules/node-env/files/50-mesh.conf b/go/testdata/beside/mesh-catalog/modules/node-env/files/50-mesh.conf new file mode 100644 index 0000000..3aebc87 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/node-env/files/50-mesh.conf @@ -0,0 +1,4 @@ +# The operator account's environment for its service manager and graphical session, generated by +# the mesh (module node-env, novox/hq ADR 0203). Do not edit: this file is replaced at every push. +# The same facts as ~/.config/mesh/environment.sh, in environment.d(5) syntax. +${collected:environment:systemd} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/node-env/files/environment.sh b/go/testdata/beside/mesh-catalog/modules/node-env/files/environment.sh new file mode 100644 index 0000000..c28adf3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/node-env/files/environment.sh @@ -0,0 +1,5 @@ +# The operator account's environment, generated by the mesh (module node-env, novox/hq ADR 0203). +# Do not edit: this file is replaced at every push. Every line names the module that contributed it. +# Sourced by the login shell from its always-read startup file (for zsh, ~/.zshenv), so a script, a +# login and the shell's execute verb all see it. Your own variables belong in your shell's own lines. +${collected:environment:posix} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/nodered/files/config.json b/go/testdata/beside/mesh-catalog/modules/nodered/files/config.json new file mode 100644 index 0000000..a396fbd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodered/files/config.json @@ -0,0 +1,3 @@ +{ + "token": "${secret:api-token}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/nodered/files/mqtt.env b/go/testdata/beside/mesh-catalog/modules/nodered/files/mqtt.env new file mode 100644 index 0000000..5331f3c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodered/files/mqtt.env @@ -0,0 +1,4 @@ +MESH_NODERED_URL=http://127.0.0.1:${port:1880} +MESH_NODERED_CONFIG_FILE=${dir:mesh-state}/config.json +MESH_PROVISIONS_DIR=${dir:state} +MESH_WRITTEN_DIR=${dir:written} diff --git a/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.js b/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.js new file mode 100644 index 0000000..0d0f351 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.js @@ -0,0 +1,48 @@ +// Node-RED's settings, written by the mesh from the nodered module. What an assignment may change +// is settings.json beside this file (merged key by key); the credentials are the mesh's secrets and +// reach Node-RED only through this file. The flows' own credentials stay encrypted in the user +// directory under the key Node-RED keeps there (.config.runtime.json), which is data, not this. +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +const ADMIN_PASSWORD = "${secret:admin}"; +const API_TOKEN = "${secret:api-token}"; +const ADMIN = { username: "admin", permissions: "*" }; + +const settings = JSON.parse(fs.readFileSync(path.join(__dirname, "settings.json"), "utf8")); +// The mesh's keys, not Node-RED's: endpoints lands in every merged file; timeZone is the +// assignment's way to set the zone flows schedule and format in; mqtt names the broker nodes the +// module's MQTT step keeps pointed at the mesh's broker; topics is what nodered asks the broker for. +if (settings.timeZone) process.env.TZ = settings.timeZone; +delete settings.timeZone; +delete settings.endpoints; +delete settings.mqtt; +delete settings.topics; + +function same(a, b) { + const x = crypto.createHash("sha256").update(String(a)).digest(); + const y = crypto.createHash("sha256").update(String(b)).digest(); + return crypto.timingSafeEqual(x, y); +} + +// The admin secret is a password, or, accepted from an existing install, the bcrypt hash its +// settings held, so the password people already use keeps working. +function passwordMatches(given) { + if (/^\$2[aby]\$\d\d\$/.test(ADMIN_PASSWORD)) return require("bcryptjs").compare(String(given), ADMIN_PASSWORD); + return Promise.resolve(same(given, ADMIN_PASSWORD)); +} + +module.exports = Object.assign(settings, { + uiPort: 1880, + adminAuth: { + type: "credentials", + users: (username) => Promise.resolve(username === ADMIN.username ? ADMIN : null), + authenticate: (username, password) => + username === ADMIN.username + ? passwordMatches(password).then((ok) => (ok ? ADMIN : null)) + : Promise.resolve(null), + // The module's own tools call the admin API with this bearer token. + tokens: (token) => Promise.resolve(same(token, API_TOKEN) ? { username: "mesh", permissions: "*" } : null), + }, +}); diff --git a/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.json b/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.json new file mode 100644 index 0000000..93cf157 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodered/files/settings.json @@ -0,0 +1,14 @@ +{ + "flowFile": "flows.json", + "flowFilePretty": true, + "diagnostics": { "enabled": true, "ui": true }, + "runtimeState": { "enabled": false, "ui": false }, + "logging": { "console": { "level": "info", "metrics": false, "audit": false } }, + "exportGlobalContextKeys": false, + "externalModules": {}, + "editorTheme": { "projects": { "enabled": false } }, + "functionExternalModules": true, + "debugMaxLength": 1000, + "mqttReconnectTime": 15000, + "serialReconnectTime": 15000 +} diff --git a/go/testdata/beside/mesh-catalog/modules/only-office/files/server.env b/go/testdata/beside/mesh-catalog/modules/only-office/files/server.env new file mode 100644 index 0000000..19fab11 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/only-office/files/server.env @@ -0,0 +1,5 @@ +JWT_ENABLED=true +JWT_SECRET=${secret:jwt} +JWT_HEADER=Authorization +JWT_IN_BODY=true +ALLOW_PRIVATE_IP_ADDRESS=true diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/files/pacman.conf b/go/testdata/beside/mesh-catalog/modules/pacman/files/pacman.conf new file mode 100644 index 0000000..0390e25 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/pacman/files/pacman.conf @@ -0,0 +1,29 @@ +# The mesh's (module pacman, novox/hq to-be 42): the package manager's configuration. Written +# whole at every push: an edit here is overwritten, and the file a machine had before is kept once by +# the host. Owned whole because [options] cannot take a block by appending: anything added at the end +# of the file lands in the last repository's section. +# +# The repositories are the union of what the machines had enabled when the module was written +# (core, extra, multilib). The options are the distribution's defaults with four more: colour on a +# terminal, parallel downloads, package lists in columns, and downloads run as the unprivileged +# alpm user, which pacman 7 creates. + +[options] +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +Color +VerbosePkgLists +ParallelDownloads = 5 +DownloadUser = alpm +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[multilib] +Include = /etc/pacman.d/mirrorlist diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/files/reflector.conf b/go/testdata/beside/mesh-catalog/modules/pacman/files/reflector.conf new file mode 100644 index 0000000..eb9e731 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/pacman/files/reflector.conf @@ -0,0 +1,8 @@ +# The mesh's (module pacman, novox/hq to-be 42): how reflector refreshes the mirror list, weekly, +# through reflector.timer. Written whole at every push. Before the module, every machine's list was +# generated once and never again. +--save /etc/pacman.d/mirrorlist +--protocol https +--country Belgium,Netherlands,Luxembourg,Germany,France +--latest 20 +--sort rate diff --git a/go/testdata/beside/mesh-catalog/modules/picom/files/picom.conf b/go/testdata/beside/mesh-catalog/modules/picom/files/picom.conf new file mode 100644 index 0000000..c6b8555 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/picom/files/picom.conf @@ -0,0 +1,38 @@ +# picom, the X compositor (module picom, novox/hq ADR 0208). Owned by the mesh: this file is +# replaced at every push. Adopted from the two workstations' file of 2026-10-04 and moved to +# picom's window rules, which supersede opacity-rule, inactive-opacity and inactive-dim. + +# The backend and damage tracking are each machine's settings (backend, use-damage; novox/hq research +# 037, issue 345): the right ones depend on the machine's graphics driver. The defaults are what every +# machine ran before: glx, and picom's own default of repainting only what changed. xrender, or +# use-damage false (repaint the whole screen each frame), is what to try on a machine where a window is +# left unpainted or stale. +backend = "${config:backend}"; +use-damage = ${config:use-damage}; +vsync = true; + +fading = true; +fade-in-step = 0.05; +fade-out-step = 0.05; + +# Tiled windows hide their shadows and corners, so neither is drawn. +shadow = false; +corner-radius = 0; +blur-method = "none"; + +# "Focused" is i3's _NET_ACTIVE_WINDOW, not X focus events: under i3 those do not reliably reach +# the toplevel picom tracks, and a terminal then never lifts to its focused opacity. +use-ewmh-active-win = true; + +# Window rules are applied in order, and a later match wins. Only terminals are translucent: +# nothing else on screen (browsers, video, games) is touched. A terminal is matched by its class; +# the node's terminal emulator today is xterm (class XTerm). A window's own opacity property is +# used wherever no rule sets one, which is what the window-opacity tool sets. +rules = ( + { match = "window_type = 'tooltip'"; fade = false; opacity = 0.95; }, + { match = "window_type = 'dock' || window_type = 'desktop'"; fade = false; }, + { match = "class_g = 'XTerm' && focused"; opacity = 0.95; }, + { match = "class_g = 'XTerm' && !focused"; opacity = 0.75; }, + # A full-screen window is opaque, a terminal included: a video or a game is never see-through. + { match = "fullscreen"; opacity = 1; } +); diff --git a/go/testdata/beside/mesh-catalog/modules/polychromatic/files/display-session b/go/testdata/beside/mesh-catalog/modules/polychromatic/files/display-session new file mode 100644 index 0000000..bbdbd6f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/polychromatic/files/display-session @@ -0,0 +1,6 @@ +# The Razer peripherals' tray (module polychromatic, novox/hq ADR 0208, ADR 0212). Owned by the mesh: +# replaced at every push. This line is the tray's one start, at the session's start (exec, not +# exec_always, so a reload starts nothing). The package's own login helper starts it too while the +# application's setting "Start the tray applet when I log on" is ticked; polychromatic_check names +# that as a second start. +exec --no-startup-id polychromatic-tray-applet diff --git a/go/testdata/beside/mesh-catalog/modules/postgres/files/consumers.json b/go/testdata/beside/mesh-catalog/modules/postgres/files/consumers.json new file mode 100644 index 0000000..30d160e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/postgres/files/consumers.json @@ -0,0 +1 @@ +${consumers:postgres-database} diff --git a/go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment b/go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment new file mode 100644 index 0000000..e7095e4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment @@ -0,0 +1,90 @@ +#!/bin/sh +# power-moment MOMENT [--force] — run every module's code for one power moment (novox/hq ADR 0211). +# Shipped by the mesh's power module; edit the catalogue. +# +# MOMENT is after-boot, before-sleep, after-wake, before-shutdown, on-mains or on-battery, or +# `supply`, which reads the machine's power source and runs on-mains or on-battery once per change. +# +# The moment's file holds what the controller placed: each module's code under a line `# `. +# Each module's piece runs on its own, with sh, as root, bounded by MESH_POWER_BOUND seconds +# (default 30) and killed 5 s after that, so one module that hangs cannot hold the machine awake or +# stop the next module. Every outcome goes to the journal under the tag mesh-power. +set -u + +moment=${1:-} +force=${2:-} +moments=/etc/mesh-power/moments +bound=${MESH_POWER_BOUND:-30} +runtime=/run/mesh-power + +say() { logger -t mesh-power -- "$*" 2>/dev/null || true; printf '%s\n' "$*"; } + +# The power source as the kernel says it: on mains when any Mains supply is online. A machine with +# no Mains supply (a desktop, a server, a USB-C source only) has no source to change. +source_now() { + found=no + for s in /sys/class/power_supply/*; do + [ "$(cat "$s/type" 2>/dev/null)" = Mains ] || continue + found=yes + [ "$(cat "$s/online" 2>/dev/null)" = 1 ] && { echo on-mains; return; } + done + [ "$found" = yes ] && echo on-battery || echo none +} + +case "$moment" in +supply) + now=$(source_now) + [ "$now" = none ] && exit 0 + mkdir -p "$runtime" + last=$(cat "$runtime/source" 2>/dev/null || true) + [ "$now" = "$last" ] && [ "$force" != --force ] && exit 0 + echo "$now" >"$runtime/source" + moment=$now + ;; +after-boot | before-sleep | after-wake | on-mains | on-battery) ;; +before-shutdown) + # Run only while the machine is really going down. The unit that runs it is stopped at shutdown, + # and also when the module is taken off the machine; the second must not run anyone's shutdown code. + state=$(systemctl is-system-running 2>/dev/null || true) + if [ "$state" != stopping ] && [ "$force" != --force ]; then + say "moment=before-shutdown skipped: the system is $state, not stopping" + exit 0 + fi + ;; +*) + echo "power-moment: no moment $moment (after-boot, before-sleep, after-wake, before-shutdown, on-mains, on-battery, supply)" >&2 + exit 2 + ;; +esac + +file="$moments/$moment" +[ -r "$file" ] || { say "moment=$moment: nothing placed"; exit 0; } + +work=$(mktemp -d) || exit 1 +trap 'rm -rf "$work"' EXIT + +# One file per module piece, in the order the controller placed them. A line that is exactly +# "# " opens a piece; lines before the first are the holder's own header. +awk -v dir="$work" ' + /^# [a-z0-9][a-z0-9-]*$/ { n++; name = substr($0, 3); out = sprintf("%s/%03d-%s", dir, n, name); next } + n > 0 { print > out } +' "$file" + +ran=0 failed=0 +for piece in "$work"/*; do + [ -e "$piece" ] || continue + module=${piece##*/} + module=${module#*-} + start=$(date +%s) + timeout --kill-after=5 "$bound" sh "$piece" "$piece.out" 2>&1 + rc=$? + took=$(($(date +%s) - start)) + ran=$((ran + 1)) + case $rc in + 0) say "moment=$moment module=$module ok in ${took}s" ;; + 124 | 137) failed=$((failed + 1)); say "moment=$moment module=$module ended after ${bound}s: it did not finish" ;; + *) failed=$((failed + 1)); say "moment=$moment module=$module exited $rc: $(tail -n 3 "$piece.out" | tr '\n' ' ')" ;; + esac +done +say "moment=$moment: $ran piece(s), $failed failed" +exit 0 diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/bar-battery b/go/testdata/beside/mesh-catalog/modules/power/templates/bar-battery new file mode 100644 index 0000000..d2386ba --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/bar-battery @@ -0,0 +1,9 @@ +[[block]] +block = "battery" +format = "$icon $percentage{ $time|}" +charging_format = "$icon $percentage{ $time|}" +not_charging_format = "$icon $percentage" +full_format = "$icon $percentage" +empty_format = "$icon $percentage" +missing_format = "" + diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/bar-power-draw b/go/testdata/beside/mesh-catalog/modules/power/templates/bar-power-draw new file mode 100644 index 0000000..3d0d338 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/bar-power-draw @@ -0,0 +1,6 @@ +[[block]] +block = "custom" +command = "~/.local/bin/i3status-value power.draw ${machine:name}" +interval = 5 +hide_when_empty = true + diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-boot b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-boot new file mode 100644 index 0000000..c5a90f6 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-boot @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment after-boot, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:after-boot:first}${shell:after-boot:normal}${shell:after-boot:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-wake b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-wake new file mode 100644 index 0000000..2655c22 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/after-wake @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment after-wake, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:after-wake:first}${shell:after-wake:normal}${shell:after-wake:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-shutdown b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-shutdown new file mode 100644 index 0000000..65cc76d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-shutdown @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment before-shutdown, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:before-shutdown:first}${shell:before-shutdown:normal}${shell:before-shutdown:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-sleep b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-sleep new file mode 100644 index 0000000..e5fc2a5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/before-sleep @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment before-sleep, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:before-sleep:first}${shell:before-sleep:normal}${shell:before-sleep:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-battery b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-battery new file mode 100644 index 0000000..58e41a9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-battery @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment on-battery, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:on-battery:first}${shell:on-battery:normal}${shell:on-battery:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-mains b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-mains new file mode 100644 index 0000000..b4a6586 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/mesh-power/moments/on-mains @@ -0,0 +1,4 @@ +# What every module on this machine runs at the moment on-mains, as the mesh placed it (module power, +# novox/hq ADR 0211). Replaced on every push: code is added by the module it belongs to, as a +# contribution for this moment. Each module's piece runs on its own, as root, bounded in time. +${shell:on-mains:first}${shell:on-mains:normal}${shell:on-mains:last} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/logind.conf.d/power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/logind.conf.d/power.conf new file mode 100644 index 0000000..9cf93bd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/logind.conf.d/power.conf @@ -0,0 +1,8 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# +# The power key and the lid, from this module's settings: the mesh's, then this machine's. +[Login] +HandlePowerKey=${config:handle-power-key} +HandleLidSwitch=${config:handle-lid-switch} +HandleLidSwitchExternalPower=${config:handle-lid-switch-external-power} +HandleLidSwitchDocked=${config:handle-lid-switch-docked} diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hibernate.target.d/mesh-power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hibernate.target.d/mesh-power.conf new file mode 100644 index 0000000..81223d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hibernate.target.d/mesh-power.conf @@ -0,0 +1,3 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Wants=mesh-power-after-wake.service diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hybrid-sleep.target.d/mesh-power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hybrid-sleep.target.d/mesh-power.conf new file mode 100644 index 0000000..81223d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/hybrid-sleep.target.d/mesh-power.conf @@ -0,0 +1,3 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Wants=mesh-power-after-wake.service diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-boot.service b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-boot.service new file mode 100644 index 0000000..b704577 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-boot.service @@ -0,0 +1,13 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Description=Every module's code after boot (mesh power) +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/mesh-power/bin/power-moment after-boot + +[Install] +WantedBy=multi-user.target diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-wake.service b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-wake.service new file mode 100644 index 0000000..773b228 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-after-wake.service @@ -0,0 +1,11 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# +# Pulled in by the four sleep targets through this module's drop-ins on them, and ordered after +# them, so it runs once the machine is awake again. Never enabled. +[Unit] +Description=Every module's code after waking (mesh power) +After=suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/mesh-power/bin/power-moment after-wake diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-shutdown.service b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-shutdown.service new file mode 100644 index 0000000..52db0a4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-shutdown.service @@ -0,0 +1,18 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# +# Started at boot and left active; stopping it is the shutdown. Ordered after the network, so at +# shutdown it stops, and runs every module's code, while the network is still up. +[Unit] +Description=Every module's code before shutdown (mesh power) +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/true +ExecStop=/usr/local/lib/mesh-power/bin/power-moment before-shutdown +TimeoutStopSec=120 + +[Install] +WantedBy=multi-user.target diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-sleep.service b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-sleep.service new file mode 100644 index 0000000..727587e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-before-sleep.service @@ -0,0 +1,11 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# +# Pulled in by sleep.target through this module's drop-in on it, never enabled: a one-shot waiting +# for a sleep is not a service whose state the mesh can declare. +[Unit] +Description=Every module's code before sleep (mesh power) +Before=sleep.target + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/mesh-power/bin/power-moment before-sleep diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-supply.service b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-supply.service new file mode 100644 index 0000000..5096a00 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/mesh-power-supply.service @@ -0,0 +1,10 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# +# Started by the module's udev rule when a power supply changes; runs on-mains or on-battery once per +# change of source. +[Unit] +Description=Every module's code for the power source (mesh power) + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/mesh-power/bin/power-moment supply diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/sleep.target.d/mesh-power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/sleep.target.d/mesh-power.conf new file mode 100644 index 0000000..096f793 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/sleep.target.d/mesh-power.conf @@ -0,0 +1,3 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Wants=mesh-power-before-sleep.service diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf new file mode 100644 index 0000000..81223d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf @@ -0,0 +1,3 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Wants=mesh-power-after-wake.service diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend.target.d/mesh-power.conf b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend.target.d/mesh-power.conf new file mode 100644 index 0000000..81223d3 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/systemd/system/suspend.target.d/mesh-power.conf @@ -0,0 +1,3 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +[Unit] +Wants=mesh-power-after-wake.service diff --git a/go/testdata/beside/mesh-catalog/modules/power/templates/udev/rules.d/90-mesh-power.rules b/go/testdata/beside/mesh-catalog/modules/power/templates/udev/rules.d/90-mesh-power.rules new file mode 100644 index 0000000..d0a75bb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/power/templates/udev/rules.d/90-mesh-power.rules @@ -0,0 +1,5 @@ +# Written by the mesh (module power, novox/hq ADR 0211). Replaced on every push. +# Only the charger's change can switch the machine between mains and battery. A battery reports its +# level many times a minute, and USB-C supplies come and go; matching them started the supply unit +# every second or two on the laptop. +SUBSYSTEM=="power_supply", ACTION=="change", ATTR{type}=="Mains", RUN+="/usr/bin/systemctl --no-block start mesh-power-supply.service" diff --git a/go/testdata/beside/mesh-catalog/modules/powerlevel10k/files/zsh-normal b/go/testdata/beside/mesh-catalog/modules/powerlevel10k/files/zsh-normal new file mode 100644 index 0000000..ba4fc1d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/powerlevel10k/files/zsh-normal @@ -0,0 +1,3 @@ +# The prompt: powerlevel10k v1.20.0, pinned in this module (novox/hq ADR 0205), and its configuration. +[[ ! -f ~/.local/share/powerlevel10k/powerlevel10k.zsh-theme ]] || source ~/.local/share/powerlevel10k/powerlevel10k.zsh-theme +[[ ! -f ~/.config/powerlevel10k/p10k.zsh ]] || source ~/.config/powerlevel10k/p10k.zsh diff --git a/go/testdata/beside/mesh-catalog/modules/records/files/config.json b/go/testdata/beside/mesh-catalog/modules/records/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/records/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/records/files/origin b/go/testdata/beside/mesh-catalog/modules/records/files/origin new file mode 100644 index 0000000..8c829dc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/records/files/origin @@ -0,0 +1 @@ +${bound:git:scheme}://${bound:git:at}:${bound:git:port} diff --git a/go/testdata/beside/mesh-catalog/modules/redis/files/redis.conf b/go/testdata/beside/mesh-catalog/modules/redis/files/redis.conf new file mode 100644 index 0000000..ac27331 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/redis/files/redis.conf @@ -0,0 +1,3 @@ +requirepass ${secret:secret} +appendonly yes +dir /data diff --git a/go/testdata/beside/mesh-catalog/modules/restic/files/backups.conf b/go/testdata/beside/mesh-catalog/modules/restic/files/backups.conf new file mode 100644 index 0000000..2f23f90 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/restic/files/backups.conf @@ -0,0 +1,2 @@ +# What the modules on this machine back up, composed by the mesh (novox/hq to-be 43). Do not edit. +${backup:lines} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/restic/files/data.conf b/go/testdata/beside/mesh-catalog/modules/restic/files/data.conf new file mode 100644 index 0000000..f19e849 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/restic/files/data.conf @@ -0,0 +1,2 @@ +# The data the modules on this machine declare, composed by the mesh (novox/hq ADR 0233). Do not edit. +${backup:data} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/restic/files/retention.conf b/go/testdata/beside/mesh-catalog/modules/restic/files/retention.conf new file mode 100644 index 0000000..cd7954c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/restic/files/retention.conf @@ -0,0 +1,6 @@ +# How many restore points the night keeps, written by the mesh from this machine's settings keep-daily, +# keep-weekly, keep-monthly (novox/hq issue 416) and keep-hand-taken-days (novox/hq issue 453). Replaced on every push; change the settings, never this file. +daily=${config:keep-daily} +weekly=${config:keep-weekly} +monthly=${config:keep-monthly} +hand=${config:keep-hand-taken-days} diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu new file mode 100644 index 0000000..b7460e9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu @@ -0,0 +1,26 @@ +#!/bin/sh +# dmenu, as this node's launcher answers it (module rofi, novox/hq ADR 0208). The node-launcher +# seat's dmenu-compatible command: choices on standard input, the chosen one on standard output, +# exit 1 when nothing was chosen. A script calls `dmenu` and works whichever module holds the seat. +# +# rofi understands dmenu's -p, -l and -i. dmenu's look options (-fn, -nb, -nf, -sb, -sf, -m, -w) +# take a value rofi would misread, so they are dropped with it; -b and -f are dropped alone. The +# look is the launcher's theme. +set -u +n=$# +while [ "$n" -gt 0 ]; do + arg=$1 + shift + n=$((n - 1)) + case $arg in + -fn | -nb | -nf | -sb | -sf | -m | -w) + if [ "$n" -gt 0 ]; then + shift + n=$((n - 1)) + fi + ;; + -b | -f) ;; + *) set -- "$@" "$arg" ;; + esac +done +exec rofi -dmenu "$@" diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch new file mode 100644 index 0000000..788f23c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch @@ -0,0 +1,39 @@ +#!/bin/sh +# rofi-launch drun|run|window|filebrowser|sudo (module rofi, novox/hq ADR 0208): what the launcher's +# key bindings run. +# +# After a resume the keyboard grab can fail for a moment, and rofi then exits at once: a key press +# that opens nothing. So a failure within half a second is tried again, up to five times. A person +# closing the menu takes longer than that, and is never shown it a second time. +set -u +mode=${1:-drun} + +now_ms() { date +%s%3N; } + +attempt() { + i=0 + while [ "$i" -lt 5 ]; do + start=$(now_ms) + "$@" && return 0 + [ $(($(now_ms) - start)) -ge 500 ] && return 1 + i=$((i + 1)) + sleep 0.1 + done + return 1 +} + +case $mode in +drun | run | window | filebrowser) + attempt rofi -show "$mode" + ;; +sudo) + # A command line, run with sudo in the node's terminal. + command=$(attempt rofi -dmenu -p sudo &2 + exit 2 + ;; +esac diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu new file mode 100644 index 0000000..2bdf64c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# rofi-powermenu (module rofi, novox/hq ADR 0208): lock, suspend, log out, reboot, shut down. +# +# It belongs to the session, and it is the launcher's: its whole face is rofi's. Every action is a +# verb of logind or the service manager, so nothing here names a window manager or a locker: +# - lock asks logind to lock the session, which the holder of node-lock-screen answers; +# - log out ends this login session, whichever window manager runs in it. +# Adopted from the workstations' power menu of 2026-10-04 (after adi1090x's powermenu type-2). +set -u + +theme=mesh-powermenu +uptime="$(uptime -p | sed -e 's/^up //')" + +shutdown='󰤂' +reboot='󰜉' +lock='󰌾' +suspend='󰤄' +logout='󰍃' +yes='󰄲' +no='󰅖' + +menu() { + rofi -dmenu -p "Uptime: $uptime" -mesg "Uptime: $uptime" -theme "$theme" +} + +confirm() { + printf '%s\n%s\n' "$yes" "$no" | rofi -dmenu -p 'Confirmation' -mesg 'Are you sure?' -theme "$theme" \ + -theme-str 'window {location: center; anchor: center; fullscreen: false; width: 350px;}' \ + -theme-str 'mainbox {children: [ "message", "listview" ];}' \ + -theme-str 'listview {columns: 2; lines: 1;}' \ + -theme-str 'element-text {horizontal-align: 0.5;}' \ + -theme-str 'textbox {horizontal-align: 0.5;}' +} + +session() { + # The login session this menu runs in: the session's own id, else logind's answer for this process. + if [ -n "${XDG_SESSION_ID:-}" ]; then + echo "$XDG_SESSION_ID" + else + loginctl show-session auto -p Id --value 2>/dev/null + fi +} + +confirmed() { [ "$(confirm)" = "$yes" ]; } + +chosen="$(printf '%s\n' "$lock" "$suspend" "$logout" "$reboot" "$shutdown" | menu)" || exit 0 +case $chosen in +"$lock") loginctl lock-session "$(session)" ;; +"$suspend") confirmed && systemctl suspend ;; +"$logout") confirmed && loginctl terminate-session "$(session)" ;; +"$reboot") confirmed && systemctl reboot ;; +"$shutdown") confirmed && systemctl poweroff ;; +esac +exit 0 diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/config.rasi b/go/testdata/beside/mesh-catalog/modules/rofi/files/config.rasi new file mode 100644 index 0000000..d123371 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/config.rasi @@ -0,0 +1,54 @@ +/* rofi, the launcher (module rofi, novox/hq ADR 0208). Owned by the mesh: this file is replaced at + * every push. Adopted from the two workstations' file of 2026-10-04: the settings that differ from + * rofi's defaults, nothing else. Its look is the theme "mesh", in themes/ beside this file. */ + +configuration { + modi: "drun,run,window,filebrowser"; + show-icons: true; + case-sensitive: false; + normalize-match: true; + matching: "normal"; + tokenize: true; + cycle: true; + steal-focus: false; + click-to-exit: true; + sort: false; + + /* Applications: the user's and the system's desktop entries, read fresh each time so a + * program installed a minute ago is there. */ + drun-match-fields: "name,generic,exec,categories,keywords"; + drun-display-format: "{name} [({generic})]"; + drun-show-actions: false; + drun-url-launcher: "xdg-open"; + drun-use-desktop-cache: false; + drun-reload-desktop-cache: false; + run,drun { + fallback-icon: "application-x-addon"; + } + + /* A command run from `run` opens in the node's terminal when it asks for one. */ + terminal: "rofi-sensible-terminal"; + run-shell-command: "{terminal} -e {cmd}"; + ssh-command: "{terminal} -e {ssh-client} {host} [-p {port}]"; + parse-hosts: true; + parse-known-hosts: true; + + window-match-fields: "title,class,role,name,desktop"; + window-format: "{w} - {c} - {t:0}"; + + disable-history: false; + max-history-size: 25; + + filebrowser { + directories-first: true; + sorting-method: "name"; + } + + display-window: "Windows"; + display-run: "Run"; + display-ssh: "SSH"; + display-drun: "Apps"; + display-filebrowser: "Files"; +} + +@theme "mesh" diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/i3/50-rofi.conf b/go/testdata/beside/mesh-catalog/modules/rofi/files/i3/50-rofi.conf new file mode 100644 index 0000000..1395d2c --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/i3/50-rofi.conf @@ -0,0 +1,8 @@ +# The launcher's key bindings (module rofi, novox/hq ADR 0208). Owned by the mesh: replaced at every +# push. i3 reads this file through its configuration's `include ~/.config/i3/config.d/*.conf`, after +# the variables it sets, so $mod is i3's. +bindsym $mod+d exec --no-startup-id ~/.local/bin/rofi-launch drun +bindsym $mod+t exec --no-startup-id ~/.local/bin/rofi-launch run +bindsym $mod+Shift+t exec --no-startup-id ~/.local/bin/rofi-launch sudo +bindsym $mod+Shift+w exec --no-startup-id ~/.local/bin/rofi-launch window +bindsym $mod+Escape exec --no-startup-id ~/.local/bin/rofi-powermenu diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh-powermenu.rasi b/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh-powermenu.rasi new file mode 100644 index 0000000..5b837f4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh-powermenu.rasi @@ -0,0 +1,172 @@ +/** + * The theme "mesh-powermenu" (module rofi, novox/hq ADR 0208): the power menu's five buttons. + * Owned by the mesh; replaced at every push. Adopted from the workstations' power menu theme of + * 2026-10-04 (after adi1090x's powermenu type-2, style-1), its colours inlined. + **/ + +/*****----- Configuration -----*****/ +configuration { + show-icons: false; +} + +/*****----- Global Properties -----*****/ +* { + background: #000000FF; + background-alt: #282B31FF; + foreground: #FFFFFFFF; + selected: #DE5200FF; + active: #DE5200FF; + urgent: #CC0000FF; + + font: "JetBrainsMono Nerd Font 11"; +} + + +/*****----- Main Window -----*****/ +window { + /* properties for window widget */ + transparency: "real"; + location: center; + anchor: center; + fullscreen: false; + width: 800px; + x-offset: 0px; + y-offset: 0px; + + /* properties for all widgets */ + enabled: true; + margin: 0px; + padding: 0px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + cursor: "default"; + background-color: @background; +} + +/*****----- Main Box -----*****/ +mainbox { + enabled: true; + spacing: 15px; + margin: 0px; + padding: 30px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + background-color: transparent; + children: [ "inputbar", "listview" ]; +} + +/*****----- Inputbar -----*****/ +inputbar { + enabled: true; + spacing: 15px; + margin: 0px; + padding: 0px; + border: 0px; + border-radius: 0px; + border-color: @selected; + background-color: transparent; + text-color: @foreground; + children: [ "textbox-prompt-colon", "prompt"]; +} + +dummy { + background-color: transparent; +} + +textbox-prompt-colon { + enabled: true; + expand: false; + str: ""; + padding: 12px 16px; + border-radius: 0px; + background-color: @urgent; + text-color: @background; +} +prompt { + enabled: true; + padding: 12px; + border-radius: 0px; + background-color: @active; + text-color: @background; +} + +/*****----- Message -----*****/ +message { + enabled: true; + margin: 0px; + padding: 12px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + background-color: @background-alt; + text-color: @foreground; +} +textbox { + background-color: inherit; + text-color: inherit; + vertical-align: 0.5; + horizontal-align: 0.5; + placeholder-color: @foreground; + blink: true; + markup: true; +} +error-message { + padding: 12px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + background-color: @background; + text-color: @foreground; +} + +/*****----- Listview -----*****/ +listview { + enabled: true; + columns: 5; + lines: 1; + cycle: true; + dynamic: true; + scrollbar: false; + layout: vertical; + reverse: false; + fixed-height: true; + fixed-columns: true; + + spacing: 15px; + margin: 0px; + padding: 0px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + background-color: transparent; + text-color: @foreground; + cursor: "default"; +} + +/*****----- Elements -----*****/ +element { + enabled: true; + spacing: 0px; + margin: 0px; + padding: 40px 10px; + border: 0px solid; + border-radius: 0px; + border-color: @selected; + background-color: @background-alt; + text-color: @foreground; + cursor: pointer; +} +element-text { + font: "JetBrainsMono Nerd Font Bold 32"; + background-color: transparent; + text-color: inherit; + cursor: inherit; + vertical-align: 0.5; + horizontal-align: 0.5; +} +element selected.normal { + background-color: var(selected); + text-color: var(background); +} diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh.rasi b/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh.rasi new file mode 100644 index 0000000..8be0f55 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/rofi/files/themes/mesh.rasi @@ -0,0 +1,263 @@ +/** + * The theme "mesh" (module rofi, novox/hq ADR 0208): every rofi window unless a caller names + * another. Owned by the mesh; replaced at every push. Adopted from the workstations' theme of + * 2026-10-04 (after adi1090x's launcher type-4, style-1), its colour file inlined so the theme is + * one file, and its face the monospace one every desktop module names. + **/ + +/*****----- Global Properties -----*****/ +* { + background: #000000FF; + background-alt: #282B31FF; + foreground: #FFFFFFFF; + selected: #DE5200FF; + active: #DE5200FF; + urgent: #CC0000FF; + + font: "JetBrainsMono Nerd Font 11"; + + border-colour: var(selected); + handle-colour: var(selected); + background-colour: var(background); + foreground-colour: var(foreground); + alternate-background: var(background-alt); + normal-background: var(background); + normal-foreground: var(foreground); + urgent-background: var(urgent); + urgent-foreground: var(background); + active-background: var(active); + active-foreground: var(background); + selected-normal-background: var(selected); + selected-normal-foreground: var(background); + selected-urgent-background: var(active); + selected-urgent-foreground: var(background); + selected-active-background: var(urgent); + selected-active-foreground: var(background); + alternate-normal-background: var(background); + alternate-normal-foreground: var(foreground); + alternate-urgent-background: var(urgent); + alternate-urgent-foreground: var(background); + alternate-active-background: var(active); + alternate-active-foreground: var(background); +} + +/*****----- Main Window -----*****/ +window { + transparency: "real"; + location: center; + anchor: center; + fullscreen: false; + width: 600px; + x-offset: 0px; + y-offset: 0px; + enabled: true; + margin: 0px; + padding: 0px; + border: 1px solid; + border-radius: 0px; + border-color: @border-colour; + cursor: "default"; + background-color: @background-colour; +} + +/*****----- Main Box -----*****/ +mainbox { + enabled: true; + spacing: 10px; + margin: 0px; + padding: 10px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: transparent; + children: [ "inputbar", "message", "listview", "mode-switcher" ]; +} + +/*****----- Inputbar -----*****/ +inputbar { + enabled: true; + spacing: 10px; + margin: 0px; + padding: 10px; + border: 0px 0px 1px 0px; + border-radius: 0px; + border-color: @border-colour; + background-color: @alternate-background; + text-color: @foreground-colour; + children: [ "prompt", "entry" ]; +} + +prompt { + enabled: true; + background-color: inherit; + text-color: inherit; +} +textbox-prompt-colon { + enabled: true; + expand: false; + str: "::"; + background-color: inherit; + text-color: inherit; +} +entry { + enabled: true; + background-color: inherit; + text-color: inherit; + cursor: text; + placeholder: "Search..."; + placeholder-color: inherit; +} + +/*****----- Listview -----*****/ +listview { + enabled: true; + columns: 1; + lines: 8; + cycle: true; + dynamic: true; + scrollbar: false; + layout: vertical; + reverse: false; + fixed-height: true; + fixed-columns: true; + spacing: 0px; + margin: 0px; + padding: 0px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: transparent; + text-color: @foreground-colour; + cursor: "default"; +} +scrollbar { + handle-width: 5px; + handle-color: @handle-colour; + border-radius: 0px; + background-color: @alternate-background; +} + +/*****----- Elements -----*****/ +element { + enabled: true; + spacing: 10px; + margin: 0px; + padding: 10px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: transparent; + text-color: @foreground-colour; + cursor: pointer; +} +element normal.normal { + background-color: var(normal-background); + text-color: var(normal-foreground); +} +element normal.urgent { + background-color: var(urgent-background); + text-color: var(urgent-foreground); +} +element normal.active { + background-color: var(active-background); + text-color: var(active-foreground); +} +element selected.normal { + background-color: var(selected-normal-background); + text-color: var(selected-normal-foreground); +} +element selected.urgent { + background-color: var(selected-urgent-background); + text-color: var(selected-urgent-foreground); +} +element selected.active { + background-color: var(selected-active-background); + text-color: var(selected-active-foreground); +} +element alternate.normal { + background-color: var(alternate-normal-background); + text-color: var(alternate-normal-foreground); +} +element alternate.urgent { + background-color: var(alternate-urgent-background); + text-color: var(alternate-urgent-foreground); +} +element alternate.active { + background-color: var(alternate-active-background); + text-color: var(alternate-active-foreground); +} +element-icon { + background-color: transparent; + text-color: inherit; + size: 24px; + cursor: inherit; +} +element-text { + background-color: transparent; + text-color: inherit; + highlight: inherit; + cursor: inherit; + vertical-align: 0.5; + horizontal-align: 0.0; +} + +/*****----- Mode Switcher -----*****/ +mode-switcher { + enabled: true; + spacing: 10px; + margin: 0px; + padding: 0px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: transparent; + text-color: @foreground-colour; +} +button { + padding: 10px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: @alternate-background; + text-color: inherit; + cursor: pointer; +} +button selected { + background-color: var(selected-normal-background); + text-color: var(selected-normal-foreground); +} + +/*****----- Message -----*****/ +message { + enabled: true; + margin: 0px; + padding: 0px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: transparent; + text-color: @foreground-colour; +} +textbox { + font: "Inter 11"; + padding: 10px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: @alternate-background; + text-color: @foreground-colour; + vertical-align: 0.5; + horizontal-align: 0.0; + highlight: none; + placeholder-color: @foreground-colour; + blink: true; + markup: true; +} +error-message { + padding: 10px; + border: 0px solid; + border-radius: 0px; + border-color: @border-colour; + background-color: @background-colour; + text-color: @foreground-colour; +} diff --git a/go/testdata/beside/mesh-catalog/modules/route-adapter/files/config.json b/go/testdata/beside/mesh-catalog/modules/route-adapter/files/config.json new file mode 100644 index 0000000..1d8d8c2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/route-adapter/files/config.json @@ -0,0 +1,6 @@ +{ + "dynamic": "/services/traefik/dynamic", + "entrypoint": "websecure", + "certificate-resolver": "le", + "machine": "host.docker.internal" +} diff --git a/go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock b/go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock new file mode 100644 index 0000000..18008bd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# screen-lock (module screen-lock, novox/hq ADR 0208): the locker. xss-lock runs it on idle, before +# suspend and on logind's Lock, and node-lock-screen's `lock` ends in it. +# +# The operator's look, adopted from the predecessor's my-i3lock: the screen as it was, blurred, with +# an orange ring, the time and the date. That needs i3lock-color, from the distribution's user +# repository, kept as found until the mesh carries such software (novox/hq research 027, question 1). +# On a machine without it the distribution's i3lock shows the same blurred screen, taken here, with +# its own plain ring; failing that, black. +# +# Before a suspend, xss-lock hands this script a lock on the sleep (XSS_SLEEP_LOCK_FD) and suspends +# once it is released. The locker must not inherit it, or the machine would wait for the unlock +# before sleeping; it is released once i3lock is up, so the machine never sleeps unlocked. This is +# xss-lock's own documented pattern for i3lock. +set -u + +# One locker: a second press of the key, or a lock while locked, changes nothing. +if pgrep -xu "$EUID" i3lock >/dev/null; then + exit 0 +fi + +# The colour build numbers its versions .c. (2.13.c.5); its version line never says "color". +if i3lock --version 2>&1 | grep -qE '[0-9]\.c\.[0-9]'; then + blank='#00000000' clear='#ffffff22' accent='#ca4a00' wrong='#880000bb' verifying='#bb00bbbb' + options=( + --insidever-color="$clear" --ringver-color="$verifying" + --insidewrong-color="$clear" --ringwrong-color="$wrong" + --inside-color="$blank" --ring-color="$accent" --line-color="$blank" --separator-color="$accent" + --verif-color="$accent" --wrong-color="$accent" --time-color="$accent" --date-color="$accent" + --layout-color="$accent" --keyhl-color="$wrong" --bshl-color="$wrong" + --screen 1 --blur 5 --ring-width=7.0 --clock --indicator + --time-str="%H:%M:%S" --date-str="%A, %Y-%m-%d" + --time-font=sans-serif --date-font=sans-serif --verif-font=sans-serif + --wrong-font=sans-serif --layout-font=sans-serif --keylayout 1 + --show-failed-attempts --ignore-empty-password + ) +else + options=(--color=000000 --show-failed-attempts --ignore-empty-password) + shot="${XDG_RUNTIME_DIR:-/tmp}/screen-lock.png" + if command -v magick >/dev/null && magick import -window root -resize 25% -blur 0x3 -resize 400% "$shot" 2>/dev/null; then + options+=(--image="$shot") + fi +fi + +if [[ -e /dev/fd/${XSS_SLEEP_LOCK_FD:--1} ]]; then + kill_i3lock() { pkill -xu "$EUID" "$@" i3lock; } + trap kill_i3lock TERM INT + i3lock "${options[@]}" {XSS_SLEEP_LOCK_FD}<&- + exec {XSS_SLEEP_LOCK_FD}<&- + while kill_i3lock -0; do + sleep 0.5 + done +else + trap 'kill %%' TERM INT + i3lock --nofork "${options[@]}" & + wait +fi diff --git a/go/testdata/beside/mesh-catalog/modules/screen-lock/files/xinitrc-normal b/go/testdata/beside/mesh-catalog/modules/screen-lock/files/xinitrc-normal new file mode 100644 index 0000000..f7f7123 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/screen-lock/files/xinitrc-normal @@ -0,0 +1,8 @@ +# The lock screen (module screen-lock, novox/hq ADR 0208): the session locks after 30 minutes idle, +# the displays go to standby and suspend then and off after an hour, and xss-lock runs the locker on +# idle, before suspend and on logind's Lock. xss-lock needs this login session, so it starts here and +# not as a unit. It is started again if it exits, for as long as this session lasts ($$ is the +# session's own process, which becomes the window manager). +xset s 1800 1800 +xset dpms 1800 1800 3600 +(while kill -0 $$ 2>/dev/null; do xss-lock --transfer-sleep-lock -- "$HOME/.local/bin/screen-lock"; sleep 2; done) & diff --git a/go/testdata/beside/mesh-catalog/modules/searxng/files/config.json b/go/testdata/beside/mesh-catalog/modules/searxng/files/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/searxng/files/config.json @@ -0,0 +1 @@ +{} diff --git a/go/testdata/beside/mesh-catalog/modules/searxng/files/settings.yml b/go/testdata/beside/mesh-catalog/modules/searxng/files/settings.yml new file mode 100644 index 0000000..6d7248e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/searxng/files/settings.yml @@ -0,0 +1,16 @@ +{ + "use_default_settings": true, + "server": { + "secret_key": "${secret:secret}", + "base_url": false, + "limiter": false, + "image_proxy": false, + "public_instance": false + }, + "search": { + "formats": ["html", "json"] + }, + "valkey": { + "url": "valkey://valkey:6379/0" + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/showcase/files/README.md b/go/testdata/beside/mesh-catalog/modules/showcase/files/README.md new file mode 100644 index 0000000..5c369cc --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/showcase/files/README.md @@ -0,0 +1,6 @@ +# showcase's packed files + +Packed as an `archive` artifact and unpacked onto the machine by an `archive` resource. + +This exists to exercise the case inlining cannot serve: a tree of files that belongs on a machine +and would make a declaration enormous if it were carried inside one. diff --git a/go/testdata/beside/mesh-catalog/modules/showcase/files/marker.txt b/go/testdata/beside/mesh-catalog/modules/showcase/files/marker.txt new file mode 100644 index 0000000..0246772 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/showcase/files/marker.txt @@ -0,0 +1 @@ +showcase diff --git a/go/testdata/beside/mesh-catalog/modules/showcase/templates/showcase.env b/go/testdata/beside/mesh-catalog/modules/showcase/templates/showcase.env new file mode 100644 index 0000000..8ea4a90 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/showcase/templates/showcase.env @@ -0,0 +1,5 @@ +SHOWCASE_GREETING=hello +SHOWCASE_EVERY_SECONDS=30 +SHOWCASE_STATE=/var/lib/showcase +SHOWCASE_DATABASE=${bound:postgres-database:at} +SHOWCASE_LISTEN=${port:8080} diff --git a/go/testdata/beside/mesh-catalog/modules/slack/files/display-session b/go/testdata/beside/mesh-catalog/modules/slack/files/display-session new file mode 100644 index 0000000..01660f8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/slack/files/display-session @@ -0,0 +1,11 @@ +# Slack (module slack, novox/hq ADR 0208, ADR 0212). Owned by the mesh: replaced at every push. +# Its one start: at login, to the tray (-s), with the arguments of the package's own desktop entry. +# No XDG autostart entry starts it as well; slack_check names one if it appears. +exec --no-startup-id /usr/bin/slack --gtk-version=3 -s +# The chat window. Slack owns four X windows, and the only one i3 manages has the class "slack" in +# lower case; the three of class "Slack" (the packaged entry's StartupWMClass) are its unmanaged +# helpers and its tray icon, so a rule on class="Slack" matches nothing. window_role keeps a call or +# screen-share window out of the rules. $ws3 is i3's, in scope here. +for_window [class="(?i)^slack$" window_role="browser-window"] move to workspace $ws3 +for_window [class="(?i)^slack$" window_role="browser-window"] floating disable +for_window [class="(?i)^slack$" window_role="browser-window"] border pixel 2 diff --git a/go/testdata/beside/mesh-catalog/modules/ssh-client/files/config b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/config new file mode 100644 index 0000000..dac07fb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/config @@ -0,0 +1,6 @@ +# The mesh's region (module ssh-client, novox/hq research 027/03). It comes first because ssh +# takes the first value it finds for each option. It brings in ~/.ssh/config.d/ in name order: +# 00-mesh, the mesh's Host per machine, then each file another module places there. Replaced at +# every push. Everything below it is yours, kept as you wrote it, and applies to every host the +# files above leave unsettled. +Include ~/.ssh/config.d/* diff --git a/go/testdata/beside/mesh-catalog/modules/ssh-client/files/forge b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/forge new file mode 100644 index 0000000..6bbf4e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/go/testdata/beside/mesh-catalog/modules/ssh-client/files/gitconfig b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/gitconfig new file mode 100644 index 0000000..0525230 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/go/testdata/beside/mesh-catalog/modules/ssh-client/files/known_hosts b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/known_hosts new file mode 100644 index 0000000..e0df6d8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/ssh-client/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/go/testdata/beside/mesh-catalog/modules/sshd/files/10-mesh.conf b/go/testdata/beside/mesh-catalog/modules/sshd/files/10-mesh.conf new file mode 100644 index 0000000..86cd883 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/sshd/files/10-mesh.conf @@ -0,0 +1,10 @@ +# Managed by the mesh (module sshd). Replaced on every push; edit the catalogue instead. +Port 22 +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +KbdInteractiveAuthentication no +UsePAM yes +X11Forwarding no +PrintMotd no +AcceptEnv LANG LC_* diff --git a/go/testdata/beside/mesh-catalog/modules/step-ca/files/config.json b/go/testdata/beside/mesh-catalog/modules/step-ca/files/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/step-ca/files/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/go/testdata/beside/mesh-catalog/modules/step-ca/files/init.env b/go/testdata/beside/mesh-catalog/modules/step-ca/files/init.env new file mode 100644 index 0000000..31d0896 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/step-ca/files/init.env @@ -0,0 +1,2 @@ +DOCKER_STEPCA_INIT_PASSWORD=${secret:password} +DOCKER_STEPCA_INIT_DNS_NAMES=${machine:at},${machine:name},localhost,127.0.0.1 diff --git a/go/testdata/beside/mesh-catalog/modules/sudo/files/10-mesh-operator b/go/testdata/beside/mesh-catalog/modules/sudo/files/10-mesh-operator new file mode 100644 index 0000000..997a49a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/sudo/files/10-mesh-operator @@ -0,0 +1,9 @@ +# The mesh's (module sudo, novox/hq to-be 42, research 027): the operator account escalates +# without a prompt. The mesh's tools that act as root run `sudo -n` as this account and rely on it; +# until this file, every machine said so only in a line set by hand in /etc/sudoers. +# Written whole at every push: an edit here is overwritten. A file of this directory whose name +# holds a dot or ends in ~ is not read by sudo; this name holds neither. +# Every command run through sudo gets a terminal of its own (novox/hq ADR 0266): an agent the operator +# starts as the agent account through sudo cannot push keystrokes into the operator's shell (TIOCSTI). +Defaults use_pty +${machine:account} ALL=(ALL:ALL) NOPASSWD: ALL diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-env.env new file mode 100644 index 0000000..b1b877f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-env.env @@ -0,0 +1,14 @@ +LOGFLARE_NODE_HOST=127.0.0.1 +DB_USERNAME=supabase_admin +DB_DATABASE=_supabase +DB_HOSTNAME=supabase-db +DB_PORT=5432 +DB_PASSWORD=${secret:postgres} +DB_SCHEMA=_analytics +LOGFLARE_API_KEY=${secret:logflare} +LOGFLARE_SINGLE_TENANT=true +LOGFLARE_SUPABASE_MODE=true +LOGFLARE_MIN_CLUSTER_SIZE=1 +POSTGRES_BACKEND_URL=postgresql://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase +POSTGRES_BACKEND_SCHEMA=_analytics +LOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=true diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-reconcile.exs b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-reconcile.exs new file mode 100644 index 0000000..179776e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-reconcile.exs @@ -0,0 +1,86 @@ +# Generated by the mesh. Do not edit: module supabase writes this file and replaces it at every push. +# +# logflare 1.4.0 copies two of its settings into its own database once and never reads them again: +# LOGFLARE_API_KEY becomes the default user's api_key when that user is created, and +# POSTGRES_BACKEND_URL becomes each source's backend when that source is created. A changed value +# reaches neither: a rotated key is refused, and a moved database is never written to (every +# source here pointed at db:5432, a name that resolves nowhere, so nothing was stored). +# +# Run by the start script before logflare starts, through logflare's own code (its Repo, its Users +# and Backends contexts and their changesets): it makes both copies say what the environment says. +# It changes nothing when they already do, says what it changed, never prints the key or a URL's +# password, and stops the start when it cannot finish. +fail = fn why -> + IO.puts(:stderr, "analytics: #{why}; not starting") + System.halt(1) +end + +say = fn what -> IO.puts(:stderr, "analytics: #{what}") end + +key = System.get_env("LOGFLARE_API_KEY") +url = System.get_env("POSTGRES_BACKEND_URL") +schema = System.get_env("POSTGRES_BACKEND_SCHEMA") + +if key in [nil, ""], do: fail.("LOGFLARE_API_KEY is not set, so the key logflare must accept is unknown") +if url in [nil, ""], do: fail.("POSTGRES_BACKEND_URL is not set, so where logflare must store logs is unknown") + +where = fn u -> + case URI.parse(to_string(u)) do + %URI{host: host, port: port, path: path} when is_binary(host) -> "#{host}:#{port}#{path}" + _ -> "an unreadable URL" + end +end + +Application.ensure_all_started(:ssl) + +outcome = + Ecto.Migrator.with_repo(Logflare.Repo, fn _repo -> + # Backends.update_source_backend_config restarts the source's pipeline after saving, and looks + # it up in this registry to do so; logflare is not running yet, so the registry is empty and + # there is nothing to restart. + {:ok, _} = Registry.start_link(keys: :unique, name: Logflare.Backends.SourceRegistry) + + case Logflare.SingleTenant.get_default_user() do + nil -> + say.("no default user yet; logflare creates it from LOGFLARE_API_KEY and POSTGRES_BACKEND_URL as it starts") + :ok + + user -> + if user.api_key == key and is_nil(user.old_api_key) do + say.("the default user's API key is LOGFLARE_API_KEY already") + else + # old_api_key is cleared too: logflare's UI can swap it back in, and the key replaced + # here may be one that leaked. + case Logflare.Users.update_user_all_fields(user, %{api_key: key, old_api_key: nil}) do + {:ok, _} -> say.("the default user's API key was replaced with LOGFLARE_API_KEY") + {:error, changeset} -> fail.("the default user's API key could not be replaced: #{inspect(changeset.errors)}") + end + end + + results = + for source <- Logflare.Sources.list_sources_by_user(user), + backend <- Logflare.Backends.list_source_backends(source), + backend.type == :postgres do + if backend.config.url == url and backend.config.schema == schema do + :same + else + case Logflare.Backends.update_source_backend_config(backend, %{url: url, schema: schema}) do + {:ok, _} -> + say.("source #{source.name} stored logs at #{where.(backend.config.url)} schema #{inspect(backend.config.schema)}; now at #{where.(url)} schema #{inspect(schema)}") + + {:error, changeset} -> + fail.("source #{source.name}'s backend could not be pointed at #{where.(url)}: #{inspect(changeset.errors)}") + end + end + end + + say.("#{Enum.count(results, &(&1 == :same))} source backend(s) already at #{where.(url)}") + + :ok + end + end) + +case outcome do + {:ok, :ok, _} -> :ok + other -> fail.("logflare's database could not be reconciled: #{inspect(other)}") +end diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-start.sh b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-start.sh new file mode 100644 index 0000000..bf39892 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/analytics-start.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Generated by the mesh. Do not edit: module supabase writes this file and replaces it at every push. +# +# Two things logflare 1.4.0 does not do for itself, done here before it starts; either failing +# stops the start, with the reason as the last line of this container's log. +# +# 1. The API key never travels in a URL. logflare prints the whole URL of every request that fails, +# query string included, in the error report Plug.Cowboy writes ("Request: POST +# /api/logs?source_name=...&api_key=..."). That is an error, so no log level hides it while errors +# are logged at all (novox/hq issue 268). logflare reads the key from the x-api-key header as well, +# and the mesh's caller, vector, sends it there; this refuses to start logflare while the vector +# config it is given still passes the key in a URL: an analytics that does not start says why +# here, and one that leaks says nothing. +# +# 2. Its database says what its environment says. logflare copies LOGFLARE_API_KEY into its default +# user and POSTGRES_BACKEND_URL into every source's backend once, when it creates them, and never +# again; reconcile.exs makes both copies match the environment through logflare's own code, after +# its migrations and before it starts. That is what lets the mesh rotate the key by starting +# logflare again, and what moves the sources' backend when the database's address or password +# changes. +set -e +conf=/run/logflare/vector.yml +if [ ! -r "$conf" ]; then + echo "analytics: $conf is not there to check, so whether the API key travels in a URL is unknown; not starting (novox/hq issue 268)" >&2 + exit 1 +fi +if grep -v '^[[:space:]]*#' "$conf" | grep -q 'api_key='; then + echo "analytics: $conf passes the API key in a URL, and logflare prints a failed request's URL; not starting (novox/hq issue 268)" >&2 + exit 1 +fi +reconcile=/run/logflare/reconcile.exs +if [ ! -r "$reconcile" ]; then + echo "analytics: $reconcile is not there, so whether logflare's stored key and backends match its environment is unknown; not starting" >&2 + exit 1 +fi +cd /opt/app/rel/logflare/bin +# The tables reconcile.exs reads exist only once the migrations have run; run.sh runs them again, +# which finds nothing left to do. +./logflare eval Logflare.Release.migrate +./logflare eval 'Code.eval_file("/run/logflare/reconcile.exs")' +exec sh run.sh diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/jwt.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/jwt.sql new file mode 100644 index 0000000..90d8822 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/jwt.sql @@ -0,0 +1,2 @@ +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO '${secret:jwt}'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO '3600'; diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/logs.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/logs.sql new file mode 100644 index 0000000..255c0f4 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/logs.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; +\c postgres diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/pooler.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/pooler.sql new file mode 100644 index 0000000..162c5b9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/pooler.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _supavisor; +alter schema _supavisor owner to :pguser; +\c postgres diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/realtime.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/realtime.sql new file mode 100644 index 0000000..4d4b9ff --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/realtime.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/roles.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/roles.sql new file mode 100644 index 0000000..2a05c0b --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/roles.sql @@ -0,0 +1,7 @@ +\set pgpass `echo "$POSTGRES_PASSWORD"` + +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/supabase.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/supabase.sql new file mode 100644 index 0000000..6236ae1 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/supabase.sql @@ -0,0 +1,3 @@ +\set pguser `echo "$POSTGRES_USER"` + +CREATE DATABASE _supabase WITH OWNER :pguser; diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/webhooks.sql b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/webhooks.sql new file mode 100644 index 0000000..5837b86 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/db-init/webhooks.sql @@ -0,0 +1,208 @@ +BEGIN; + -- Create pg_net extension + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + -- Create supabase_functions schema + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + -- supabase_functions.migrations definition + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + -- Initial supabase_functions migration + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + -- supabase_functions.hooks definition + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get( + url, + params, + headers, + timeout_ms + ); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, + 'record', NEW, + 'type', TG_OP, + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA + ); + + SELECT http_post INTO request_id FROM net.http_post( + url, + payload, + params, + headers, + timeout_ms + ); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + + INSERT INTO supabase_functions.hooks + (hook_table_id, hook_name, request_id) + VALUES + (TG_RELID, TG_NAME, request_id); + + RETURN NEW; + END + $function$; + -- Supabase super admin + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + -- Remove unused supabase_pg_net_admin role + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_pg_net_admin' + ) + THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + -- pg_net grants when extension is already enabled + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + -- Event trigger for pg_net + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_event_trigger + WHERE evtname = 'issue_pg_net_access' + ) THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-env.env new file mode 100644 index 0000000..5eade5e --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-env.env @@ -0,0 +1,6 @@ +JWT_SECRET=${secret:jwt} +SUPABASE_URL=http://supabase-kong:8000 +SUPABASE_ANON_KEY=${secret:anon-key} +SUPABASE_SERVICE_ROLE_KEY=${secret:service-role-key} +SUPABASE_DB_URL=postgresql://postgres:${secret:postgres}@supabase-db:5432/postgres +VERIFY_JWT=false diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-main/index.ts b/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-main/index.ts new file mode 100644 index 0000000..a094010 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/functions-main/index.ts @@ -0,0 +1,94 @@ +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' + +console.log('main function started') + +const JWT_SECRET = Deno.env.get('JWT_SECRET') +const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' + +function getAuthToken(req: Request) { + const authHeader = req.headers.get('authorization') + if (!authHeader) { + throw new Error('Missing authorization header') + } + const [bearer, token] = authHeader.split(' ') + if (bearer !== 'Bearer') { + throw new Error(`Auth header is not 'Bearer {token}'`) + } + return token +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder() + const secretKey = encoder.encode(JWT_SECRET) + try { + await jose.jwtVerify(jwt, secretKey) + } catch (err) { + console.error(err) + return false + } + return true +} + +serve(async (req: Request) => { + if (req.method !== 'OPTIONS' && VERIFY_JWT) { + try { + const token = getAuthToken(req) + const isValidJWT = await verifyJWT(token) + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } catch (e) { + console.error(e) + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } + + const url = new URL(req.url) + const { pathname } = url + const path_parts = pathname.split('/') + const service_name = path_parts[1] + + if (!service_name || service_name === '') { + const error = { msg: 'missing function name in request' } + return new Response(JSON.stringify(error), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + const servicePath = `/home/deno/functions/${service_name}` + console.error(`serving the request with ${servicePath}`) + + const memoryLimitMb = 150 + const workerTimeoutMs = 1 * 60 * 1000 + const noModuleCache = false + const importMapPath = null + const envVarsObj = Deno.env.toObject() + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }) + return await worker.fetch(req) + } catch (e) { + const error = { msg: e.toString() } + return new Response(JSON.stringify(error), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } +}) diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/gotrue.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/gotrue.env new file mode 100644 index 0000000..f29f778 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/gotrue.env @@ -0,0 +1,28 @@ +GOTRUE_API_HOST=0.0.0.0 +GOTRUE_API_PORT=9999 +API_EXTERNAL_URL=https://${bound:route:name} +GOTRUE_DB_DRIVER=postgres +GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:${secret:postgres}@supabase-db:5432/postgres +GOTRUE_SITE_URL=https://${bound:route:name} +GOTRUE_URI_ALLOW_LIST= +GOTRUE_DISABLE_SIGNUP=true +GOTRUE_JWT_ADMIN_ROLES=service_role +GOTRUE_JWT_AUD=authenticated +GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated +GOTRUE_JWT_EXP=3600 +GOTRUE_JWT_SECRET=${secret:jwt} +GOTRUE_EXTERNAL_EMAIL_ENABLED=false +GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false +GOTRUE_MAILER_AUTOCONFIRM=false +GOTRUE_SMTP_ADMIN_EMAIL=admin@example.com +GOTRUE_SMTP_HOST=supabase-mail +GOTRUE_SMTP_PORT=2500 +GOTRUE_SMTP_USER=fake_mail_user +GOTRUE_SMTP_PASS=fake_mail_password +GOTRUE_SMTP_SENDER_NAME=fake_sender +GOTRUE_MAILER_URLPATHS_INVITE=/auth/v1/verify +GOTRUE_MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify +GOTRUE_MAILER_URLPATHS_RECOVERY=/auth/v1/verify +GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify +GOTRUE_EXTERNAL_PHONE_ENABLED=false +GOTRUE_SMS_AUTOCONFIRM=true diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/kong.yml b/go/testdata/beside/mesh-catalog/modules/supabase/files/kong.yml new file mode 100644 index 0000000..e13bdb2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/kong.yml @@ -0,0 +1,242 @@ +# Written by the mesh (modules/supabase): the gateway's declarative config, credentials rendered in. +_format_version: '2.1' +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: "${secret:anon-key}" + - username: service_role + keyauth_credentials: + - key: "${secret:service-role-key}" + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: "${secret:dashboard-user}" + password: "${secret:dashboard}" + +### +### API Routes +### +services: + ## Open Auth routes + - name: auth-v1-open + url: http://supabase-auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://supabase-auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://supabase-auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + url: http://supabase-auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + url: http://supabase-rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://supabase-rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' + url: http://supabase-storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' + url: http://supabase-edge-functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + url: http://supabase-analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://supabase-meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://supabase-studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/meta-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/meta-env.env new file mode 100644 index 0000000..c9d2509 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/meta-env.env @@ -0,0 +1,6 @@ +PG_META_PORT=8080 +PG_META_DB_HOST=supabase-db +PG_META_DB_PORT=5432 +PG_META_DB_NAME=postgres +PG_META_DB_USER=supabase_admin +PG_META_DB_PASSWORD=${secret:postgres} diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler-env.env new file mode 100644 index 0000000..f02c7cd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler-env.env @@ -0,0 +1,14 @@ +PORT=4000 +POSTGRES_PORT=5432 +POSTGRES_DB=postgres +POSTGRES_PASSWORD=${secret:postgres} +DATABASE_URL=ecto://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase +CLUSTER_POSTGRES=true +SECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b} +VAULT_ENC_KEY=${secret:pooler-vault} +API_JWT_SECRET=${secret:jwt} +METRICS_JWT_SECRET=${secret:jwt} +REGION=local +ERL_AFLAGS=-proto_dist inet_tcp +RELEASE_NODE=supavisor@127.0.0.1 +POOLER_POOL_MODE=transaction diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler.exs b/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler.exs new file mode 100644 index 0000000..61b5ba8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/pooler.exs @@ -0,0 +1,43 @@ +{:ok, _} = Application.ensure_all_started(:supavisor) + +# Written by the mesh (modules/supabase). Per-machine values come from settings.json (the module's +# one merge:json file); secrets come from the environment the image reads. +settings = "/etc/pooler/settings.json" |> File.read!() |> Jason.decode!() |> Map.get("pooler", %{}) + +{:ok, version} = + case Supavisor.Repo.query!("select version()") do + %{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver) + _ -> nil + end + +params = %{ + "external_id" => to_string(settings["tenant"] || "default"), + "db_host" => "supabase-db", + "db_port" => System.get_env("POSTGRES_PORT"), + "db_database" => System.get_env("POSTGRES_DB"), + "require_user" => false, + "auth_query" => "SELECT * FROM pgbouncer.get_auth($1)", + "default_max_clients" => settings["max-client-connections"] || 100, + "default_pool_size" => settings["pool-size"] || 20, + "default_parameter_status" => %{"server_version" => version}, + "users" => [%{ + "db_user" => "pgbouncer", + "db_password" => System.get_env("POSTGRES_PASSWORD"), + "mode_type" => System.get_env("POOLER_POOL_MODE"), + "pool_size" => settings["pool-size"] || 20, + "is_manager" => true + }] +} + +case Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do + nil -> + {:ok, _} = Supavisor.Tenants.create_tenant(params) + + # A tenant made elsewhere (HAL named the database host "db") is pointed at this module's + # database container; nothing else it holds is changed. + %{db_host: host} = tenant when host != "supabase-db" -> + {:ok, _} = Supavisor.Tenants.update_tenant(tenant, %{"db_host" => "supabase-db"}) + + _ -> + :ok +end diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/postgrest.conf b/go/testdata/beside/mesh-catalog/modules/supabase/files/postgrest.conf new file mode 100644 index 0000000..1755753 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/postgrest.conf @@ -0,0 +1,7 @@ +db-uri = "postgres://authenticator:${secret:postgres}@supabase-db:5432/postgres" +db-schemas = "public,storage,graphql_public" +db-anon-role = "anon" +jwt-secret = "${secret:jwt}" +db-use-legacy-gucs = false +app.settings.jwt_secret = "${secret:jwt}" +app.settings.jwt_exp = "3600" diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/realtime-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/realtime-env.env new file mode 100644 index 0000000..75e2136 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/realtime-env.env @@ -0,0 +1,16 @@ +PORT=4000 +DB_HOST=supabase-db +DB_PORT=5432 +DB_USER=supabase_admin +DB_PASSWORD=${secret:postgres} +DB_NAME=postgres +DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime +DB_ENC_KEY=supabaserealtime +API_JWT_SECRET=${secret:jwt} +SECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b} +ERL_AFLAGS=-proto_dist inet_tcp +DNS_NODES='' +RLIMIT_NOFILE=10000 +APP_NAME=realtime +SEED_SELF_HOST=true +RUN_JANITOR=true diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/settings.json b/go/testdata/beside/mesh-catalog/modules/supabase/files/settings.json new file mode 100644 index 0000000..8ab8cfa --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/settings.json @@ -0,0 +1,7 @@ +{ + "pooler": { + "tenant": "default", + "pool-size": 20, + "max-client-connections": 100 + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/storage-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/storage-env.env new file mode 100644 index 0000000..fd5da86 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/storage-env.env @@ -0,0 +1,13 @@ +ANON_KEY=${secret:anon-key} +SERVICE_KEY=${secret:service-role-key} +POSTGREST_URL=http://supabase-rest:3000 +PGRST_JWT_SECRET=${secret:jwt} +DATABASE_URL=postgres://supabase_storage_admin:${secret:postgres}@supabase-db:5432/postgres +FILE_SIZE_LIMIT=52428800 +STORAGE_BACKEND=file +FILE_STORAGE_BACKEND_PATH=/var/lib/storage +TENANT_ID=stub +REGION=stub +GLOBAL_S3_BUCKET=stub +ENABLE_IMAGE_TRANSFORMATION=true +IMGPROXY_URL=http://supabase-imgproxy:5001 diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/studio-env.env b/go/testdata/beside/mesh-catalog/modules/supabase/files/studio-env.env new file mode 100644 index 0000000..303dbc5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/studio-env.env @@ -0,0 +1,18 @@ +# Next.js listens on the address HOSTNAME names, and docker sets HOSTNAME to the container's id, +# so studio answered only on its network address while its image's health check asks localhost: +# unhealthy for ever while working. Every address, as the upstream compose file has it. +HOSTNAME=:: +STUDIO_PG_META_URL=http://supabase-meta:8080 +POSTGRES_PASSWORD=${secret:postgres} +DEFAULT_ORGANIZATION_NAME=Default Organization +DEFAULT_PROJECT_NAME=Default Project +OPENAI_API_KEY=${secret:openai} +SUPABASE_URL=http://supabase-kong:8000 +SUPABASE_PUBLIC_URL=https://${bound:route:name} +SUPABASE_ANON_KEY=${secret:anon-key} +SUPABASE_SERVICE_KEY=${secret:service-role-key} +AUTH_JWT_SECRET=${secret:jwt} +LOGFLARE_API_KEY=${secret:logflare} +LOGFLARE_URL=http://supabase-analytics:4000 +NEXT_PUBLIC_ENABLE_LOGS=true +NEXT_ANALYTICS_BACKEND_PROVIDER=postgres diff --git a/go/testdata/beside/mesh-catalog/modules/supabase/files/vector.yml b/go/testdata/beside/mesh-catalog/modules/supabase/files/vector.yml new file mode 100644 index 0000000..fb0f03d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/supabase/files/vector.yml @@ -0,0 +1,258 @@ +# Written by the mesh (modules/supabase). +# +# Every sink hands logflare its API key in the x-api-key header, never as an api_key query parameter: +# logflare prints a failed request's whole URL, query string included, in its error report, so a +# key in the URL is a key in its log (novox/hq issue 268). supabase-analytics refuses to start +# while this file still puts the key in a URL. +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + include_containers: + - supabase-kong + - supabase-auth + - supabase-rest + - realtime-dev.supabase-realtime + - supabase-storage + - supabase-edge-functions + - supabase-db + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "realtime-dev.supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-edge-functions"' + db: '.appname == "supabase-db"' + # Ignores non nginx errors since they are related with kong booting up + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.request.method = req.method + .metadata.request.path = req.path + .metadata.request.protocol = req.protocol + .metadata.response.status_code = req.status + } + if err != null { + abort + } + # Ignores non nginx errors since they are related with kong booting up + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + # PostgREST logs are structured so we separate timestamp from message using regex + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P