841 lines
29 KiB
Go
841 lines
29 KiB
Go
package catalogue
|
||
|
||
import (
|
||
"fmt"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// Libraries, and the modules that join them (novox/hq ADR 0328).
|
||
//
|
||
// **A library is a named tree of shared data with one home** (§1). The operator defines it — its name, the machine it
|
||
// lives on, its root there and the machines it reaches — as the mesh-wide `libraries` setting of the controller's own
|
||
// module, a trusted setting set at the controller's terminal or on the operator's Approve of a proposal (ADR 0277),
|
||
// never through a verb alone. Libraries never overlap on a machine (§13), and a library never shares a name with a
|
||
// module (§4), so `mesh-<library>` and `mesh-<module>` never collide.
|
||
//
|
||
// **A module asks for a library by name, sub-path and use** (§2): a manifest's `libraries` entries {id, library,
|
||
// path, use}. On each machine an entry resolves to the library's root joined with the path; a module assigned to a
|
||
// machine the library does not reach is left out there, naming the library. `${library:<id>}` is that host path, and
|
||
// is filled only where a host path belongs (§5): a container's volume source, and a word of the module's tools, which
|
||
// run on the machine. A `read` entry is mounted read-only (§10).
|
||
//
|
||
// **A module joins by using the model** (§14): it declares `libraries` or uses `${identity:…}`. It is given one number
|
||
// from the mesh's range, as its user's and its own group's (§4); its user's primary group is the group of the first
|
||
// library it uses, the others are added groups, and with no library its own group is primary. `${identity:user}`,
|
||
// `${identity:uid}`, `${identity:group}` and `${identity:gid}` are filled wherever its resources take values (§5), and
|
||
// its directories are owned by its user and primary group (§4). A module that does neither is composed exactly as
|
||
// before.
|
||
//
|
||
// **The range is 50000–59999** for every number the mesh allocates — a joining module's and a library's group — from
|
||
// one sequence, so a module's own group and a library's never share a number. It lies above the numbers distributions
|
||
// give people and below the 60000s Debian keeps for its daemons, and nowhere near root's 0 or a root group's (0, adm's 4,
|
||
// wheel's 10): no allocated number is root's (§15). The store refuses a row outside it.
|
||
//
|
||
// **Only to an engine that reads it** (Rendering.KeepsLibraries: its statement's contract is link.LibraryContract or
|
||
// later), the declaration carries `groups`, `identities` and `libraries` (libraryEnvelope). To an older engine a module
|
||
// that joins is left out of the declaration, naming why — its placeholders could not be filled — and everything else is
|
||
// composed as before.
|
||
|
||
// The range the mesh allocates user and group numbers from (novox/hq ADR 0328 §3, §4, §15). The store holds the same
|
||
// bounds.
|
||
const (
|
||
IdentityLow = 50000
|
||
IdentityHigh = 59999
|
||
)
|
||
|
||
// AllocatableID says whether n is a number the mesh may give: inside the range, so never 0 nor a root group's.
|
||
func AllocatableID(n int) bool { return n >= IdentityLow && n <= IdentityHigh }
|
||
|
||
// ADR0328 is how the checks name the decision their rules come from.
|
||
const ADR0328 = "novox/hq ADR 0328"
|
||
|
||
// MeshName is the user or group a module or a library is on every machine: mesh-<name>.
|
||
func MeshName(name string) string { return "mesh-" + name }
|
||
|
||
// IdentityName is the user a joining module runs as, and its own group.
|
||
func IdentityName(module string) string { return MeshName(module) }
|
||
|
||
// ---- libraries, as the operator defines them -----------------------------------------------------------------
|
||
|
||
// LibrariesSetting is the key of the controller's mesh-wide settings layer that defines the libraries (§1).
|
||
const LibrariesSetting = "libraries"
|
||
|
||
// LibrariesModule is the module whose mesh-wide layer holds LibrariesSetting: the controller's own.
|
||
const LibrariesModule = "mesh-controller"
|
||
|
||
// Library is one library as the operator defined it.
|
||
type Library struct {
|
||
Name string `json:"-"`
|
||
// Home is the machine the data lives on; Root its path there, the same on every machine it reaches.
|
||
Home string `json:"home"`
|
||
Root string `json:"root"`
|
||
// Reach is every machine the library reaches, its home among them.
|
||
Reach []string `json:"reach,omitempty"`
|
||
}
|
||
|
||
// Reaches says whether the library reaches a machine: its home, or one it names.
|
||
func (l Library) Reaches(node string) bool { return node == l.Home || hasName(l.Reach, node) }
|
||
|
||
// Machines is every machine the library reaches, its home first.
|
||
func (l Library) Machines() []string {
|
||
out := []string{l.Home}
|
||
for _, n := range l.Reach {
|
||
if n != l.Home && !hasName(out, n) {
|
||
out = append(out, n)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Group is the library's group: mesh-<library>.
|
||
func (l Library) Group() string { return MeshName(l.Name) }
|
||
|
||
var libraryName = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
||
|
||
// ParseLibraries reads the `libraries` setting: {<name>: {home, root, reach}}, or refuses it in words.
|
||
func ParseLibraries(v any) (map[string]Library, error) {
|
||
if v == nil {
|
||
return nil, nil
|
||
}
|
||
blocks, ok := v.(map[string]any)
|
||
if !ok {
|
||
return nil, fmt.Errorf("%s is a { <library>: {home, root, reach} } map (%s §1)", LibrariesSetting, ADR0328)
|
||
}
|
||
out := map[string]Library{}
|
||
for name, raw := range blocks {
|
||
if !libraryName.MatchString(name) {
|
||
return nil, fmt.Errorf("the library %q: a library's name is lowercase letters, digits and dashes, starting "+
|
||
"with a letter", name)
|
||
}
|
||
fields, ok := raw.(map[string]any)
|
||
if !ok {
|
||
return nil, fmt.Errorf("the library %s is {home, root, reach}, not %v", name, raw)
|
||
}
|
||
l := Library{Name: name}
|
||
for k, v := range fields {
|
||
switch k {
|
||
case "home":
|
||
l.Home, _ = v.(string)
|
||
case "root":
|
||
l.Root, _ = v.(string)
|
||
case "reach":
|
||
list, ok := v.([]any)
|
||
if !ok {
|
||
return nil, fmt.Errorf("the library %s's reach is a list of machines", name)
|
||
}
|
||
for _, n := range list {
|
||
s, _ := n.(string)
|
||
if s == "" {
|
||
return nil, fmt.Errorf("the library %s's reach names a machine that is not a name: %v", name, n)
|
||
}
|
||
l.Reach = append(l.Reach, s)
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("the library %s says %q; a library is its home, its root and its reach, and "+
|
||
"nothing about an owner, a group, a mode or a number (%s §1)", name, k, ADR0328)
|
||
}
|
||
}
|
||
if strings.TrimSpace(l.Home) == "" {
|
||
return nil, fmt.Errorf("the library %s names no home machine", name)
|
||
}
|
||
if !strings.HasPrefix(l.Root, "/") {
|
||
return nil, fmt.Errorf("the library %s's root %q is not an absolute path", name, l.Root)
|
||
}
|
||
l.Root = filepath.Clean(l.Root)
|
||
if why := systemPath(l.Root); why != "" {
|
||
return nil, fmt.Errorf("the library %s's root is %s: %s, and the node-engine keeps a library's root in its "+
|
||
"group — no library is ever there", name, l.Root, why)
|
||
}
|
||
sort.Strings(l.Reach)
|
||
out[name] = l
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// LibrariesProblems refuses what no library may be together (§4, §13): a library named as a registered module, and two
|
||
// libraries on one machine whose roots lie inside each other — each named.
|
||
func LibrariesProblems(libs map[string]Library, modules []string) []string {
|
||
var problems []string
|
||
names := make([]string, 0, len(libs))
|
||
for n := range libs {
|
||
names = append(names, n)
|
||
}
|
||
sort.Strings(names)
|
||
for _, n := range names {
|
||
if hasName(modules, n) {
|
||
problems = append(problems, fmt.Sprintf("the library %s has the name of the module %s: %s would be both "+
|
||
"(%s §4)", n, n, MeshName(n), ADR0328))
|
||
}
|
||
}
|
||
for i, a := range names {
|
||
for _, b := range names[i+1:] {
|
||
la, lb := libs[a], libs[b]
|
||
shared := ""
|
||
for _, m := range la.Machines() {
|
||
if lb.Reaches(m) {
|
||
shared = m
|
||
break
|
||
}
|
||
}
|
||
if shared == "" {
|
||
continue
|
||
}
|
||
if within(la.Root, lb.Root) || within(lb.Root, la.Root) {
|
||
problems = append(problems, fmt.Sprintf("the libraries %s (%s) and %s (%s) overlap on %s: a library's "+
|
||
"root lies neither inside another's nor around it — divide one library by the paths its modules "+
|
||
"ask for (%s §13)", a, la.Root, b, lb.Root, shared, ADR0328))
|
||
}
|
||
}
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// within says path is root or lies below it.
|
||
func within(path, root string) bool {
|
||
return path == root || strings.HasPrefix(path, strings.TrimSuffix(root, "/")+"/")
|
||
}
|
||
|
||
func hasName(list []string, s string) bool {
|
||
for _, x := range list {
|
||
if x == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ---- a module's use of them ---------------------------------------------------------------------------------
|
||
|
||
// LibraryUse is one `libraries` entry of a manifest: the id the module's resources name it by, the library, the
|
||
// sub-path within it, and the use.
|
||
type LibraryUse struct {
|
||
ID string `json:"id"`
|
||
Library string `json:"library"`
|
||
Path string `json:"path"`
|
||
Use string `json:"use"`
|
||
}
|
||
|
||
// At is the entry's use, read by default.
|
||
func (u LibraryUse) At() string {
|
||
if u.Use == "" {
|
||
return AccessRead
|
||
}
|
||
return u.Use
|
||
}
|
||
|
||
// libraryRef is how a module names one of its library entries: ${library:<id>}.
|
||
var libraryRef = regexp.MustCompile(`\$\{library:([a-z0-9][a-z0-9-]*)\}`)
|
||
|
||
// identityRef is a module's identity by name: ${identity:user|uid|group|gid}.
|
||
var identityRef = regexp.MustCompile(`\$\{identity:(user|uid|group|gid)\}`)
|
||
|
||
// Joins says a module joins the model (§14): it declares libraries or uses its identity anywhere.
|
||
func Joins(m Manifest) bool {
|
||
if len(m.Libraries) > 0 {
|
||
return true
|
||
}
|
||
found := false
|
||
for _, r := range m.Resources {
|
||
walkStrings(r, func(s string) {
|
||
found = found || identityRef.MatchString(s) || strings.Contains(s, "${identity:")
|
||
})
|
||
}
|
||
for _, b := range m.Bundles {
|
||
for _, v := range b.Env {
|
||
found = found || strings.Contains(v, "${identity:")
|
||
}
|
||
}
|
||
return found
|
||
}
|
||
|
||
// walkStrings calls see with every string a value holds, however deep.
|
||
func walkStrings(v any, see func(string)) {
|
||
switch v := v.(type) {
|
||
case string:
|
||
see(v)
|
||
case []any:
|
||
for _, x := range v {
|
||
walkStrings(x, see)
|
||
}
|
||
case map[string]any:
|
||
for _, x := range v {
|
||
walkStrings(x, see)
|
||
}
|
||
}
|
||
}
|
||
|
||
// mapStrings is v with every string it holds replaced by fill's answer, however deep; the first error stops it.
|
||
func mapStrings(v any, fill func(string) (string, error)) (any, error) {
|
||
switch v := v.(type) {
|
||
case string:
|
||
return fill(v)
|
||
case []any:
|
||
out := make([]any, len(v))
|
||
for i, x := range v {
|
||
y, err := mapStrings(x, fill)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out[i] = y
|
||
}
|
||
return out, nil
|
||
case map[string]any:
|
||
out := make(map[string]any, len(v))
|
||
for k, x := range v {
|
||
y, err := mapStrings(x, fill)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out[k] = y
|
||
}
|
||
return out, nil
|
||
}
|
||
return v, nil
|
||
}
|
||
|
||
var libraryEntryID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||
|
||
// numberWords are the setting names that carry a user or group number or an owner: none for a module that joins (§5).
|
||
// "user" and "group" are left out: an application's own login or group name is a setting like any other.
|
||
var numberWords = map[string]bool{"uid": true, "gid": true, "puid": true, "pgid": true, "user-id": true,
|
||
"group-id": true, "owner": true}
|
||
|
||
// libraryProblems is what the module check refuses of a manifest's libraries and identity (§2, §4, §5, §14):
|
||
//
|
||
// - an entry that is not {id, library, path, use} with a relative sub-path inside its library, or names a host
|
||
// path, an owner, a group, a mode or a number (the entry's shape refuses any other key);
|
||
// - two entries with one id, or a `${library:<id>}` naming none;
|
||
// - `${library:<id>}` anywhere but a container's volume source (and a word of its tools, a bundle's env);
|
||
// - `${identity:…}` other than user, uid, group or gid;
|
||
// - in a module that joins: a directory naming its owner. A setting of it naming a number or an owner is refused
|
||
// where it is set (JoiningSettingProblem).
|
||
func libraryProblems(m Manifest) []string {
|
||
var problems []string
|
||
ids := map[string]bool{}
|
||
for _, u := range m.Libraries {
|
||
where := fmt.Sprintf("%s's library entry %q", m.Module, u.ID)
|
||
switch {
|
||
case !libraryEntryID.MatchString(u.ID):
|
||
problems = append(problems, where+": an id is lowercase letters, digits and dashes")
|
||
case ids[u.ID]:
|
||
problems = append(problems, where+": two entries share the id")
|
||
}
|
||
ids[u.ID] = true
|
||
if !libraryName.MatchString(u.Library) {
|
||
problems = append(problems, fmt.Sprintf("%s names the library %q, which is not a library's name", where,
|
||
u.Library))
|
||
}
|
||
clean := filepath.Clean(u.Path)
|
||
if u.Path == "" || strings.HasPrefix(u.Path, "/") || clean == ".." || strings.HasPrefix(clean, "../") ||
|
||
strings.ContainsAny(u.Path, "${}") {
|
||
problems = append(problems, fmt.Sprintf("%s's path %q is a sub-path inside the library, relative and "+
|
||
"staying inside it — never a host path (%s §2)", where, u.Path, ADR0328))
|
||
}
|
||
if u.At() != AccessRead && u.At() != AccessReadWrite {
|
||
problems = append(problems, fmt.Sprintf("%s is used %q; a use is %q or %q", where, u.Use, AccessRead,
|
||
AccessReadWrite))
|
||
}
|
||
}
|
||
for _, r := range m.Resources {
|
||
kind := fmt.Sprint(r["type"])
|
||
for field, v := range r {
|
||
walkStrings(v, func(s string) {
|
||
for _, tok := range regexp.MustCompile(`\$\{identity:[^}]*\}`).FindAllString(s, -1) {
|
||
if !identityRef.MatchString(tok) {
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v says %s: the identity is "+
|
||
"${identity:user}, ${identity:uid}, ${identity:group} or ${identity:gid} (%s §5)", m.Module,
|
||
r["id"], tok, ADR0328))
|
||
}
|
||
}
|
||
for _, match := range libraryRef.FindAllStringSubmatch(s, -1) {
|
||
if !ids[match[1]] {
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v says ${library:%s}, and declares no "+
|
||
"library entry %q", m.Module, r["id"], match[1], match[1]))
|
||
}
|
||
source := ""
|
||
if kind == "container" && field == "volumes" {
|
||
source = volumeSource(s)
|
||
}
|
||
if !strings.Contains(source, match[0]) {
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v says %s in its %s: a library's path "+
|
||
"is a host path, filled only in a container's volume source — the process reads the "+
|
||
"container side its volume gives (%s §5)", m.Module, r["id"], match[0], field, ADR0328))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
if !Joins(m) {
|
||
return problems
|
||
}
|
||
for _, r := range m.Resources {
|
||
if fmt.Sprint(r["type"]) == "directory" {
|
||
if owner, said := r["owner"]; said {
|
||
problems = append(problems, fmt.Sprintf("%s: the directory %v names its owner %v; a module that joins "+
|
||
"owns its directories by its user and primary group, and names no owner (%s §4)", m.Module,
|
||
r["id"], owner, ADR0328))
|
||
}
|
||
}
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// LibraryWarnings is what the module check warns of a module's libraries (§4): an image that drops added groups
|
||
// reaches only the first library the module uses.
|
||
func LibraryWarnings(m Manifest) []string {
|
||
seen := map[string]bool{}
|
||
var libs []string
|
||
for _, u := range m.Libraries {
|
||
if !seen[u.Library] {
|
||
seen[u.Library] = true
|
||
libs = append(libs, u.Library)
|
||
}
|
||
}
|
||
if len(libs) < 2 {
|
||
return nil
|
||
}
|
||
return []string{fmt.Sprintf("%s uses the libraries %s: its user's primary group is %s, and %s are added groups — an "+
|
||
"image that drops added groups when it changes user reaches only %s (%s §4)", m.Module, strings.Join(libs, ", "),
|
||
MeshName(libs[0]), strings.Join(meshNames(libs[1:]), ", "), libs[0], ADR0328)}
|
||
}
|
||
|
||
func meshNames(names []string) []string {
|
||
out := make([]string, len(names))
|
||
for i, n := range names {
|
||
out[i] = MeshName(n)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// JoiningSettingProblem refuses, where a setting is set, a layer of a module that joins that carries a user or group
|
||
// number or an owner (§5): a key whose name says so, or an owner a `places` setting names — its directories are owned
|
||
// by its user and primary group, and it reads its own as ${identity:…}.
|
||
func JoiningSettingProblem(m Manifest, layers []Layer) error {
|
||
if !Joins(m) {
|
||
return nil
|
||
}
|
||
for _, l := range layers {
|
||
for k := range l.Values {
|
||
if numberWords[k] {
|
||
return fmt.Errorf("%s joins, and the layer %q sets %q: no setting of a module that joins carries a user "+
|
||
"or group number or an owner — it reads its own as ${identity:…} (%s §5)", m.Module, l.From, k, ADR0328)
|
||
}
|
||
}
|
||
}
|
||
return PlacesOwnerProblem(m, layers)
|
||
}
|
||
|
||
// PlacesOwnerProblem refuses an owner a `places` setting names for a module that joins (§4, §5): its directories are
|
||
// owned by its user and primary group.
|
||
func PlacesOwnerProblem(m Manifest, layers []Layer) error {
|
||
if !Joins(m) {
|
||
return nil
|
||
}
|
||
placed, err := Places(m, layers)
|
||
if err != nil {
|
||
return nil // refused by name where it is judged
|
||
}
|
||
var owned []string
|
||
for id, p := range placed {
|
||
if p.Owner != "" {
|
||
owned = append(owned, id)
|
||
}
|
||
}
|
||
if len(owned) == 0 {
|
||
return nil
|
||
}
|
||
sort.Strings(owned)
|
||
return fmt.Errorf("%s places %s with an owner, and it joins: its directories are owned by its user and primary "+
|
||
"group, and no setting of it carries an owner (%s §4, §5)", m.Module, strings.Join(owned, ", "), ADR0328)
|
||
}
|
||
|
||
// ---- composition ------------------------------------------------------------------------------------------------
|
||
|
||
// ModuleIdentity is a joining module's allocated number: its user's and its own group's.
|
||
type ModuleIdentity struct {
|
||
Module string
|
||
UID int
|
||
}
|
||
|
||
// Joined is a joining module's identity on a machine, as its placeholders are filled and its directories owned.
|
||
type Joined struct {
|
||
Module string
|
||
UID int
|
||
// Primary is its user's primary group, by name and number; Added its other groups, by name, in the order used.
|
||
Primary string
|
||
PrimaryGID int
|
||
Added []string
|
||
AddedGIDs []int
|
||
}
|
||
|
||
// librariesOf is the libraries a module uses, by name, in the order its entries first name them.
|
||
func librariesOf(m Manifest) []string {
|
||
var out []string
|
||
for _, u := range m.Libraries {
|
||
if !hasName(out, u.Library) {
|
||
out = append(out, u.Library)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// joinedOf is module m's identity here: its number, and its groups from the libraries it uses. Refused, naming
|
||
// what, where a number is missing or outside the range (§15).
|
||
func joinedOf(m Manifest, with Rendering) (Joined, error) {
|
||
id, has := with.Identities[m.Module]
|
||
if !has {
|
||
return Joined{}, fmt.Errorf("%s joins, and has no number yet: it is allocated at its first send", m.Module)
|
||
}
|
||
if !AllocatableID(id.UID) {
|
||
return Joined{}, fmt.Errorf("%s's number is %d, outside %d–%d: no number the mesh gives is root's or a root "+
|
||
"group's, and none is sent (%s §15)", m.Module, id.UID, IdentityLow, IdentityHigh, ADR0328)
|
||
}
|
||
j := Joined{Module: m.Module, UID: id.UID, Primary: IdentityName(m.Module), PrimaryGID: id.UID}
|
||
for i, lib := range librariesOf(m) {
|
||
gid, has := with.LibraryGroups[lib]
|
||
if !has || !AllocatableID(gid) {
|
||
return Joined{}, fmt.Errorf("%s uses the library %s, whose group has no number the mesh may give (%s §3, "+
|
||
"§15)", m.Module, lib, ADR0328)
|
||
}
|
||
if i == 0 {
|
||
j.Primary, j.PrimaryGID = MeshName(lib), gid
|
||
continue
|
||
}
|
||
j.Added = append(j.Added, MeshName(lib))
|
||
j.AddedGIDs = append(j.AddedGIDs, gid)
|
||
}
|
||
return j, nil
|
||
}
|
||
|
||
// libraryLeftOut is why module m is left out of this machine's declaration for its libraries, or "": a library it
|
||
// uses not defined or not reaching the machine (§2), an engine that cannot be told it joins, or a container held at
|
||
// the user it ran as until every library it uses is taken (§8).
|
||
func libraryLeftOut(m Manifest, node string, with Rendering) string {
|
||
if !Joins(m) {
|
||
return ""
|
||
}
|
||
if !with.KeepsLibraries {
|
||
return fmt.Sprintf("%s joins the mesh's users and libraries (%s), and the node-engine on %s is older than "+
|
||
"them: it is left out until that engine is updated", m.Module, ADR0328, node)
|
||
}
|
||
for _, lib := range librariesOf(m) {
|
||
l, defined := with.Libraries[lib]
|
||
switch {
|
||
case !defined:
|
||
return fmt.Sprintf("%s uses the library %s, which is not defined: the operator defines it (%s §1)",
|
||
m.Module, lib, ADR0328)
|
||
case !l.Reaches(node):
|
||
return fmt.Sprintf("%s uses the library %s, which does not reach %s (%s §2)", m.Module, lib, node, ADR0328)
|
||
}
|
||
}
|
||
if _, has := with.Identities[m.Module]; !has {
|
||
return fmt.Sprintf("%s joins, and its number is allocated at its first send", m.Module)
|
||
}
|
||
if lib := with.Held[m.Module]; lib != "" {
|
||
return fmt.Sprintf("%s keeps the user it ran as until the library %s is taken: the operator is asked once "+
|
||
"(%s §8)", m.Module, lib, ADR0328)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// libraryPaths is where each of module m's library entries is on this machine: its library's root joined with the
|
||
// entry's path, by entry id.
|
||
func libraryPaths(m Manifest, with Rendering) map[string]string {
|
||
out := map[string]string{}
|
||
for _, u := range m.Libraries {
|
||
if l, ok := with.Libraries[u.Library]; ok {
|
||
out[u.ID] = filepath.Join(l.Root, filepath.Clean(u.Path))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// identityFill fills ${identity:…} in one string.
|
||
func identityFill(s string, j Joined) string {
|
||
return identityRef.ReplaceAllStringFunc(s, func(tok string) string {
|
||
switch identityRef.FindStringSubmatch(tok)[1] {
|
||
case "user":
|
||
return IdentityName(j.Module)
|
||
case "uid":
|
||
return fmt.Sprint(j.UID)
|
||
case "group":
|
||
return j.Primary
|
||
default:
|
||
return fmt.Sprint(j.PrimaryGID)
|
||
}
|
||
})
|
||
}
|
||
|
||
// joinedInto composes one resource of a joining module (§4, §5, §10): ${identity:…} filled everywhere, a container's
|
||
// ${library:<id>} volume sources filled with the host path and a read entry mounted read-only, and a directory owned by
|
||
// the module's user and primary group.
|
||
func joinedInto(resource map[string]any, m Manifest, j Joined, paths map[string]string) error {
|
||
for k, v := range resource {
|
||
if k == "id" || k == "type" {
|
||
continue
|
||
}
|
||
filled, err := mapStrings(v, func(s string) (string, error) { return identityFill(s, j), nil })
|
||
if err != nil {
|
||
return err
|
||
}
|
||
resource[k] = filled
|
||
}
|
||
if fmt.Sprint(resource["type"]) == "directory" {
|
||
resource["owner"] = fmt.Sprintf("%d:%d", j.UID, j.PrimaryGID)
|
||
}
|
||
if fmt.Sprint(resource["type"]) != "container" {
|
||
return nil
|
||
}
|
||
volumes, _ := resource["volumes"].([]any)
|
||
out := make([]any, len(volumes))
|
||
for i, v := range volumes {
|
||
out[i] = v
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
continue
|
||
}
|
||
source := volumeSource(s)
|
||
match := libraryRef.FindStringSubmatch(source)
|
||
if match == nil {
|
||
continue
|
||
}
|
||
path, has := paths[match[1]]
|
||
if !has {
|
||
return fmt.Errorf("%s says ${library:%s}, and has no library entry %q", m.Module, match[1], match[1])
|
||
}
|
||
filled := strings.Replace(source, match[0], path, 1) + strings.TrimPrefix(s, source)
|
||
for _, u := range m.Libraries {
|
||
if u.ID == match[1] && u.At() == AccessRead {
|
||
filled = readOnly(filled)
|
||
}
|
||
}
|
||
out[i] = filled
|
||
}
|
||
if len(volumes) > 0 {
|
||
resource["volumes"] = out
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// readOnly is a volume mounted read-only: "src:dst" becomes "src:dst:ro", and options gain ro in place of rw.
|
||
func readOnly(v string) string {
|
||
parts := strings.Split(v, ":")
|
||
if len(parts) < 2 {
|
||
return v
|
||
}
|
||
if len(parts) == 2 {
|
||
return v + ":ro"
|
||
}
|
||
opts := strings.Split(parts[len(parts)-1], ",")
|
||
kept := []string{"ro"}
|
||
for _, o := range opts {
|
||
if o != "ro" && o != "rw" && o != "" {
|
||
kept = append(kept, o)
|
||
}
|
||
}
|
||
parts[len(parts)-1] = strings.Join(kept, ",")
|
||
return strings.Join(parts, ":")
|
||
}
|
||
|
||
// volumeSource is a volume's source as written: up to its first colon outside a ${…} placeholder, so
|
||
// "${dir:config}:/config" is "${dir:config}".
|
||
func volumeSource(v string) string {
|
||
depth := 0
|
||
for i, r := range v {
|
||
switch {
|
||
case r == '{' && i > 0 && v[i-1] == '$':
|
||
depth++
|
||
case r == '}' && depth > 0:
|
||
depth--
|
||
case r == ':' && depth == 0:
|
||
return v[:i]
|
||
}
|
||
}
|
||
return v
|
||
}
|
||
|
||
// bundleLibraryFill fills a joining module's tool words (a bundle's env): ${identity:…}, and ${library:<id>} with the
|
||
// host path, since its tools run on the machine (§5).
|
||
func bundleLibraryFill(value string, m Manifest, with Rendering) (string, error) {
|
||
if !strings.Contains(value, "${identity:") && !strings.Contains(value, "${library:") {
|
||
return value, nil
|
||
}
|
||
j, err := joinedOf(m, with)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
paths := libraryPaths(m, with)
|
||
var missing error
|
||
value = libraryRef.ReplaceAllStringFunc(identityFill(value, j), func(tok string) string {
|
||
id := libraryRef.FindStringSubmatch(tok)[1]
|
||
p, has := paths[id]
|
||
if !has {
|
||
missing = fmt.Errorf("%s's tools say ${library:%s}, and it has no library entry %q", m.Module, id, id)
|
||
return tok
|
||
}
|
||
return p
|
||
})
|
||
return value, missing
|
||
}
|
||
|
||
// toolsAccount is the account module m's tools run as on this machine, or empty where they run nowhere as one: no
|
||
// runtime here, no bundle it loads, or a module left out of an adopted machine's declaration.
|
||
func (r Resolution) toolsAccount(m Manifest, with Rendering) string {
|
||
if !r.runtimeHere() || (with.Adopted && m.Filtering != nil) {
|
||
return ""
|
||
}
|
||
loads := false
|
||
for _, b := range m.Bundles {
|
||
if len(b.Loads) > 0 {
|
||
loads = true
|
||
}
|
||
}
|
||
if !loads {
|
||
return ""
|
||
}
|
||
if m.RunsAs != "" {
|
||
return m.RunsAs
|
||
}
|
||
return r.Account
|
||
}
|
||
|
||
// LibraryEntry is one library as a machine's declaration names it (§3, §6, §7, §11).
|
||
type LibraryEntry struct {
|
||
Library string `json:"library"`
|
||
Root string `json:"root"`
|
||
Group string `json:"group"`
|
||
GID int `json:"gid"`
|
||
// Home says this machine is the library's home, where the engine keeps it; Taken that the operator let the mesh
|
||
// take its existing data there (§7).
|
||
Home bool `json:"home"`
|
||
Taken bool `json:"taken"`
|
||
// Paths are the sub-paths its modules ask for, relative to its root.
|
||
Paths []string `json:"paths"`
|
||
// Runner is the tool runner's account where a module whose tools run there uses the library: a member of its
|
||
// group (§11). Empty otherwise.
|
||
Runner string `json:"runner,omitempty"`
|
||
}
|
||
|
||
// GroupEntry is one group a machine's declaration names: a library's, made on every machine the library reaches (§3).
|
||
type GroupEntry struct {
|
||
Name string `json:"name"`
|
||
GID int `json:"gid"`
|
||
}
|
||
|
||
// IdentityEntry is one joining module's user as a machine's declaration names it (§4; mesh-host internal/declaration
|
||
// Identity): the engine always makes its own group with its uid; GID is its primary group's number — its first
|
||
// library's, or its own — and Groups the numbers of its added groups.
|
||
type IdentityEntry struct {
|
||
Module string `json:"module"`
|
||
Name string `json:"name"`
|
||
UID int `json:"uid"`
|
||
GID int `json:"gid"`
|
||
Groups []int `json:"groups,omitempty"`
|
||
}
|
||
|
||
// libraryEnvelope is what a machine's declaration says of libraries and joining modules: the group of every library
|
||
// that reaches it, each joining module composed here, and each library a module here uses or that has its home here.
|
||
func (r Resolution) libraryEnvelope(with Rendering, joined []Joined) ([]GroupEntry, []IdentityEntry, []LibraryEntry) {
|
||
if !with.KeepsLibraries {
|
||
return nil, nil, nil
|
||
}
|
||
var groups []GroupEntry
|
||
for name, l := range with.Libraries {
|
||
if gid, has := with.LibraryGroups[name]; has && l.Reaches(r.Node) && AllocatableID(gid) {
|
||
groups = append(groups, GroupEntry{Name: l.Group(), GID: gid})
|
||
}
|
||
}
|
||
sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })
|
||
var ids []IdentityEntry
|
||
for _, j := range joined {
|
||
e := IdentityEntry{Module: j.Module, Name: IdentityName(j.Module), UID: j.UID, GID: j.PrimaryGID,
|
||
Groups: append([]int(nil), j.AddedGIDs...)}
|
||
ids = append(ids, e)
|
||
}
|
||
sort.Slice(ids, func(a, b int) bool { return ids[a].Module < ids[b].Module })
|
||
used := map[string]map[string]bool{}
|
||
runner := map[string]string{}
|
||
for _, m := range r.Modules {
|
||
if with.Held[m.Module] == "" && libraryLeftOut(m, r.Node, with) != "" {
|
||
continue
|
||
}
|
||
for _, u := range m.Libraries {
|
||
if used[u.Library] == nil {
|
||
used[u.Library] = map[string]bool{}
|
||
}
|
||
used[u.Library][filepath.Clean(u.Path)] = true
|
||
if a := r.toolsAccount(m, with); a != "" && runner[u.Library] == "" {
|
||
runner[u.Library] = a
|
||
}
|
||
}
|
||
}
|
||
var libs []LibraryEntry
|
||
for name, l := range with.Libraries {
|
||
home := l.Home == r.Node
|
||
if used[name] == nil && !home {
|
||
continue
|
||
}
|
||
gid, has := with.LibraryGroups[name]
|
||
if !has || !l.Reaches(r.Node) {
|
||
continue
|
||
}
|
||
paths := map[string]bool{}
|
||
for p := range used[name] {
|
||
paths[p] = true
|
||
}
|
||
if home {
|
||
// On its home the engine keeps every sub-path any module asks for, wherever it runs (§6).
|
||
for _, p := range with.LibraryPaths[name] {
|
||
paths[filepath.Clean(p)] = true
|
||
}
|
||
}
|
||
e := LibraryEntry{Library: name, Root: l.Root, Group: l.Group(), GID: gid, Home: home,
|
||
Taken: with.TakenLibraries[name], Paths: []string{}, Runner: runner[name]}
|
||
for p := range paths {
|
||
e.Paths = append(e.Paths, p)
|
||
}
|
||
sort.Strings(e.Paths)
|
||
libs = append(libs, e)
|
||
}
|
||
sort.Slice(libs, func(i, j int) bool { return libs[i].Library < libs[j].Library })
|
||
return groups, ids, libs
|
||
}
|
||
|
||
// LibraryUseShape is a module's libraries and whether it joins, in a form that compares: a move that changes either
|
||
// changes who uses its libraries (the gate's reading of an unknown or a held library).
|
||
func LibraryUseShape(m Manifest) string {
|
||
var out []string
|
||
for _, u := range m.Libraries {
|
||
out = append(out, u.ID+"\x00"+u.Library+"\x00"+filepath.Clean(u.Path)+"\x00"+u.At())
|
||
}
|
||
sort.Strings(out)
|
||
return fmt.Sprint(Joins(m), out)
|
||
}
|
||
|
||
// LibraryOfEntry is the library a module's entry id names, or "".
|
||
func LibraryOfEntry(m Manifest, id string) string {
|
||
for _, u := range m.Libraries {
|
||
if u.ID == id {
|
||
return u.Library
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// AccessPathOn is where access id of module m is on a machine with these settings: its placement, else the
|
||
// definition's default; false where neither says, or the module declares no such access.
|
||
func AccessPathOn(m Manifest, layers []Layer, id string) (string, bool) {
|
||
_, byID, err := accessesFor(m, layers)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
path, has := byID[id]
|
||
if !has || path == "" {
|
||
return "", false
|
||
}
|
||
return filepath.Clean(path), true
|
||
}
|