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-` and `mesh-` 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:}` 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-. 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-. func (l Library) Group() string { return MeshName(l.Name) } var libraryName = regexp.MustCompile(`^[a-z][a-z0-9-]*$`) // ParseLibraries reads the `libraries` setting: {: {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 { : {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:}. 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:}` naming none; // - `${library:}` 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 } problems = append(problems, toolsDirectoryProblems(m)...) 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 } // toolsDirectoryProblems refuses a joining module whose tools name one of its directories whose mode closes it to its // group (novox/hq ADR 0334 §3): the tool runner reads it through the module's own group, so it needs group read and // enter. func toolsDirectoryProblems(m Manifest) []string { modes := map[string]string{} for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "directory" { mode, _ := r["mode"].(string) modes[fmt.Sprint(r["id"])] = mode } } var words []string for _, b := range m.Bundles { for _, v := range b.Env { words = append(words, v) } } if m.Build != nil { for _, a := range m.Build.Artifacts { for _, v := range a.Env { words = append(words, v) } } } seen := map[string]bool{} var problems []string for _, w := range words { for _, match := range dirRef.FindAllStringSubmatch(w, -1) { id := match[1] mode, declared := modes[id] if !declared || seen[id] { continue } seen[id] = true var n uint64 if _, err := fmt.Sscanf(mode, "%o", &n); err != nil || mode == "" { continue // its mode is judged where the directory is } if n&0o050 != 0o050 { problems = append(problems, fmt.Sprintf("%s's tools name its directory %s, whose mode %s closes it to its "+ "own group: the tool runner reads it through that group, so it needs group read and enter (0750; "+ "the mesh adds setgid, 2750) (novox/hq ADR 0334 §3)", m.Module, id, mode)) } } } sort.Strings(problems) 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), or an engine that cannot be told it joins. A module held at the // user it ran as until its libraries are taken (§8) is not left out: it is composed at the build it ran before it // joined (cmd/mesh-controller holdJoining). 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) } 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:} 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, setgid bool) 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 } // Its own directories belong to its user and its own group, with setgid (ADR 0334 §1, v4): the library's group is // its processes' primary, so what they make in a library keeps that group — and what they make in their own // directories keeps mesh- by the setgid bit. The bit only to an engine that takes a mode with it // (Rendering.RunnerJoinsOwnGroups, link.OwnGroupContract): the one pinned refuses a mode not starting with 0. if fmt.Sprint(resource["type"]) == "directory" { resource["owner"] = fmt.Sprintf("%d:%d", j.UID, j.UID) if mode, ok := resource["mode"].(string); ok && setgid { var n uint64 if _, err := fmt.Sscanf(mode, "%o", &n); err == nil && mode != "" { resource["mode"] = fmt.Sprintf("%04o", n|0o2000) } } } 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:} 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"` // Runner is the tool runner's account, put in this module's own group, where its tools run there (ADR 0334 §2). // Only to an engine that reads it (Rendering.RunnerJoinsOwnGroups). Runner string `json:"runner,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...)} if with.RunnerJoinsOwnGroups { for _, m := range r.Modules { if m.Module == j.Module { e.Runner = r.toolsAccount(m, with) } } } 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 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 }