mesh/merge-gate head 24347ea5a02b merged into trunk 84deb530ee5a — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 24347ea5a02b merged into trunk 84deb530ee5a — pass: its merge-check.sh passed
mesh/delivery delivered
A resource on the resolver file was left out by the check and still composed by the declaration, so an uplink declaring it as a resource beside the resolver passed with two writers; it is refused again, as before. And /etc//hosts or /etc/../etc/hosts is /etc/hosts to the machine, so it is compared cleaned. A seat names no root or top-level directory.
316 lines
12 KiB
Go
316 lines
12 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// The paths a seat's holder owns (novox/hq ADR 0287).
|
|
//
|
|
// **Who owns a machine-wide file is the role's question, answered once.** Until this, which module owned
|
|
// /etc/hosts or /etc/resolv.conf was decided record by record (ADRs 0199, 0222, 0223, 0241) and enforced
|
|
// only after the fact, by checkResources finding two modules on one machine declaring the same path: a
|
|
// module that did not hold node-hostname could declare /etc/hosts on a machine where nobody held it, and
|
|
// nothing refused it. Now a seat's definition names the paths the role owns on any machine, and three
|
|
// refusals follow from it, each where the mesh can still say no:
|
|
//
|
|
// - a claim whose module does not declare every path its seat names, by CanHold, at registration and at
|
|
// handover;
|
|
// - a module declaring a path a seat names while claiming none of the seats that name it, at
|
|
// registration (CatalogueProblems), and what the controller generates for a machine, at composition;
|
|
// - a module's own seat naming a path another seat already names, at registration — the second loses.
|
|
//
|
|
// Reading a seat's path is access, never a resource, so it stays allowed.
|
|
|
|
// SeatPath is one path a seat's holder owns: absolute, literal, on the machine where the holder runs.
|
|
type SeatPath struct {
|
|
Path string `json:"path"`
|
|
// StepsBackFor is a node seat whose holder owns the path instead, on a machine where that seat is held
|
|
// (novox/hq ADR 0287, ADR 0247): this seat's holder still declares the path, and its declaration is not
|
|
// composed there. Only the mesh's own seats step back, each by a record.
|
|
StepsBackFor string `json:"steps-back-for,omitempty"`
|
|
}
|
|
|
|
// seatPathProblem is what is wrong with one path a seat names, judged alone: absolute, written literally
|
|
// with no placeholder, and clean, so it compares with what a module declares byte for byte.
|
|
func seatPathProblem(seat, scope, p string) string {
|
|
switch {
|
|
case scope != ScopeNode:
|
|
return fmt.Sprintf("the seat %s names the path %q and is a %s seat; only a node seat names paths, "+
|
|
"because a path is a machine's (novox/hq ADR 0287)", seat, p, scope)
|
|
case strings.Contains(p, "${"):
|
|
return fmt.Sprintf("the seat %s names the path %q, which has a placeholder; a seat's path is written "+
|
|
"literally (novox/hq ADR 0287)", seat, p)
|
|
case !strings.HasPrefix(p, "/") || path.Clean(p) != p:
|
|
return fmt.Sprintf("the seat %s names the path %q, which is not a clean absolute path (novox/hq ADR 0287)",
|
|
seat, p)
|
|
case path.Dir(p) == "/":
|
|
// The root or a top-level directory is every role's, never one role's own (ADR 0287).
|
|
return fmt.Sprintf("the seat %s names the path %q, the root or a top-level directory, which no one "+
|
|
"role owns (novox/hq ADR 0287)", seat, p)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SeatPathProblems is what is wrong with the paths a set of the mesh's own seats names (novox/hq ADR
|
|
// 0287): only a node seat names one, each absolute and literal; every path has exactly one seat naming it
|
|
// without stepping back; and a seat steps back only for another node seat naming the same path without
|
|
// stepping back, so the stepping back cannot cycle.
|
|
func SeatPathProblems(set []Seat) []string {
|
|
var problems []string
|
|
owners := map[string][]string{} // path → the seats naming it without stepping back
|
|
stepping := map[string][]SeatPath{} // seat → the paths it names stepping back
|
|
byName := map[string]Seat{}
|
|
for _, s := range set {
|
|
byName[s.Name] = s
|
|
for _, p := range s.Paths {
|
|
if why := seatPathProblem(s.Name, s.Scope, p.Path); why != "" {
|
|
problems = append(problems, why)
|
|
}
|
|
if p.StepsBackFor == "" {
|
|
owners[p.Path] = append(owners[p.Path], s.Name)
|
|
} else {
|
|
stepping[s.Name] = append(stepping[s.Name], p)
|
|
}
|
|
}
|
|
}
|
|
for _, s := range set {
|
|
for _, p := range stepping[s.Name] {
|
|
other, known := byName[p.StepsBackFor]
|
|
switch {
|
|
case !known || other.Scope != ScopeNode:
|
|
problems = append(problems, fmt.Sprintf("the seat %s steps back from %q for %q, which is not a "+
|
|
"node seat of the mesh's (novox/hq ADR 0287)", s.Name, p.Path, p.StepsBackFor))
|
|
case !namesWithoutSteppingBack(other, p.Path):
|
|
problems = append(problems, fmt.Sprintf("the seat %s steps back from %q for %s, which does not "+
|
|
"own that path itself; a seat steps back only for the one that owns it (novox/hq ADR 0287)",
|
|
s.Name, p.Path, other.Name))
|
|
}
|
|
}
|
|
}
|
|
for _, p := range sortedKeys(owners) {
|
|
if len(owners[p]) > 1 {
|
|
problems = append(problems, fmt.Sprintf("the path %q is named by %s, each without stepping back; "+
|
|
"no two seats name one path (novox/hq ADR 0287)", p, strings.Join(owners[p], " and ")))
|
|
}
|
|
}
|
|
for _, s := range set {
|
|
for _, p := range stepping[s.Name] {
|
|
if len(owners[p.Path]) == 0 {
|
|
problems = append(problems, fmt.Sprintf("the path %q has no seat naming it without stepping back "+
|
|
"(novox/hq ADR 0287)", p.Path))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
func namesWithoutSteppingBack(s Seat, p string) bool {
|
|
for _, sp := range s.Paths {
|
|
if sp.Path == p && sp.StepsBackFor == "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// declaredPaths is every machine path a module declares it owns: a resource's literal path, and a file
|
|
// the mesh renders for it outside the operator's home. An access is not here — reading is not owning.
|
|
//
|
|
// **Cleaned, because the machine writes the cleaned path**: /etc//hosts, /etc/./hosts, /etc/../etc/hosts and
|
|
// /etc/hosts/ are all /etc/hosts to it, and a seat's path is clean by its own rule, so a declaration compared
|
|
// as written would pass for a path nobody named.
|
|
func declaredPaths(m Manifest) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, r := range m.Resources {
|
|
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
|
|
out[path.Clean(p)] = true
|
|
}
|
|
}
|
|
for _, f := range m.Facts {
|
|
if !f.Home && strings.HasPrefix(f.Path, "/") {
|
|
out[path.Clean(f.Path)] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// declaresPath says whether a module declares one path, as a resource or as a file the mesh renders.
|
|
func declaresPath(m Manifest, p string) bool { return declaredPaths(m)[p] }
|
|
|
|
// missingPaths is each path a seat names that a module does not declare, in the seat's order.
|
|
func missingPaths(m Manifest, paths []SeatPath) []string {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
has := declaredPaths(m)
|
|
var missing []string
|
|
for _, p := range paths {
|
|
if !has[p.Path] {
|
|
missing = append(missing, p.Path)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
// pathSeats is which seats name each path: the mesh's own, and then those modules declare, in the order
|
|
// given. A module's seat naming a path a seat already names loses it, and the refusal says so (novox/hq
|
|
// ADR 0287 — as for a seat's name, the second declaration loses).
|
|
func pathSeats(order []string, shelf Shelf, declaredBy map[string]string) (map[string][]string, []string) {
|
|
named := map[string][]string{}
|
|
for _, s := range Seats() {
|
|
for _, p := range s.Paths {
|
|
named[p.Path] = append(named[p.Path], s.Name)
|
|
}
|
|
}
|
|
var problems []string
|
|
for _, module := range order {
|
|
for _, s := range shelf[module].DefinesSeats {
|
|
if declaredBy != nil && declaredBy[s.Name] != module {
|
|
continue // a second declaration of the seat's name, refused for that already
|
|
}
|
|
for _, p := range s.Paths {
|
|
if others := named[p]; len(others) > 0 {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the seat %s naming the path %q, which %s already names; no two seats "+
|
|
"name one path (novox/hq ADR 0287)", module, s.Name, p, strings.Join(others, " and ")))
|
|
continue
|
|
}
|
|
named[p] = append(named[p], s.Name)
|
|
}
|
|
}
|
|
}
|
|
return named, problems
|
|
}
|
|
|
|
// unclaimedSeatPaths is each path a module declares that a seat names while it claims none of the seats
|
|
// naming it (novox/hq ADR 0287): only a claimant declares a seat's path, and a module claiming any one of
|
|
// them may.
|
|
func unclaimedSeatPaths(m Manifest, named map[string][]string) []string {
|
|
claims := map[string]bool{}
|
|
for _, c := range m.Claims {
|
|
claims[canonicalSeat(c.Name)] = true
|
|
}
|
|
var problems []string
|
|
declared := declaredPaths(m)
|
|
for _, p := range sortedKeys(declared) {
|
|
seats := named[p]
|
|
if len(seats) == 0 {
|
|
continue
|
|
}
|
|
claimsOne := false
|
|
for _, s := range seats {
|
|
claimsOne = claimsOne || claims[s]
|
|
}
|
|
if !claimsOne {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the path %q, which the seat %s names, and claims none of them; only a holder of "+
|
|
"a seat naming a path declares it (novox/hq ADR 0287)", m.Module, p, strings.Join(seats, " and ")))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// declaredSeatPathProblems is what one module's own seat declaration can be judged on alone: a path
|
|
// only on a node seat, absolute, literal and clean.
|
|
func declaredSeatPathProblems(m Manifest, s SeatDeclaration) []string {
|
|
var problems []string
|
|
for _, p := range s.Paths {
|
|
if why := seatPathProblem(s.Name, s.At(), p); why != "" {
|
|
problems = append(problems, m.Module+": "+why)
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// asSeatPaths is a module's own seat's paths in the shape the mesh's seats name theirs. A module's seat
|
|
// never steps back: only the mesh's own seats do, each by a record.
|
|
func asSeatPaths(paths []string) []SeatPath {
|
|
out := make([]SeatPath, 0, len(paths))
|
|
for _, p := range paths {
|
|
out = append(out, SeatPath{Path: p})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// PathOwner is the module that owns one seat's path on one machine (novox/hq ADR 0287): the seat naming
|
|
// the path, and that seat's holder there, with the stepping back applied.
|
|
type PathOwner struct {
|
|
Path string `json:"path"`
|
|
Node string `json:"node"`
|
|
Seat string `json:"seat"`
|
|
Module string `json:"module"`
|
|
}
|
|
|
|
// PathOwners is, for every path a seat in the set names and every machine where a seat naming it is held,
|
|
// who owns the path there. A seat that steps back for another held on the same machine is passed over.
|
|
func PathOwners(set []Seat, held []Held) []PathOwner {
|
|
// node → seat → holding module
|
|
holders := map[string]map[string]string{}
|
|
for _, h := range held {
|
|
s, ok := SeatNamed(h.Claim)
|
|
if !ok || h.Scope != ScopeNode || s.Scope != ScopeNode {
|
|
continue
|
|
}
|
|
if holders[h.Node] == nil {
|
|
holders[h.Node] = map[string]string{}
|
|
}
|
|
if _, taken := holders[h.Node][s.Name]; !taken {
|
|
holders[h.Node][s.Name] = h.Module
|
|
}
|
|
}
|
|
naming := map[string][]Seat{} // path → the seats naming it
|
|
steps := map[string]map[string]string{} // path → seat → the seat it steps back for
|
|
for _, s := range set {
|
|
for _, p := range s.Paths {
|
|
naming[p.Path] = append(naming[p.Path], s)
|
|
if p.StepsBackFor != "" {
|
|
if steps[p.Path] == nil {
|
|
steps[p.Path] = map[string]string{}
|
|
}
|
|
steps[p.Path][s.Name] = p.StepsBackFor
|
|
}
|
|
}
|
|
}
|
|
var out []PathOwner
|
|
for _, p := range sortedKeys(naming) {
|
|
for _, node := range sortedKeys(holders) {
|
|
for _, s := range naming[p] {
|
|
module, holds := holders[node][s.Name]
|
|
if !holds {
|
|
continue
|
|
}
|
|
if forSeat := steps[p][s.Name]; forSeat != "" {
|
|
if _, there := holders[node][forSeat]; there {
|
|
continue
|
|
}
|
|
}
|
|
out = append(out, PathOwner{Path: p, Node: node, Seat: s.Name, Module: module})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// seatPathsOnMachine is each path the modules of one machine declare that a seat names, for a module
|
|
// claiming none of the seats naming it (novox/hq ADR 0287). Registration refuses the same of a manifest;
|
|
// this holds what the controller generates for a machine to it too, because a computed module's resources
|
|
// exist only once composed (checkResources is asked them there). The seats are the mesh's own and those the
|
|
// machine's modules declare.
|
|
func seatPathsOnMachine(modules []Manifest) []string {
|
|
shelf := Shelf{}
|
|
var order []string
|
|
for _, m := range modules {
|
|
shelf[m.Module] = m
|
|
order = append(order, m.Module)
|
|
}
|
|
named, _ := pathSeats(order, shelf, nil)
|
|
var problems []string
|
|
for _, m := range modules {
|
|
problems = append(problems, unclaimedSeatPaths(m, named)...)
|
|
}
|
|
return problems
|
|
}
|