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) } 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. 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[p] = true } } for _, f := range m.Facts { if !f.Home && strings.HasPrefix(f.Path, "/") { out[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 }