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.
411 lines
13 KiB
Go
411 lines
13 KiB
Go
// Command owned judges how the catalogue's directories and archives treat files they did not write (novox/hq
|
|
// ADR 0422 §8, issue 1024). It refuses, naming the module, the resource and the reason:
|
|
//
|
|
// 1. a directory or archive without `unknown`, or with a value but move-out, module or program;
|
|
//
|
|
// 2. `move-out` on a path not under ${machine:account-home} or ${machine:agent-home};
|
|
//
|
|
// 3. `knows` with an empty, absolute, `.`/`..`/empty-component or repeated path;
|
|
//
|
|
// 4. a path one module declares (a resource's path, or a fact with "home": true) lying inside a directory of
|
|
// 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).
|
|
//
|
|
// 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"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"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}"
|
|
|
|
var (
|
|
ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
|
|
homeVar = strings.NewReplacer("${machine:account-home}", Home, "${machine:agent-home}", Home)
|
|
values = map[string]bool{"move-out": true, "module": true, "program": true}
|
|
)
|
|
|
|
// Finding is one refusal.
|
|
type Finding struct {
|
|
Rule int
|
|
Module, ID, Why string
|
|
}
|
|
|
|
type resource struct {
|
|
ID, Type, Path, Unknown, Place string
|
|
Knows []string
|
|
HasKnows bool
|
|
}
|
|
|
|
type parsed struct {
|
|
Module string
|
|
Resources []resource
|
|
Facts []string // paths of facts with "home": true, under the home
|
|
}
|
|
|
|
func load(file string) (*parsed, error) {
|
|
raw, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Resources []struct {
|
|
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"`
|
|
Facts map[string]struct {
|
|
Path string `json:"path"`
|
|
Home bool `json:"home"`
|
|
} `json:"facts"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", file, err)
|
|
}
|
|
out := &parsed{Module: m.Module}
|
|
if out.Module == "" {
|
|
out.Module = filepath.Base(filepath.Dir(file))
|
|
}
|
|
for _, r := range m.Resources {
|
|
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 == "" {
|
|
x.Unknown = "(empty)"
|
|
}
|
|
}
|
|
out.Resources = append(out.Resources, x)
|
|
}
|
|
keys := make([]string, 0, len(m.Facts))
|
|
for k := range m.Facts {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
if f := m.Facts[k]; f.Home && f.Path != "" {
|
|
out.Facts = append(out.Facts, f.Path)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// norm resolves ${dir:<id>} from the module's own resources, reads both homes as one root and cleans the path.
|
|
func norm(p string, dirs map[string]string) string {
|
|
for i := 0; i < 8 && ofDir.MatchString(p); i++ {
|
|
p = ofDir.ReplaceAllStringFunc(p, func(w string) string {
|
|
if to, ok := dirs[ofDir.FindStringSubmatch(w)[1]]; ok {
|
|
return to
|
|
}
|
|
return w
|
|
})
|
|
}
|
|
p = homeVar.Replace(p)
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
return path.Clean(p)
|
|
}
|
|
|
|
func underHome(p string) bool { return strings.HasPrefix(p, Home+"/") }
|
|
|
|
// badKnows says why a knows entry is refused, or "".
|
|
func badKnows(k string) string {
|
|
if k == "" {
|
|
return "is empty"
|
|
}
|
|
if strings.HasPrefix(k, "/") {
|
|
return "is absolute"
|
|
}
|
|
for _, c := range strings.Split(strings.TrimSuffix(k, "/"), "/") {
|
|
if c == "" || c == "." || c == ".." {
|
|
return "has an empty, . or .. component"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type declared struct {
|
|
module, id, path string
|
|
}
|
|
|
|
// Check reads every manifest under root/modules and answers what it refuses.
|
|
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 {
|
|
m, err := load(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dirs := map[string]string{}
|
|
for _, r := range m.Resources {
|
|
switch {
|
|
case r.ID != "" && r.Path != "":
|
|
dirs[r.ID] = r.Path
|
|
case r.ID != "" && r.Type == "directory":
|
|
// A directory the mesh places for the module (`place`): where it lies is the node-engine's, but it is
|
|
// the module's own, so a path under it belongs to this module and to no other's directory of the same
|
|
// id (issue 1484: two runs-as modules each keep a .ssh in their own state directory).
|
|
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 != "" {
|
|
paths = append(paths, declared{m.Module, r.ID, p})
|
|
}
|
|
if r.Type != "directory" && r.Type != "archive" {
|
|
continue
|
|
}
|
|
if r.Type == "directory" && p != "" {
|
|
containers = append(containers, container{m.Module, p})
|
|
}
|
|
add := func(rule int, why string) { out = append(out, Finding{rule, m.Module, r.ID, why}) }
|
|
switch {
|
|
case r.Unknown == "":
|
|
add(1, "a "+r.Type+" without `unknown` (move-out, module or program)")
|
|
case !values[r.Unknown]:
|
|
add(1, fmt.Sprintf("`unknown` is %q, not move-out, module or program", r.Unknown))
|
|
case r.Unknown == "move-out" && !underHome(p):
|
|
add(2, fmt.Sprintf("`move-out` on %q, which is not under ${machine:account-home} or ${machine:agent-home}", r.Path))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, k := range r.Knows {
|
|
if why := badKnows(k); why != "" {
|
|
add(3, fmt.Sprintf("`knows` entry %q %s", k, why))
|
|
continue
|
|
}
|
|
c := strings.TrimSuffix(k, "/")
|
|
if seen[c] {
|
|
add(3, fmt.Sprintf("`knows` entry %q is repeated", k))
|
|
}
|
|
seen[c] = true
|
|
}
|
|
if r.HasKnows && len(r.Knows) == 0 {
|
|
add(3, "`knows` is empty")
|
|
}
|
|
}
|
|
for _, f := range m.Facts {
|
|
if p := norm(Home+"/"+f, nil); p != "" {
|
|
paths = append(paths, declared{m.Module, "fact " + f, p})
|
|
}
|
|
}
|
|
}
|
|
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+"/")) {
|
|
out = append(out, Finding{4, d.module, d.id, fmt.Sprintf("declares %q, inside a directory of %s (%q)", d.path, c.module, c.path)})
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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 {
|
|
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, or in a system package's")
|
|
}
|