Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
321 lines
12 KiB
Go
321 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
|
|
}
|
|
}
|
|
for _, p := range m.RunsAt {
|
|
if strings.HasPrefix(p, "/") {
|
|
out[path.Clean(p)] = 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
|
|
}
|