Compare commits

...
12 Commits
Author SHA1 Message Date
mesh-tools a21da33d48 Merge pull request 'Make buildah's runtime directory before every buildah run, so mesh-build-image's health does not fail for want of it (issue 1884)' (#456) from fix/1884-runtime-dir into main 2026-10-22 21:48:47 +00:00
jschoubben b53b0e7e2c Make buildah's runtime directory before every buildah run, so a machine without it is not unhealthy forever (issue 1884)
mesh/merge-gate head b53b0e7e2cc4 merged into trunk 48dd41db91a6 — pass: builds mesh-build-image → ace, shanks; no bus step; every machine composes with th…
mesh/repo-check head b53b0e7e2cc4 merged into trunk 48dd41db91a6 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (7.92618ms) | b5…
The look made <store>/xdg only after `buildah --version`, which refuses first
when its XDG_RUNTIME_DIR is missing: the directory was never made on ace.
2026-10-22 23:39:51 +02:00
mesh-tools 48dd41db91 Merge pull request 'Fill a module again when a node-engine returns after any failed fill, not only a missing socket (issue 1873)' (#455) from fix/1873-fill-path into main 2026-10-22 21:19:09 +00:00
jschoubben 16331cdfdc Fill a module again when the node-engine's socket returns after any failed fill, not only one for want of the socket (issue 1873)
mesh/merge-gate head 16331cdfdce6 merged into trunk 66c404fd39a5 — pass: builds mesh-provisioner-client → ace, g14, novox, shanks; no bus step; every machi…
mesh/repo-check head 16331cdfdce6 merged into trunk 66c404fd39a5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/check failed: mesh-build-go: no go build node qualifies: builders disagree: go1.27.1-X:nodwarf5 recipe 74744709d277ded9 on g14; go1.27.3-X:nodwar…
mesh/delivery delivered
On ace postgres's fill of superuser.secret failed at 22:12 with the engine's own
"is a directory", what a container runtime had left at the path. The engine
delivered at 22:42 clears such a directory, but the client tried again only
modules whose fill had failed for want of the socket (issue 1165), so postgres
was never filled again and its old failure was said every 30 s. A newer engine
may write what the older could not: any failed fill is now tried again when a
node-engine makes its socket again.
2026-10-22 22:56:16 +02:00
jschoubben ab3722d3b3 Test that a fill the node-engine failed is tried again when its socket returns (issue 1873) 2026-10-22 22:53:39 +02:00
mesh-tools 66c404fd39 Merge pull request 'Let a run read from a node's pacman log never hold back a new upgrade, so the mesh can upgrade nodes at all (issue 1841)' (#454) from fix/1841-stale-run into main 2026-10-22 20:14:07 +00:00
jschoubben 3ad9d42be9 Name the captured log's machine by its role, not its name (issue 1841)
mesh/merge-gate head 3ad9d42be923 merged into trunk 083515da1be7 — pass: builds pacman → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 3ad9d42be923 merged into trunk 083515da1be7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
mesh/delivery delivered
2026-10-22 22:05:27 +02:00
mesh-tools 083515da1b Merge pull request 'Give /var/lib/systemd and /var/lib/postgres back to their packages and keep the two modules' own files under /var/lib/mesh (issue 1873, delivery A)' (#453) from fix/1873-system-state-dirs into main 2026-10-22 20:04:28 +00:00
jschoubben ac5d257a1c Let no run read from pacman's log, nor a run whose transaction ended, hold an asked upgrade back; read one run of the addressed machine whole (issue 1841)
Every run imported from the log ends at finished or failed and is never checked after, so the
one-run-at-a-time rule counted each as still going: novox's ask of 2026-10-22 22:00 was refused on
its install-time run 20220123T194900Z, and every node holds such runs. The rule now holds back a new
run only for a run the mesh ran whose transaction may still go on.

pacman_upgrades refused a run without its node, and the console takes node out of the arguments of
an address that names the machine, so no run could be read whole: a run without a node is now that
machine's.
2026-10-22 22:04:23 +02:00
jschoubben e0bf87c2e3 Give /var/lib/systemd and /var/lib/postgres back to their packages, and keep the modules' files in their own directories (issue 1873)
mesh/repo-check head e0bf87c2e3da merged into trunk ef1e98f8678c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head e0bf87c2e3da merged into trunk ef1e98f8678c — pass: group fix/1873-system-state-dirs composed: every machine composes with the change …
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
mesh/delivery delivered
mesh/delivery-group group fix/1873-system-state-dirs delivered: every member is delivered
The systemd and postgres modules' state directories said place ".", which is
/var/lib/systemd and /var/lib/postgres: directories the systemd and postgresql
packages own (root 0755; postgres 0700). The node-engine took both before issue
339's rule and gave them to the operator's account, 0700, on every machine; postgres's
grants directory, placed by default, lay inside /var/lib/postgres too.

What each module keeps moves to a directory of its own, "data", placed "mesh"
(/var/lib/mesh/systemd, /var/lib/mesh/postgres): the unlink record, and postgres's
own secrets (the same values: the vault keys them by name), consumers.json and
grants. The store's data stays /var/lib/mesh-store, untouched.

The node-engine keeps no found owner or mode for a directory, and forgets one no
longer declared as it stands; it sets the declared owner and mode on a directory it
recorded at the same path under the same id. So for this delivery "state" names
the package's directory with the package's owner and mode, and the engine gives it
back; the next change drops "state". The postgres container is recreated, its
secret's volume moving.

checks/owned/system-known keeps systemd state and postgres state, declared for
this delivery only; postgres's consumers and grants leave the list.
2026-10-22 21:31:58 +02:00
mesh-tools ef1e98f867 Merge pull request 'Keep the pacman keeper's state in a directory of its own, not in the package database's /var/lib/pacman (issue 1841)' (#452) from fix/1841-keeper-state-dir into main 2026-10-22 19:31:24 +00:00
jschoubben 098c6d3ec7 Place pacman's state outside the package manager's own directory, and refuse a module directory in a system package's (issue 1841)
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
mesh/merge-gate head 098c6d3ec70f merged into trunk 2f05e70734cd — pass: builds pacman → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 098c6d3ec70f merged into trunk 2f05e70734cd — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
pacman's state directory said place ".", which resolves to /var/lib/pacman: the
package manager's database directory. The node-engine used it as found and
reported it unhealthy on every machine, and wrote keeper.json into it. It is now
the mesh's directory for the module (place "mesh", /var/lib/mesh/pacman), as
docker's is; keeper.json, the keeper's bus secret and the root helper's socket
follow through ${dir:state}.

checks/owned rule 5 refuses a directory that is, lies in or holds a /var/lib
directory a system package owns (checks/owned/system, read from shanks through
pacman_owns and tmpfiles.d), and any resource inside one. systemd and postgres
collide today and are listed in checks/owned/system-known under issue 1873; that
list only shrinks.
2026-10-22 21:19:15 +02:00
22 changed files with 831 additions and 50 deletions
+181 -13
View File
@@ -11,13 +11,23 @@
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
//
// go run . <catalogue root>
// 5. a directory that is, lies inside or holds a directory a system package owns under /var/lib, or any other
// resource inside one (issue 1841: pacman's state directory, placed with "place": ".", was /var/lib/pacman, the
// package manager's own database directory). A placed directory is resolved as the mesh resolves it with the
// default data root: "." is /var/lib/<module>, "mesh" /var/lib/mesh/<module>, neither /var/lib/<module>/<id>.
// The system directories are checks/owned/system, captured from a machine (pacman -Qo, /usr/lib/tmpfiles.d);
// checks/owned/system-known names the exceptions still to fix, by module, resource and issue. That list only
// shrinks: an entry that no longer collides is refused, and with -base one the trunk's copy does not hold.
//
// go run . [-base <trunk's system-known>] <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path"
@@ -27,6 +37,12 @@ import (
"strings"
)
// The lists of rule 5, relative to the catalogue's root.
const (
SystemList = "checks/owned/system"
SystemKnown = "checks/owned/system-known"
)
// Home is the one root both machine homes are read as.
const Home = "${home}"
@@ -43,9 +59,9 @@ type Finding struct {
}
type resource struct {
ID, Type, Path, Unknown string
Knows []string
HasKnows bool
ID, Type, Path, Unknown, Place string
Knows []string
HasKnows bool
}
type parsed struct {
@@ -65,6 +81,7 @@ func load(file string) (*parsed, error) {
ID string `json:"id"`
Type string `json:"type"`
Path string `json:"path"`
Place string `json:"place"`
Unknown *string `json:"unknown"`
Knows []string `json:"knows"`
} `json:"resources"`
@@ -81,7 +98,7 @@ func load(file string) (*parsed, error) {
out.Module = filepath.Base(filepath.Dir(file))
}
for _, r := range m.Resources {
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil}
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Place: r.Place, Knows: r.Knows, HasKnows: r.Knows != nil}
if r.Unknown != nil {
x.Unknown = *r.Unknown
if x.Unknown == "" {
@@ -143,14 +160,23 @@ type declared struct {
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
func Check(root string, base []string) ([]Finding, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
system, err := readSystem(filepath.Join(root, SystemList))
if err != nil {
return nil, err
}
known, err := readLines(filepath.Join(root, SystemKnown))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
var out []Finding
var paths []declared
collided := map[string]bool{}
type container struct{ module, path string }
var containers []container
for _, file := range files {
@@ -170,6 +196,7 @@ func Check(root string) ([]Finding, error) {
dirs[r.ID] = "/placed/" + m.Module + "/" + r.ID
}
}
out = append(out, inSystem(m, system, known, collided)...)
for _, r := range m.Resources {
p := norm(r.Path, dirs)
if p != "" {
@@ -212,6 +239,28 @@ func Check(root string) ([]Finding, error) {
}
}
}
for _, k := range known {
if !collided[k] {
module, id, _ := strings.Cut(k, " ")
id, _, _ = strings.Cut(id, " ")
out = append(out, Finding{5, module, id, fmt.Sprintf("is listed in %s, and no longer lies in a system "+
"package's directory: take the line out, so the list only shrinks", SystemKnown)})
}
}
if base != nil {
held := map[string]bool{}
for _, b := range base {
held[b] = true
}
for _, k := range known {
if !held[k] {
module, id, _ := strings.Cut(k, " ")
id, _, _ = strings.Cut(id, " ")
out = append(out, Finding{5, module, id, fmt.Sprintf("is a new line in %s, which the trunk's copy "+
"does not hold: a module's directory is placed outside a system package's, never listed", SystemKnown)})
}
}
}
for _, d := range paths {
for _, c := range containers {
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
@@ -222,21 +271,140 @@ func Check(root string) ([]Finding, error) {
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
// systemDir is one directory a system package owns, and what says so.
type systemDir struct{ Path, Source string }
// readLines is a list's lines, without comments and blank lines.
func readLines(file string) ([]string, error) {
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
found, err := Check(root)
var lines []string
for _, line := range strings.Split(string(raw), "\n") {
if line = strings.TrimSpace(line); line != "" && !strings.HasPrefix(line, "#") {
lines = append(lines, line)
}
}
return lines, nil
}
// readSystem is the list of system directories: "<absolute path> <package>: <how it was read>" per line. Absent or
// empty, the check fails: a list that says nothing would let every module through.
func readSystem(file string) ([]systemDir, error) {
lines, err := readLines(file)
if err != nil {
return nil, fmt.Errorf("rule 5 reads the system packages' directories from %s: %w", file, err)
}
if len(lines) == 0 {
return nil, fmt.Errorf("%s names no system package's directory, so rule 5 would pass everything", file)
}
var out []systemDir
for _, line := range lines {
p, source, _ := strings.Cut(line, " ")
if !strings.HasPrefix(p, "/var/lib/") || strings.TrimSpace(source) == "" {
return nil, fmt.Errorf("%s: %q is not \"/var/lib/<directory> <package>: <how it was read>\"", file, line)
}
out = append(out, systemDir{path.Clean(p), strings.TrimSpace(source)})
}
return out, nil
}
// placedAt is where the mesh places a pathless directory with the default data root (mesh-sdk catalogue dirsFor).
func placedAt(module string, r resource) string {
switch r.Place {
case ".":
return "/var/lib/" + module
case "mesh":
return "/var/lib/mesh/" + module
}
return "/var/lib/" + module + "/" + r.ID
}
// inSystem is rule 5 for one module: each resource whose resolved path collides with a system package's directory,
// unless system-known lists it (then it is marked collided, so a stale line is found).
func inSystem(m *parsed, system []systemDir, known []string, collided map[string]bool) []Finding {
real := map[string]string{}
for _, r := range m.Resources {
if r.Type == "directory" && r.Path == "" && r.ID != "" {
real[r.ID] = placedAt(m.Module, r)
}
}
for _, r := range m.Resources {
if r.ID != "" && r.Path != "" {
real[r.ID] = r.Path
}
}
var out []Finding
for _, r := range m.Resources {
p := ""
if r.Path != "" {
p = norm(r.Path, real)
} else if r.Type == "directory" {
p = real[r.ID]
}
if !strings.HasPrefix(p, "/") {
continue
}
for _, s := range system {
inside := p == s.Path || strings.HasPrefix(p, s.Path+"/")
holds := r.Type == "directory" && strings.HasPrefix(s.Path, p+"/")
if !inside && !holds {
continue
}
listed := ""
for _, k := range known {
if strings.HasPrefix(k, m.Module+" "+r.ID+" ") || k == m.Module+" "+r.ID {
listed = k
}
}
if listed != "" {
collided[listed] = true
break
}
how := "is or lies inside"
if holds {
how = "holds"
}
out = append(out, Finding{5, m.Module, r.ID, fmt.Sprintf("resolves to %q, which %s %s, the directory of the "+
"system package %s; place it under /var/lib/mesh/%s (\"place\": \"mesh\") or a path of its own", p, how,
s.Path, s.Source, m.Module)})
break
}
}
return out
}
func main() {
baseFile := flag.String("base", "", "the trunk's copy of "+SystemKnown+": a line it does not hold is refused")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
var base []string
if *baseFile != "" {
lines, err := readLines(*baseFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
base = append([]string{}, lines...)
}
found, err := Check(root, base)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule)
cite := "novox/hq ADR 0422 §8, issue 1024"
if f.Rule == 5 {
cite = "issue 1841"
}
fmt.Printf("%s: %s: %s (rule %d, %s)\n", f.Module, f.ID, f.Why, f.Rule, cite)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory")
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory, or in a system package's")
}
+102 -1
View File
@@ -20,7 +20,10 @@ func manifest(t *testing.T, root, module, body string) {
func check(t *testing.T, root string) []Finding {
t.Helper()
found, err := Check(root)
if _, err := os.Stat(filepath.Join(root, SystemList)); os.IsNotExist(err) {
lists(t, root, "/var/lib/pacman pacman: pacman -Qo\n", "")
}
found, err := Check(root, nil)
if err != nil {
t.Fatal(err)
}
@@ -35,6 +38,20 @@ func only(t *testing.T, found []Finding, rule int, module, id string) {
}
}
// lists writes the catalogue's list of system directories and its known exceptions.
func lists(t *testing.T, root, system, known string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemList), []byte(system), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemKnown), []byte(known), 0o644); err != nil {
t.Fatal(err)
}
}
func dir(id, path, rest string) string {
return `{"id":"` + id + `","type":"directory","path":"` + path + `"` + rest + `}`
}
@@ -145,3 +162,87 @@ func TestPlacedDirectoriesOfTwoModulesAreTheirOwn(t *testing.T) {
t.Fatalf("refused %+v", found)
}
}
// 1841/AC18: a module's directory that resolves to a /var/lib directory a system package owns, lies inside one or
// holds one, is refused, whether placed (place ".", or by default beneath the module's own) or stated; a placed
// directory saying place "mesh" lies under /var/lib/mesh and passes (issue 1841: pacman's state was /var/lib/pacman).
func TestADirectoryOfASystemPackageIsRefused(t *testing.T) {
system := "/var/lib/pacman pacman: pacman -Qo\n/var/lib/systemd/coredump systemd: tmpfiles.d/systemd.conf\n"
for name, resources := range map[string][]string{
"placed-own": {`{"id":"state","type":"directory","place":".","unknown":"program"}`},
"placed-beneath": {`{"id":"coredump","type":"directory","unknown":"program"}`},
"stated-inside": {dir("d", "/var/lib/pacman/keeper", `,"unknown":"program"`)},
"stated-holding": {dir("d", "/var/lib/systemd", `,"unknown":"program"`)},
"file-inside": {`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
`{"id":"f","type":"file","path":"/var/lib/pacman/keeper.json"}`},
} {
module := map[string]string{"placed-own": "pacman", "placed-beneath": "systemd"}[name]
if module == "" {
module = "keeper"
}
root := t.TempDir()
lists(t, root, system, "")
manifest(t, root, module, mod(module, resources...))
found := check(t, root)
if len(found) != 1 || found[0].Rule != 5 || found[0].Module != module ||
!strings.Contains(found[0].Why, "/var/lib/") || !strings.Contains(found[0].Why, "package") {
t.Errorf("%s: found %+v; want one finding of rule 5 for %s naming the path and the package", name, found, module)
}
}
root := t.TempDir()
lists(t, root, system, "")
manifest(t, root, "pacman", mod("pacman",
`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
`{"id":"f","type":"file","path":"${dir:state}/keeper.json"}`,
dir("other", "/var/lib/pacman-other", `,"unknown":"program"`)))
if found := check(t, root); len(found) != 0 {
t.Fatalf("a directory under /var/lib/mesh/pacman was refused: %+v", found)
}
}
// 1841/AC18: a known exception passes; one that no longer collides is refused, so the list only shrinks; one the trunk's
// copy does not hold is refused; and an absent list of system directories fails the check rather than passing it.
func TestKnownSystemExceptionsOnlyShrink(t *testing.T) {
system := "/var/lib/systemd systemd: tmpfiles.d/systemd.conf\n"
known := "# comment\nsystemd state issue 1873\n"
root := t.TempDir()
lists(t, root, system, known)
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":".","unknown":"program"}`))
if found := check(t, root); len(found) != 0 {
t.Fatalf("a known exception was refused: %+v", found)
}
if found, err := Check(root, []string{"systemd state issue 1873"}); err != nil || len(found) != 0 {
t.Fatalf("a known exception held by the trunk was refused: %+v %v", found, err)
}
if found, err := Check(root, []string{}); err != nil || len(found) != 1 || found[0].Rule != 5 {
t.Fatalf("an exception the trunk does not hold passed: %+v %v", found, err)
}
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":"mesh","unknown":"program"}`))
only(t, check(t, root), 5, "systemd", "state")
root = t.TempDir()
manifest(t, root, "a", mod("a"))
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, SystemList), []byte("# nothing\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := Check(root, nil); err == nil {
t.Fatal("an empty list of system directories passed")
}
}
// 1841/AC18: the catalogue as it stands passes rule 5 with its own lists.
func TestTheCatalogueKeepsOutOfSystemDirectories(t *testing.T) {
found, err := Check("../..", nil)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
if f.Rule == 5 {
t.Errorf("%s: %s: %s", f.Module, f.ID, f.Why)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
# The directories under /var/lib that a system package owns (rule 5 of checks/owned, issue 1841). A module's
# directory, placed or stated, may not be one of these, lie inside one or hold one: the package's program and the
# package manager keep their own files there, and the node-engine would judge, chmod or chown the directory.
#
# One line per directory: "<path> <package>: <how it was read>". Captured on the workstation on 2026-10-22, through
# the mesh: pacman's own database by `pacman -Qo` (pacman_owns), the rest from the `d`, `D` and `Q` lines of
# /usr/lib/tmpfiles.d (node-reads.search for "/var/lib/"). A directory found to belong to a package is added here.
/var/lib/pacman pacman: pacman -Qo /var/lib/pacman/ (pacman 7.1.0)
/var/lib/alsa alsa-utils: tmpfiles.d/alsa-utils.conf
/var/lib/colord colord: tmpfiles.d/colord.conf
/var/lib/containers podman: tmpfiles.d/podman.conf
/var/lib/dbus dbus: tmpfiles.d/dbus.conf
/var/lib/geoclue geoclue: tmpfiles.d/geoclue.conf
/var/lib/lastlog util-linux: tmpfiles.d/lastlog2.conf
/var/lib/libuuid util-linux: tmpfiles.d/uuidd.conf
/var/lib/machines systemd: tmpfiles.d/systemd-nspawn.conf
/var/lib/openvpn/chroot networkmanager-openvpn: tmpfiles.d/nm-openvpn-tmpfiles.conf
/var/lib/portables systemd: tmpfiles.d/portables.conf
/var/lib/postgres postgresql: tmpfiles.d/postgresql.conf
/var/lib/private systemd: tmpfiles.d/systemd.conf
/var/lib/rpcbind rpcbind: tmpfiles.d/rpcbind.conf
/var/lib/systemd systemd: tmpfiles.d/systemd.conf
/var/lib/tpm2-tss/system/keystore tpm2-tss: tmpfiles.d/tpm2-tss-fapi.conf
+5
View File
@@ -0,0 +1,5 @@
# Modules whose directory lies in a system package's directory today (rule 5 of checks/owned, issue 1841), each with
# the issue that moves it out: "<module> <resource id> issue <number>". This list only shrinks: a line whose module
# no longer collides is refused, and so is a line the trunk's copy does not hold.
postgres state issue 1873
systemd state issue 1873
+15 -3
View File
@@ -48,7 +48,8 @@
# 10. the mail-store seat (novox/hq ADR 0378 §3, issue 800): no manifest but modules/mesh-mail claims it.
# 11. owned paths (novox/hq ADR 0422 §8, issue 1024; it holds ~/.ssh for ssh-client as ADR 0415 §6 asked):
# every directory and archive says `unknown` (move-out, module or program) and a valid `knows`; `move-out`
# only under a home; and no module declares a path inside a directory of another module.
# only under a home; no module declares a path inside a directory of another module; and none is, lies in or
# holds a directory a system package owns under /var/lib (checks/owned/system, issue 1841).
# checks/sshdir (ADR 0415 §6) stays while main runs it; it goes under issue 1035 once main runs checks/owned.
# 12. the image builder (novox/hq ADR 0487 §3, issue 1704): only modules/buildah provides rootless-image-build.
# 13. the image's place (novox/hq ADR 0487 §1, issue 1707): every image builds from a Dockerfile in its own module's directory.
@@ -71,8 +72,19 @@ set -eu
# ~/.ssh (novox/hq ADR 0415 §6): only ssh-client may name a path under it. Main's merge check runs this line, so
# checks/sshdir stays until main runs checks/owned; it goes under issue 1035.
(cd checks/sshdir && go test -count=1 . && go run . ../..)
# Owned paths (novox/hq ADR 0422 §8): unknown files named per directory; no path inside another module's directory.
(cd checks/owned && go test -count=1 . && go run . ../..)
# Owned paths (novox/hq ADR 0422 §8): unknown files named per directory; no path inside another module's directory;
# none in a system package's directory under /var/lib (issue 1841), the known exceptions only shrinking: judged
# against the trunk's copy of checks/owned/system-known at the merge base; a trunk without one is the list's first copy.
owned_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/owned/system-known" 2>/dev/null; then
owned_base=$(mktemp)
git show "$merge_base:checks/owned/system-known" >"$owned_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/owned/system-known is held to the list it was first written with"
fi
(cd checks/owned && go test -count=1 . && if [ -n "$owned_base" ]; then go run . -base "$owned_base" ../..; else go run . ../..; fi)
# The secrets (novox/hq issue 342): a password is not a setting, so no setting can replace the vault's. The
# known list only shrinks: judged against the trunk's copy at the merge base, a line it does not hold is refused.
secrets_base=""
@@ -4,8 +4,6 @@ import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
@@ -60,6 +58,11 @@ type Look struct {
// missing is a *buildstep.Refusal naming it, so a step ends refused and the health says the same.
func (m *Module) look(ctx context.Context) (Look, error) {
l := Look{Account: m.Account}
// The runtime directory comes first: buildah refuses before anything else when its XDG_RUNTIME_DIR is not there
// (issue 1884), so a look that made it only after `buildah --version` never made it at all.
if err := m.ensureRuntimeDir(); err != nil {
return l, &buildstep.Refusal{Text: err.Error()}
}
var err error
if l.Buildah, err = m.buildahVersion(ctx); err != nil {
return l, &buildstep.Refusal{Text: err.Error()}
@@ -67,11 +70,6 @@ func (m *Module) look(ctx context.Context) (Look, error) {
if l.UIDs, l.GIDs, err = m.ranges(); err != nil {
return l, &buildstep.Refusal{Text: err.Error(), Have: l.Buildah}
}
for _, d := range []string{m.Store, filepath.Join(m.Store, "xdg")} {
if err := os.MkdirAll(d, 0o700); err != nil {
return l, err
}
}
if err := m.userNamespace(ctx); err != nil {
return l, &buildstep.Refusal{Text: err.Error(), Have: l.Buildah}
}
@@ -111,6 +111,25 @@ func (m *Module) storeFlags() []string {
// env is what buildah is given beyond the base environment: a runtime directory of its own inside the store, so
// nothing is read from the account's runtime directory, and no registry credential file anywhere (the module has none
// and REGISTRY_AUTH_FILE is never set).
//
// Every call makes the runtime directory first if it is not there (issue 1884): a directory removed after the last look
// is made again before buildah runs. A failure to make it is not lost: buildah then names the directory in its own
// error, and the next look refuses with it.
func (m *Module) env() []string {
return []string{"XDG_RUNTIME_DIR=" + filepath.Join(m.Store, "xdg")}
_ = m.ensureRuntimeDir()
return []string{"XDG_RUNTIME_DIR=" + m.runtimeDir()}
}
// runtimeDir is buildah's runtime directory, inside the store.
func (m *Module) runtimeDir() string { return filepath.Join(m.Store, "xdg") }
// ensureRuntimeDir makes the store and the runtime directory inside it, 0700 and under the module's own account (the
// process runs as it), when they are not there; it changes nothing that is.
func (m *Module) ensureRuntimeDir() error {
for _, d := range []string{m.Store, m.runtimeDir()} {
if err := os.MkdirAll(d, 0o700); err != nil {
return fmt.Errorf("buildah's runtime directory %s cannot be made: %w", m.runtimeDir(), err)
}
}
return nil
}
@@ -0,0 +1,83 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/forgekey"
"git.novox.be/novox/mesh-sdk/go/health"
)
// buildahLike refuses as buildah does when the XDG_RUNTIME_DIR it is given is not there (seen on ace, issue 1884):
// "lstat <dir>: no such file or directory", before it does anything else.
func buildahLike(t *testing.T, run forgekey.Runner, seen *[]string) forgekey.Runner {
t.Helper()
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
if name == "buildah" {
for _, e := range env {
dir, ok := strings.CutPrefix(e, "XDG_RUNTIME_DIR=")
if !ok {
continue
}
if _, err := os.Lstat(dir); err != nil {
*seen = append(*seen, "missing")
return forgekey.Ran{Status: 125, Stderr: "Error: lstat " + dir + ": no such file or directory\n"}
}
*seen = append(*seen, "present")
}
}
return run(ctx, env, name, args...)
}
}
// 1884/AC1: given a store directory that exists without its runtime directory, when the health is looked at, then the
// module makes the runtime directory (0700) before it runs buildah, and the health is healthy.
func TestHealthMakesTheRuntimeDirectoryBeforeBuildah(t *testing.T) {
f := newFake(t)
m := moduleWith(t, f, newStates())
var seen []string
m.Run = buildahLike(t, m.Run, &seen)
xdg := filepath.Join(m.Store, "xdg")
if _, err := os.Stat(xdg); !os.IsNotExist(err) {
t.Fatalf("the test needs a store without its runtime directory: %v", err)
}
a := m.Health(context.Background())
if a.State != health.StateHealthy || !a.Healthy {
t.Fatalf("health = %+v, want healthy", a)
}
st, err := os.Stat(xdg)
if err != nil {
t.Fatalf("the runtime directory was not made: %v", err)
}
if st.Mode().Perm() != 0o700 || !st.IsDir() {
t.Errorf("the runtime directory is %v, want a directory 0700", st.Mode())
}
for _, s := range seen {
if s != "present" {
t.Fatalf("buildah ran without its runtime directory: %v", seen)
}
}
}
// 1884/AC2: given a module whose runtime directory was removed after its last look, when a build step runs buildah, then
// the runtime directory is made again before buildah runs.
func TestEveryBuildahRunMakesTheRuntimeDirectoryAgain(t *testing.T) {
f := newFake(t)
m := moduleWith(t, f, newStates())
var seen []string
m.Run = buildahLike(t, m.Run, &seen)
if err := m.Check(context.Background()); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(filepath.Join(m.Store, "xdg")); err != nil {
t.Fatal(err)
}
seen = nil
m.remove("localhost/x:y")
if len(seen) != 1 || seen[0] != "present" {
t.Fatalf("buildah's runs saw their runtime directory %v, want [present]", seen)
}
}
@@ -120,7 +120,7 @@ type Client struct {
// Now dates the reports; nil is time.Now.
Now func() time.Time
// SocketUp wakes the client when the node-engine's write socket is made again (watchSocket); nil never does.
// A module whose write failed for want of the socket is filled again then (issue 1165), and nothing else is.
// A module whose write failed is filled again then (issue 1165, issue 1873), and nothing else is.
SocketUp <-chan struct{}
// watchBound watches a provider's bucket of entries for this machine's keys, once per run (consumers.go); set by
@@ -150,7 +150,8 @@ type state struct {
// of the consumers left out of one of its files.
providing map[string]bool
leftOut map[string]string
// unplaced is a module whose last fill failed for want of the node-engine's write socket (issue 1165).
// unplaced is a module whose last fill failed, for want of the node-engine's write socket (issue 1165) or by the
// engine's own refusal (issue 1873): filled again when the socket is made again.
unplaced map[string]bool
// bound is each provider's bucket of entries this client watches, by key: this machine's consumers' entries alone;
// boundReady is a bucket whose current entries are all read, and boundUsers the modules filled from each.
@@ -705,9 +706,10 @@ func (c *Client) reconcile(ctx context.Context, kvs map[string]jetstream.KeyValu
err = errFill(a)
}
if err != nil {
if socketAbsent(err) {
s.unplaced[module] = true
}
// Filled again when a node-engine makes its socket again: one gone (issue 1165), or one that refused for a
// reason of its own, which a newer engine may write (issue 1873: "is a directory", what a container
// runtime left at the path, cleared by the engine that followed).
s.unplaced[module] = true
failed = append(failed, fmt.Sprintf("%s: %v", f.Path, err))
continue
}
@@ -106,3 +106,38 @@ func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testin
defer l.Close()
heard("the directory and socket back")
}
// Issue 1873: a fill the node-engine refused for a reason of its own (on ace, "read …/superuser.secret: is a
// directory", what a container runtime had left there) is tried again when a node-engine makes its socket again, as a
// newer engine may write what the older could not. Before, only a fill that failed for want of the socket was, so
// ace's postgres stayed unplaced after the engine that cures it was delivered, and its old failure was said every 30 s.
func TestNatsAFillTheEngineFailedIsTriedWhenTheSocketReturns(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
e := newEngine(t, map[string]string{dunstrc: "owned"})
if err := os.MkdirAll(e.at(dunstrc), 0o755); err != nil {
t.Fatal(err)
}
stop := e.listen(t)
b.config(t, "dunst", "font-size", "13")
b.assign(t, "dunst", dunst(t))
aClient(t, b, e, t.TempDir())
eventually(t, "the engine's failure is said", func() bool {
r, _ := heard.last("dunst")
return r.Status == StatusFailed && strings.Contains(r.Reason, "is a directory")
})
// The node-engine replaces itself with one that clears what the runtime left.
stop()
if err := os.Remove(e.at(dunstrc)); err != nil {
t.Fatal(err)
}
e.listen(t)
eventually(t, "dunstrc is written by the new engine", func() bool {
return strings.Contains(e.read(t, dunstrc), "Monospace 13")
})
eventually(t, "and the report says applied", func() bool {
r, _ := heard.last("dunst")
return r.Status == StatusApplied
})
}
@@ -83,7 +83,7 @@ func TestTheKeeperIsAProcessUnderItsOwnAccountWithABusAccountOfItsOwn(t *testing
t.Errorf("the bus account is the keeper's own secret, owned by its account: %v", secrets)
}
state := m.resource(t, "state")
if state["type"] != "directory" || state["place"] != "." || state["unknown"] == nil {
if state["type"] != "directory" || state["place"] != "mesh" || state["unknown"] == nil {
t.Errorf("the state directory: %v", state)
}
order := map[string]int{}
@@ -211,3 +211,37 @@ func TestUpgradeOrderIsATrustedListOfTheMesh(t *testing.T) {
t.Error("a value falls back only to a key of its own type (ADR 0348 §4)")
}
}
// 1841/AC17: the keeper's state directory is the mesh's directory for the module (place "mesh", <data root>/mesh/pacman),
// never place "." or a placement by default: for this module both resolve under /var/lib/pacman, the package manager's
// own database directory (issue 1841: the node-engine used it as found and reported it unhealthy, and wrote keeper.json
// into it). What the keeper and its root helper keep there is still named through ${dir:state}.
func TestTheStateDirectoryIsOutsideThePackageManagersOwn(t *testing.T) {
m := rawManifest(t)
resources, _ := m["resources"].([]any)
var state map[string]any
for _, x := range resources {
if r, _ := x.(map[string]any); r["id"] == "state" {
state = r
}
}
if state == nil || state["type"] != "directory" {
t.Fatalf("no state directory: %v", state)
}
if state["place"] != "mesh" || state["path"] != nil {
t.Fatalf("the state directory says place %v and path %v; it is placed \"mesh\" with no path, so it resolves to "+
"/var/lib/mesh/pacman and not into /var/lib/pacman", state["place"], state["path"])
}
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
if bytes.Contains(raw, []byte("/var/lib/pacman")) {
t.Fatal("the manifest names /var/lib/pacman")
}
for _, want := range []string{"${dir:state}/keeper.json", "${dir:state}/broker-keeper.secret", RootSocketPath} {
if !bytes.Contains(raw, []byte(want)) {
t.Errorf("the manifest no longer names %s", want)
}
}
}
+7 -2
View File
@@ -248,12 +248,12 @@ func tools(m *Machine) []stdio.Tool {
Name: "pacman_upgrades",
Description: "Every full upgrade and rollback of every node, from the bus (ADR 0508 §1): each run's node, kind, what started it, " +
"its round, when it began and ended, its outcome (refused, finished, failed or running) and whether its after-upgrade check passed; " +
"narrowed to a node and to days, newest first. With a node and a run, that run whole, every step with its time.",
"narrowed to a node and to days, newest first. With a run, that run whole, every step with its time: the node's named, else this one's.",
Input: schema(map[string]any{
"node": map[string]any{"type": "string", "description": "only this node's runs"},
"from": sinceArg,
"until": map[string]any{"type": "string", "description": "a day, YYYY-MM-DD, the last one listed"},
"run": map[string]any{"type": "string", "description": "one run, by its id, with node"},
"run": map[string]any{"type": "string", "description": "one run, by its id: of node, else of this node"},
"limit": limitArg(50, 1000),
}),
Run: func(args map[string]any) (any, error) {
@@ -269,6 +269,11 @@ func tools(m *Machine) []stdio.Tool {
if err != nil {
return nil, err
}
if v[3] != "" && v[0] == "" {
// An address naming the machine has its node taken out of the arguments (the console):
// the run is this machine's (issue 1841).
v[0] = thisNode()
}
return ListUpgrades(upgradeState(StateRuns), v[0], v[1], v[2], v[3], n)
},
},
+1 -1
View File
@@ -228,7 +228,7 @@ func (u *Upgrades) StartRollback(of string, removeAdded bool, ask string) (Run,
if o.Run.Run == r.Run.Run {
return r.Run, fmt.Errorf("run %s of %s is there already: one run a second", o.Run.Run, u.Node)
}
if !ended(o.Run) && o.From != FromLog {
if holdsTheDatabase(o.Run) {
return r.Run, u.refuse(r, fmt.Sprintf("run %s is still going (its last step is %s): one run at a time", o.Run.Run, lastStep(o.Run)))
}
}
@@ -0,0 +1,115 @@
package main
// A run read from pacman's log, or a run whose transaction ended, never holds a new run back (issue 1841, the
// refusal of 2026-10-22 22:00 on the control-node: "run 20220123T194900Z is still going (its last step is finished): one
// run at a time"); and pacman_upgrades reads one run whole at an address that names the machine.
import (
"strings"
"testing"
"time"
)
// installLogHead is the first twelve lines of the control-node's /var/log/pacman.log, captured 2026-10-22 22:10
// with node-reads.read: the install's `pacman -Syu` with nothing to do, which became run 20220123T194900Z, its
// last step finished at the moment it began (pacman_upgrades there, 2026-10-22: from log, outcome finished,
// began 2022-01-23T19:49:00Z, ended 2022-01-23T20:49:00+01:00).
const installLogHead = "[2022-01-23T20:49:00+0100] [PACMAN] Running 'pacman -Syu'\n" +
"[2022-01-23T20:49:00+0100] [PACMAN] synchronizing package lists\n" +
"[2022-01-23T20:49:00+0100] [PACMAN] starting full system upgrade\n" +
"[2022-01-23T20:50:45+0100] [PACMAN] Running 'pacman -S nano'\n" +
"[2022-01-23T20:50:49+0100] [ALPM] transaction started\n" +
"[2022-01-23T20:50:50+0100] [ALPM] installed nano (6.0-1)\n" +
"[2022-01-23T20:50:50+0100] [ALPM] transaction completed\n" +
"[2022-01-23T20:50:50+0100] [ALPM] running '30-systemd-update.hook'...\n" +
"[2022-01-23T20:55:31+0100] [PACMAN] Running 'pacman -S docker'\n" +
"[2022-01-23T20:55:36+0100] [ALPM] transaction started\n" +
"[2022-01-23T20:55:36+0100] [ALPM] installed bridge-utils (1.7.1-1)\n" +
"[2022-01-23T20:55:36+0100] [ALPM] installed libtool (2.4.6+42+gb88cebd5-16)\n"
// 1841/AC20: given the run read from the control-node's log, when an upgrade is asked, then it starts.
func TestARunReadFromTheLogIsHistoryAndNeverHoldsAnAsk(t *testing.T) {
k := newKeeperBed(t)
past := PastUpgrades("node-a", installLogHead)
if len(past) != 1 || past[0].Run != "20220123T194900Z" || steps(past[0]) != "started finished" || past[0].From != FromLog {
t.Fatalf("the captured log reads as the record the control-node holds: %+v", past)
}
if _, err := k.runs.Create(NodeKey("node-a", past[0].Run), past[0]); err != nil {
t.Fatal(err)
}
if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil {
t.Fatal(err)
}
var r Run
k.runs.value("node-a.20261004T120000Z", &r)
if lastStep(r) == StepRefused || len(k.root.started) != 1 {
t.Errorf("a run read from the log held the ask back: %s %+v", steps(r), r.Steps)
}
}
// 1841/AC21: given a run the mesh ran whose last step is finished, failed or refused (no after-upgrade check
// yet, no end time of the run itself), when another run would start, then the earlier one does not hold it;
// a run whose transaction may still go on (no step, recorded, checked before, started) does.
func TestARunWhoseTransactionEndedNeverHoldsTheNext(t *testing.T) {
at := time.Date(2026, 10, 22, 20, 0, 0, 0, time.UTC)
for last, holds := range map[string]bool{
StepFinished: false, StepFailed: false, StepRefused: false, StepCheckedAfter: false,
"": true, StepRecorded: true, StepCheckedBefore: true, StepStarted: true,
} {
r := Run{Node: "node-a", Run: "20261022T200000Z", Kind: RunUpgrade, From: FromAsk}
if last != "" {
r.Steps = []Step{{Step: last, At: at}}
}
if got := holdsTheDatabase(r); got != holds {
t.Errorf("last step %q: holds %v, want %v", last, got, holds)
}
r.From = FromLog
if holdsTheDatabase(r) {
t.Errorf("a run read from the log, last step %q, holds the next", last)
}
}
k := newKeeperBed(t)
done := Run{Node: "node-a", Run: "20261004T110000Z", Kind: RunUpgrade, From: FromAsk,
Steps: []Step{{Step: StepStarted, At: k.now}, {Step: StepFinished, At: k.now}}}
if _, err := k.runs.Create(NodeKey("node-a", done.Run), done); err != nil {
t.Fatal(err)
}
if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil {
t.Fatal(err)
}
var r Run
k.runs.value("node-a.20261004T120000Z", &r)
if lastStep(r) == StepRefused {
t.Errorf("a finished run held the next: %+v", r.Steps)
}
}
// 1841/AC22: given an address that names the machine (the console takes `node` out of the arguments), when
// pacman_upgrades is asked for one run, then that machine's run is answered whole.
func TestPacmanUpgradesReadsOneRunOfThisNodeWithoutANode(t *testing.T) {
runs := newBucket()
past := PastUpgrades("node-a", installLogHead)[0]
if _, err := runs.Create(NodeKey("node-a", past.Run), past); err != nil {
t.Fatal(err)
}
savedState, savedNode := upgradeState, thisNode
upgradeState = func(string) Bucket { return runs }
thisNode = func() string { return "node-a" }
defer func() { upgradeState, thisNode = savedState, savedNode }()
for _, tool := range tools(machine(nil, 1000)) {
if tool.Name != "pacman_upgrades" {
continue
}
out, err := tool.Run(map[string]any{"run": past.Run})
if err != nil {
t.Fatalf("one run without a node: %v", err)
}
whole := out.(map[string]any)["run"].(Run)
if whole.Node != "node-a" || steps(whole) != "started finished" || !strings.Contains(whole.Steps[1].Why, "no transaction") {
t.Errorf("one run whole: %+v", whole)
}
return
}
t.Fatal("no pacman_upgrades")
}
+17 -1
View File
@@ -63,6 +63,22 @@ func ended(r Run) bool {
return false
}
// holdsTheDatabase says whether a run holds a new run back (one run at a time, ADR 0508 §8): only a run the
// mesh ran whose transaction may still go on. A run read from pacman's log is history (ADR 0508 §1), and a run
// whose last step is finished, failed, refused or checked after has no transaction left: its after-upgrade
// check, still to be written by Follow, reads and changes nothing (issue 1841: every run read from the log
// ended at finished and held every ask back).
func holdsTheDatabase(r Run) bool {
if r.From == FromLog {
return false
}
switch lastStep(r) {
case StepFinished, StepFailed, StepRefused, StepCheckedAfter:
return false
}
return true
}
// ownRuns are the node's runs, oldest first, each with the revision of its key.
func (u *Upgrades) ownRuns() ([]openRun, error) {
keys, err := u.Runs.Keys()
@@ -117,7 +133,7 @@ func (u *Upgrades) startUpgrade(r *openRun) error {
if o.Run.Run == r.Run.Run {
return fmt.Errorf("run %s of %s is there already: one run a second", o.Run.Run, u.Node)
}
if !ended(o.Run) {
if holdsTheDatabase(o.Run) {
return u.refuse(r, fmt.Sprintf("run %s is still going (its last step is %s): one run at a time", o.Run.Run, lastStep(o.Run)))
}
}
+1 -1
View File
@@ -147,7 +147,7 @@
"id": "state",
"type": "directory",
"mode": "0711",
"place": ".",
"place": "mesh",
"unknown": "program"
},
{
+2 -1
View File
@@ -119,7 +119,8 @@ what is declared, refusing secrets, taking back what is no longer declared, and
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:data}` (`/var/lib/mesh/<module>`): everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:state}`: the package's own directory, declared for one delivery with the package's owner and mode so the node-engine gives it back (issue 1873); the module keeps nothing there.
- `${dir:grants}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/var/lib/mesh-store`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:store-data}/dumps`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
@@ -0,0 +1,94 @@
package main
// The module's directories (novox/hq issue 1873): its state directory said place ".", which is /var/lib/postgres,
// the postgresql package's own (tmpfiles.d/postgresql.conf: postgres, 0700), and its grants directory, placed by
// default, lay inside it; the node-engine gave /var/lib/postgres to the operator's account. What the module keeps
// moves to the mesh's directory for it, and for one delivery the record "state" states the package's owner and mode,
// so the node-engine, which takes a directory it recorded at the same path under the same id as its own, gives it
// back. The store's data is not touched: it is /var/lib/mesh-store, as before.
import (
"encoding/json"
"os"
"reflect"
"strings"
"testing"
)
func postgresManifest(t *testing.T) (map[string]map[string]any, map[string]any) {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "${dir:state}") {
t.Errorf("the manifest still keeps something in ${dir:state}, which is /var/lib/postgres")
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
out := map[string]map[string]any{}
list, _ := m["resources"].([]any)
for _, x := range list {
r, _ := x.(map[string]any)
out[r["id"].(string)] = r
}
return out, m
}
// 1873/AC2: secrets, consumers and grants are kept in <data root>/mesh/postgres; "state" gives /var/lib/postgres back
// to postgres, 0700; the store's data and dumps are as they were.
func TestTheModuleKeepsItsFilesOutsideThePackagesDirectory(t *testing.T) {
rs, m := postgresManifest(t)
data := rs["data"]
if data == nil || data["type"] != "directory" || data["place"] != "mesh" || data["path"] != nil || data["mode"] != "0700" {
t.Fatalf("the module's directory is placed \"mesh\" with no path, 0700 (/var/lib/mesh/postgres): %v", data)
}
if g := rs["grants"]; g == nil || g["path"] != "${dir:data}/grants" || g["place"] != nil {
t.Errorf("grants lies in the module's directory: %v", g)
}
if c := rs["consumers"]; c == nil || c["path"] != "${dir:data}/consumers.json" {
t.Errorf("consumers.json lies in the module's directory: %v", c)
}
own, _ := m["own-secrets"].(map[string]any)
if own["superuser"] != "${dir:data}/superuser.secret" || own["reader"] != "${dir:data}/reader.secret" {
t.Errorf("the own secrets lie in the module's directory: %v", own)
}
state := rs["state"]
if state == nil || state["type"] != "directory" || state["path"] != "/var/lib/postgres" || state["owner"] != "postgres" ||
state["mode"] != "0700" || state["place"] != nil {
t.Errorf("state gives /var/lib/postgres back as postgresql declares it, postgres 0700: %v", state)
}
// The store's data, its dumps and how the container reads them: exactly as before.
if s := rs["store-data"]; !reflect.DeepEqual(s, map[string]any{"id": "store-data", "type": "directory",
"path": "/var/lib/mesh-store", "mode": "0700", "owner": "999:70", "unknown": "program"}) {
t.Errorf("store-data changed: %v", s)
}
if d := rs["dumps"]; !reflect.DeepEqual(d, map[string]any{"id": "dumps", "type": "directory",
"path": "${dir:store-data}/dumps", "mode": "0700", "owner": "999:70", "unknown": "program"}) {
t.Errorf("dumps changed: %v", d)
}
server := rs["server"]
if !reflect.DeepEqual(server["volumes"], []any{"/var/lib/mesh-store:/var/lib/postgresql/data",
"${dir:data}/superuser.secret:/run/secrets/superuser:ro"}) {
t.Errorf("the container's volumes: %v", server["volumes"])
}
if env, _ := server["env"].(map[string]any); env["PGDATA"] != "/var/lib/postgresql/data/pgdata" {
t.Errorf("PGDATA changed: %v", env)
}
build, _ := m["build"].(map[string]any)
artifacts, _ := build["artifacts"].([]any)
env := artifacts[0].(map[string]any)["env"].(map[string]any)
for key, want := range map[string]string{
"MESH_PROVISION_PASSWORD_FILE": "${dir:data}/superuser.secret",
"MESH_POSTGRES_READER_PASSWORD_FILE": "${dir:data}/reader.secret",
"MESH_CONSUMERS_FILE": "${dir:data}/consumers.json",
"MESH_RECEIVES": "${dir:grants}/mesh.json",
} {
if env[key] != want {
t.Errorf("%s is %v, not %s", key, env[key], want)
}
}
}
+18 -9
View File
@@ -61,8 +61,8 @@
"postgres-database": "${dir:grants}"
},
"own-secrets": {
"superuser": "${dir:state}/superuser.secret",
"reader": "${dir:state}/reader.secret"
"superuser": "${dir:data}/superuser.secret",
"reader": "${dir:data}/reader.secret"
},
"data": {
"own": [
@@ -93,22 +93,31 @@
},
"resources": [
{
"id": "state",
"id": "data",
"type": "directory",
"mode": "0700",
"place": ".",
"place": "mesh",
"unknown": "program"
},
{
"id": "state",
"type": "directory",
"path": "/var/lib/postgres",
"owner": "postgres",
"mode": "0700",
"unknown": "program"
},
{
"id": "grants",
"type": "directory",
"path": "${dir:data}/grants",
"mode": "0700",
"unknown": "program"
},
{
"id": "consumers",
"type": "file",
"path": "${dir:state}/consumers.json",
"path": "${dir:data}/consumers.json",
"mode": "0600",
"template": "files/consumers.json"
},
@@ -146,7 +155,7 @@
],
"volumes": [
"/var/lib/mesh-store:/var/lib/postgresql/data",
"${dir:state}/superuser.secret:/run/secrets/superuser:ro"
"${dir:data}/superuser.secret:/run/secrets/superuser:ro"
]
},
{
@@ -169,10 +178,10 @@
],
"env": {
"MESH_PROVISION_POSTGRES": "postgres://postgres@127.0.0.1:${port:5432}/postgres?sslmode=disable",
"MESH_PROVISION_PASSWORD_FILE": "${dir:state}/superuser.secret",
"MESH_PROVISION_PASSWORD_FILE": "${dir:data}/superuser.secret",
"MESH_RECEIVES": "${dir:grants}/mesh.json",
"MESH_POSTGRES_READER_PASSWORD_FILE": "${dir:state}/reader.secret",
"MESH_CONSUMERS_FILE": "${dir:state}/consumers.json"
"MESH_POSTGRES_READER_PASSWORD_FILE": "${dir:data}/reader.secret",
"MESH_CONSUMERS_FILE": "${dir:data}/consumers.json"
}
},
{
+2 -1
View File
@@ -6,4 +6,5 @@ The `systemd` module of the mesh catalogue: installs the package `systemd`; clai
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:data}` (`/var/lib/mesh/<module>`): everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:state}`: the package's own directory, declared for one delivery with the package's owner and mode so the node-engine gives it back (issue 1873); the module keeps nothing there.
@@ -0,0 +1,52 @@
package main
// The module's directories (novox/hq issue 1873): its state directory said place ".", which is /var/lib/systemd,
// systemd's own directory (tmpfiles.d/systemd.conf: d /var/lib/systemd 0755 root root); the node-engine gave it to
// the operator's account, mode 0700, on every machine. What the tools keep moves to the mesh's directory for the
// module, and for one delivery the record "state" states the package's owner and mode, so the node-engine, which
// takes a directory it recorded at the same path under the same id as its own, gives it back.
import (
"encoding/json"
"os"
"testing"
)
func manifestResources(t *testing.T) (map[string]map[string]any, map[string]any) {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
out := map[string]map[string]any{}
list, _ := m["resources"].([]any)
for _, x := range list {
r, _ := x.(map[string]any)
out[r["id"].(string)] = r
}
return out, m
}
// 1873/AC1: the tools' record is kept in <data root>/mesh/systemd; "state" gives /var/lib/systemd back to root, 0755.
func TestTheToolsKeepTheirRecordOutsideSystemdsOwnDirectory(t *testing.T) {
rs, m := manifestResources(t)
data := rs["data"]
if data == nil || data["type"] != "directory" || data["place"] != "mesh" || data["path"] != nil {
t.Fatalf("the tools' directory is placed \"mesh\" with no path (/var/lib/mesh/systemd): %v", data)
}
build, _ := m["build"].(map[string]any)
artifacts, _ := build["artifacts"].([]any)
env := artifacts[0].(map[string]any)["env"].(map[string]any)
if env["MESH_SYSTEMD_STATE"] != "${dir:data}" {
t.Errorf("MESH_SYSTEMD_STATE is %v, not ${dir:data}", env["MESH_SYSTEMD_STATE"])
}
state := rs["state"]
if state == nil || state["type"] != "directory" || state["path"] != "/var/lib/systemd" || state["owner"] != "root" ||
state["mode"] != "0755" || state["place"] != nil {
t.Errorf("state gives /var/lib/systemd back as systemd declares it, root 0755: %v", state)
}
}
+11 -3
View File
@@ -41,7 +41,7 @@
"systemd-tools"
],
"env": {
"MESH_SYSTEMD_STATE": "${dir:state}"
"MESH_SYSTEMD_STATE": "${dir:data}"
}
}
]
@@ -53,10 +53,18 @@
"package": "systemd"
},
{
"id": "state",
"id": "data",
"type": "directory",
"mode": "0700",
"place": ".",
"place": "mesh",
"unknown": "program"
},
{
"id": "state",
"type": "directory",
"path": "/var/lib/systemd",
"owner": "root",
"mode": "0755",
"unknown": "program"
}
]