576 lines
22 KiB
Go
576 lines
22 KiB
Go
package catalogue
|
||
|
||
import (
|
||
"fmt"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// Every module is given an identity of its own, and a container that declares `identity` runs as it (novox/hq ADR
|
||
// 0325 §2, §3, §11).
|
||
//
|
||
// **The manifest says how its image is told who to run as, and nothing else.** A container resource may declare
|
||
// `identity`: `{"user": "env:PUID", "group": "env:PGID"}` for an image that reads its user and group from its
|
||
// environment, or `{"user": "run"}` for the container runtime's own user option. It is the image's interface, as its
|
||
// ports are. The mesh fills it in: the controller allocates the module one user and group number, mesh-wide, once,
|
||
// from a range of its own (IdentityLow–IdentityHigh), and the node-engine makes the account `mesh-<module>` on each
|
||
// machine the module is assigned to and tells the image those numbers.
|
||
//
|
||
// **The range is 50000–59999, and a module's user and group numbers are one number.** Chosen above the numbers
|
||
// distributions give people (1000 up) and below the 60000s Debian keeps for its own daemons, and far from root's 0
|
||
// and every root group a distribution ships (0, adm's 4, wheel's 10): no number in it is 0 or a root group's, so §11
|
||
// holds by the range alone, and the store refuses a row outside it. Both numbers come from one sequence, so a
|
||
// module's uid and gid are equal; a number once given is never given again, even after the module is forgotten
|
||
// (the store keeps the row, inventory.AllocateIdentity).
|
||
//
|
||
// **What the controller sends, and only to an engine that reads it** (Rendering.KeepsFolders: its statement's
|
||
// contract is link.IdentityContract or later):
|
||
//
|
||
// - the declaration's `identities`: [{module, name, uid, gid}] for each module of the machine that declares
|
||
// `identity` on a container;
|
||
// - a container's `identity` resolved: {"mode": "env", "user_env", "group_env", "uid", "gid"} or {"mode": "run",
|
||
// "uid", "gid"};
|
||
// - the owner of each of the module's directories a container declaring `identity` mounts: the identity's
|
||
// uid:gid (§3). A directory only a process or the tools write keeps the owner it had: what writes it is not the
|
||
// identity, and owning it by the identity would lock its writer out;
|
||
// - an access's `users`: the module's identity where a container of it that declares `identity` mounts the
|
||
// access, and the tool runner's account where the module's tools run there as named, its number the engine reads by name,
|
||
// each at the access's use — the node-engine keeps one entry per user on a folder at the widest use any access
|
||
// to it gives (§4, §5), so the runner's entry is the widest of the modules whose tools use it; and `taken`,
|
||
// whether the operator let the mesh keep that folder on that machine (§8).
|
||
//
|
||
// To an older engine none of it is sent, and a container's `identity` is taken out: it runs as its image says, as
|
||
// before. A module that declares no `identity` is composed exactly as before, on any engine.
|
||
|
||
// The range module identities are allocated from (novox/hq ADR 0325 §3, §11). The store holds the same bounds.
|
||
const (
|
||
IdentityLow = 50000
|
||
IdentityHigh = 59999
|
||
)
|
||
|
||
// AllocatableID says whether n is a number the mesh may give a module's identity: inside the range, so never 0
|
||
// nor a root group's.
|
||
func AllocatableID(n int) bool { return n >= IdentityLow && n <= IdentityHigh }
|
||
|
||
// IdentityField is a container resource's field saying how its image is told who to run as.
|
||
const IdentityField = "identity"
|
||
|
||
// The two ways an image is told who to run as.
|
||
const (
|
||
IdentityEnv = "env"
|
||
IdentityRun = "run"
|
||
)
|
||
|
||
// IdentitySpec is a container's `identity` as its manifest declares it.
|
||
type IdentitySpec struct {
|
||
// Mode is IdentityEnv or IdentityRun.
|
||
Mode string
|
||
// UserEnv and GroupEnv are the environment variables an env image reads, such as PUID and PGID.
|
||
UserEnv, GroupEnv string
|
||
}
|
||
|
||
// ModuleIdentity is one module's allocated identity: its numbers, the same on every machine.
|
||
type ModuleIdentity struct {
|
||
Module string
|
||
UID int
|
||
GID int
|
||
}
|
||
|
||
// IdentityName is the account a module's identity is on every machine it is assigned to.
|
||
func IdentityName(module string) string { return "mesh-" + module }
|
||
|
||
// Where an access's user comes from (novox/hq ADR 0325 §4, §5).
|
||
const (
|
||
FromContainer = "container"
|
||
FromToolRunner = "tool-runner"
|
||
)
|
||
|
||
// AccessUser is one identity that uses a shared folder, as an access's `users` names it (mesh-host internal/declaration
|
||
// AccessUser): the account, its user number, the use, and where it runs. The tool runner's account is named with no
|
||
// number: it is the machine's, and the node-engine resolves it by name, refusing one missing or root's.
|
||
type AccessUser struct {
|
||
Identity string `json:"identity"`
|
||
UID int `json:"uid,omitempty"`
|
||
Use string `json:"use"`
|
||
From string `json:"from"`
|
||
}
|
||
|
||
var envName = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`)
|
||
|
||
// ParseIdentity reads a container's `identity`, or refuses it in words.
|
||
func ParseIdentity(v any) (IdentitySpec, error) {
|
||
fields, ok := v.(map[string]any)
|
||
if !ok {
|
||
return IdentitySpec{}, fmt.Errorf("identity is {\"user\": \"env:<NAME>\", \"group\": \"env:<NAME>\"} or " +
|
||
"{\"user\": \"run\"}")
|
||
}
|
||
for k := range fields {
|
||
if k != "user" && k != "group" {
|
||
return IdentitySpec{}, fmt.Errorf("identity says %q, and it says only how the image is told its user "+
|
||
"and group: \"user\" and \"group\", never an owner or a number (%s §1, §2)", k, ADR0325)
|
||
}
|
||
}
|
||
user, _ := fields["user"].(string)
|
||
group, hasGroup := fields["group"]
|
||
switch {
|
||
case user == IdentityRun:
|
||
if hasGroup {
|
||
return IdentitySpec{}, fmt.Errorf("identity {\"user\": \"run\"} takes no group: the runtime's user " +
|
||
"option carries both numbers")
|
||
}
|
||
return IdentitySpec{Mode: IdentityRun}, nil
|
||
case strings.HasPrefix(user, "env:"):
|
||
g, _ := group.(string)
|
||
if !strings.HasPrefix(g, "env:") {
|
||
return IdentitySpec{}, fmt.Errorf("identity names its user as %q and its group as %v; an image told "+
|
||
"its user by its environment is told its group the same way, \"env:<NAME>\"", user, group)
|
||
}
|
||
u, gr := strings.TrimPrefix(user, "env:"), strings.TrimPrefix(g, "env:")
|
||
if !envName.MatchString(u) || !envName.MatchString(gr) || u == gr {
|
||
return IdentitySpec{}, fmt.Errorf("identity names the variables %q and %q; each is an environment "+
|
||
"variable's name, upper-case letters, digits and underscores, and the two differ", u, gr)
|
||
}
|
||
return IdentitySpec{Mode: IdentityEnv, UserEnv: u, GroupEnv: gr}, nil
|
||
}
|
||
return IdentitySpec{}, fmt.Errorf("identity says its user is %q; it is \"env:<NAME>\" or \"run\"", user)
|
||
}
|
||
|
||
// DeclaresIdentity says a module has a container that declares `identity`: it is given one (§3).
|
||
func DeclaresIdentity(m Manifest) bool {
|
||
for _, r := range m.Resources {
|
||
if fmt.Sprint(r["type"]) == "container" {
|
||
if _, has := r[IdentityField]; has {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// containerMounts is what a container of the manifest mounts, read from its volumes as written: the module's own
|
||
// directories by id, and its accesses by id — or, for a literal source, as "path:<path>".
|
||
func containerMounts(r map[string]any) (dirs, accesses []string) {
|
||
volumes, _ := r["volumes"].([]any)
|
||
for _, v := range volumes {
|
||
s, _ := v.(string)
|
||
source := volumeSource(s)
|
||
if strings.HasPrefix(source, "/") {
|
||
accesses = append(accesses, "path:"+filepath.Clean(source))
|
||
continue
|
||
}
|
||
for _, m := range dirRef.FindAllStringSubmatch(source, -1) {
|
||
dirs = append(dirs, m[1])
|
||
}
|
||
for _, m := range accessRef.FindAllStringSubmatch(source, -1) {
|
||
accesses = append(accesses, m[1])
|
||
}
|
||
}
|
||
return dirs, accesses
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// AccessUsersShape is who among a module's containers uses which access as the module's identity, in a form that
|
||
// compares: each container declaring identity with how it is told, and the accesses it mounts, sorted. Two manifests
|
||
// with the same shape give their accesses the same container users.
|
||
func AccessUsersShape(m Manifest) string {
|
||
var out []string
|
||
for _, r := range m.Resources {
|
||
v, declares := r[IdentityField]
|
||
if fmt.Sprint(r["type"]) != "container" || !declares {
|
||
continue
|
||
}
|
||
_, accesses := containerMounts(r)
|
||
sort.Strings(accesses)
|
||
out = append(out, fmt.Sprint(r["id"], v, accesses))
|
||
}
|
||
sort.Strings(out)
|
||
return strings.Join(out, "\x01")
|
||
}
|
||
|
||
// accessMode is the mode an access a container mounts was declared at, by id or by "path:<path>"; "" when the
|
||
// manifest declares no such access.
|
||
func accessMode(m Manifest, ref string) string {
|
||
for _, a := range m.Accesses {
|
||
if (a.ID != "" && a.ID == ref) || (a.Path != "" && ref == "path:"+filepath.Clean(a.Path)) {
|
||
return a.At()
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// ADR0325 is how the module check names the decision its identity rules come from.
|
||
const ADR0325 = "novox/hq ADR 0325"
|
||
|
||
// identityProblems is what the module check refuses of a manifest's identities (novox/hq ADR 0325 §1, §2, §3):
|
||
//
|
||
// - an `identity` on anything but a container, or one that is neither env:<NAME> for both nor run;
|
||
// - **in a module that declares `identity` on a container** — it says it runs as its own identity, so it is held
|
||
// to the decision whole: a directory of it naming an owner (§1, §3), and a container of it that declares none
|
||
// and mounts one of the module's directories or a read-write access (§2).
|
||
//
|
||
// A module that declares no `identity` is warned of the last two (IdentityWarnings), never refused for them yet:
|
||
// every registered container module mounts its own directories without one, and refusing them would stop the
|
||
// catalogues composing before they can migrate. **This is the narrowest rule that breaks nothing registered**: no
|
||
// registered manifest declares `identity`, so no registered module is refused anything new.
|
||
func identityProblems(m Manifest) []string {
|
||
var problems []string
|
||
for _, r := range m.Resources {
|
||
v, has := r[IdentityField]
|
||
if !has {
|
||
continue
|
||
}
|
||
if fmt.Sprint(r["type"]) != "container" {
|
||
problems = append(problems, fmt.Sprintf("%s: %v is a %v and says identity; only a container says how its "+
|
||
"image is told who to run as (%s §2)", m.Module, r["id"], r["type"], ADR0325))
|
||
continue
|
||
}
|
||
if _, err := ParseIdentity(v); err != nil {
|
||
problems = append(problems, fmt.Sprintf("%s: the container %v: %v (%s §2)", m.Module, r["id"], err, ADR0325))
|
||
}
|
||
}
|
||
if !DeclaresIdentity(m) {
|
||
return problems
|
||
}
|
||
return append(problems, identityRules(m)...)
|
||
}
|
||
|
||
// IdentityWarnings is what the module check warns of a module that declares no `identity` (novox/hq ADR 0325): each
|
||
// thing the decision refuses once the module migrates, said now so the catalogues can. Refusing comes when no
|
||
// registered module still does any of it. Empty for a module that declares `identity`: it is refused these instead.
|
||
func IdentityWarnings(m Manifest) []string {
|
||
if DeclaresIdentity(m) {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for _, p := range identityRules(m) {
|
||
out = append(out, p+"; accepted while registered modules still do it, refused once they have moved")
|
||
}
|
||
return out
|
||
}
|
||
|
||
// identityRules is every place a manifest breaks what ADR 0325 §1–§3 refuse: a directory naming an owner, and a
|
||
// container declaring no identity that mounts the module's own directory or a read-write access.
|
||
func identityRules(m Manifest) []string {
|
||
var out []string
|
||
for _, r := range m.Resources {
|
||
switch fmt.Sprint(r["type"]) {
|
||
case "directory":
|
||
if owner, said := r["owner"]; said {
|
||
out = append(out, fmt.Sprintf("%s: the directory %v names its owner %v; a directory a module owns is "+
|
||
"owned by the module's identity, and no manifest names an owner (%s §1, §3)", m.Module, r["id"],
|
||
owner, ADR0325))
|
||
}
|
||
case "container":
|
||
if _, has := r[IdentityField]; has {
|
||
continue
|
||
}
|
||
dirs, accesses := containerMounts(r)
|
||
seen := map[string]bool{}
|
||
for _, d := range dirs {
|
||
if !seen[d] {
|
||
seen[d] = true
|
||
out = append(out, fmt.Sprintf("%s: the container %v declares no identity and mounts the module's "+
|
||
"own directory %s; only a container that declares identity mounts its module's directories, "+
|
||
"so what writes them is the module's identity (%s §2)", m.Module, r["id"], d, ADR0325))
|
||
}
|
||
}
|
||
for _, a := range accesses {
|
||
if accessMode(m, a) == AccessReadWrite && !seen["access:"+a] {
|
||
seen["access:"+a] = true
|
||
out = append(out, fmt.Sprintf("%s: the container %v declares no identity and mounts the read-write "+
|
||
"access %s; a container that runs as its image says is given no entry on a shared folder, so "+
|
||
"it is refused write (%s §2)", m.Module, r["id"], strings.TrimPrefix(a, "path:"), ADR0325))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// PlacesOwnerProblem refuses an owner a `places` setting names for a module that declares `identity` (novox/hq ADR
|
||
// 0325 §1, §3): its directories are owned by its identity. Nil for a module that declares none, whose placed owner
|
||
// is still applied, and warned of by PlacesOwnerWarnings.
|
||
func PlacesOwnerProblem(m Manifest, layers []Layer) error {
|
||
if !DeclaresIdentity(m) {
|
||
return nil
|
||
}
|
||
if owned := placedOwners(m, layers); len(owned) > 0 {
|
||
return fmt.Errorf("%s places %s with an owner, and declares identity: its directories are owned by its "+
|
||
"identity, and no setting names an owner (%s §1, §3)", m.Module, strings.Join(owned, ", "), ADR0325)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// PlacesOwnerWarnings is a warning for each directory a `places` setting gives an owner, for a module that declares
|
||
// no `identity` (novox/hq ADR 0325 §1): accepted while registered modules still do it, refused once they have moved.
|
||
func PlacesOwnerWarnings(m Manifest, layers []Layer) []string {
|
||
if DeclaresIdentity(m) {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for _, id := range placedOwners(m, layers) {
|
||
out = append(out, fmt.Sprintf("warning: %s's setting places.%s.owner names an owner; a directory a module owns "+
|
||
"is owned by the module's identity, and no setting names an owner (%s §1, §3). Accepted while "+
|
||
"registered modules still do it, refused once they have moved", m.Module, id, ADR0325))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func placedOwners(m Manifest, layers []Layer) []string {
|
||
placed, err := Places(m, layers)
|
||
if err != nil {
|
||
// empty-on-error: a placement Places refuses is refused by name where it is judged and composed; it names no owner here
|
||
return nil
|
||
}
|
||
var out []string
|
||
for id, p := range placed {
|
||
if p.Owner != "" {
|
||
out = append(out, id)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// resolvedIdentity is a container's `identity` as the node-engine is told it: the mode, the variables an env image
|
||
// reads, and the module's numbers.
|
||
func resolvedIdentity(spec IdentitySpec, id ModuleIdentity) map[string]any {
|
||
out := map[string]any{"mode": spec.Mode, "uid": id.UID, "gid": id.GID}
|
||
if spec.Mode == IdentityEnv {
|
||
out["user_env"], out["group_env"] = spec.UserEnv, spec.GroupEnv
|
||
}
|
||
return out
|
||
}
|
||
|
||
// identityFor is the identity module m runs as here, and whether one is composed: a module that declares `identity`,
|
||
// to an engine that keeps folders, with an allocation the composition was given. A number outside the range is
|
||
// refused (§11), never sent.
|
||
func identityFor(m Manifest, with Rendering) (ModuleIdentity, bool, error) {
|
||
if !with.KeepsFolders || !DeclaresIdentity(m) {
|
||
return ModuleIdentity{}, false, nil
|
||
}
|
||
id, allocated := with.Identities[m.Module]
|
||
if !allocated {
|
||
return ModuleIdentity{}, false, nil
|
||
}
|
||
if !AllocatableID(id.UID) || !AllocatableID(id.GID) {
|
||
return ModuleIdentity{}, false, fmt.Errorf("%s's identity is %d:%d, outside %d–%d: no module identity is root "+
|
||
"or a root group's, and none is sent (%s §11)", m.Module, id.UID, id.GID, IdentityLow, IdentityHigh, ADR0325)
|
||
}
|
||
id.Module = m.Module
|
||
return id, true, nil
|
||
}
|
||
|
||
// identityInto composes one resource of module m for its identity: a container's `identity` resolved, or taken out
|
||
// where none is composed; and the owner of a directory one of its identity's containers mounts.
|
||
func identityInto(resource map[string]any, m Manifest, id ModuleIdentity, has bool, mountedByIdentity map[string]bool) error {
|
||
switch fmt.Sprint(resource["type"]) {
|
||
case "container":
|
||
v, declared := resource[IdentityField]
|
||
if !declared {
|
||
return nil
|
||
}
|
||
if !has {
|
||
delete(resource, IdentityField)
|
||
return nil
|
||
}
|
||
spec, err := ParseIdentity(v)
|
||
if err != nil {
|
||
return fmt.Errorf("%s: the container %v: %w", m.Module, resource["id"], err)
|
||
}
|
||
resource[IdentityField] = resolvedIdentity(spec, id)
|
||
case "directory":
|
||
if has && mountedByIdentity[fmt.Sprint(resource["id"])] {
|
||
resource["owner"] = fmt.Sprintf("%d:%d", id.UID, id.GID)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// identityHeld says module m's containers are not switched to its identity yet (the operator's question on ADR 0325):
|
||
// a container declaring `identity` mounts a read-write access whose folder is neither taken (KeptFolders) nor kept by
|
||
// the engine (ReadyFolders). Switched before the take adds the identity's entry, the app would run as the new uid and
|
||
// lose write on the operator's existing files; held, it runs as it did (its environment's PUID and PGID), the take's
|
||
// users already name the identity, and the switch comes in the apply after every such folder is taken.
|
||
func identityHeld(m Manifest, with Rendering) bool {
|
||
_, byID, err := accessesFor(m, with.Settings[m.Module])
|
||
if err != nil {
|
||
return true // refused where the module itself is composed; never switched on a guess
|
||
}
|
||
for _, r := range m.Resources {
|
||
if _, declares := r[IdentityField]; !declares || fmt.Sprint(r["type"]) != "container" {
|
||
continue
|
||
}
|
||
_, refs := containerMounts(r)
|
||
for _, ref := range refs {
|
||
if accessMode(m, ref) != AccessReadWrite {
|
||
continue
|
||
}
|
||
path := byID[ref]
|
||
if strings.HasPrefix(ref, "path:") {
|
||
path = strings.TrimPrefix(ref, "path:")
|
||
}
|
||
path = filepath.Clean(path)
|
||
if path != "." && !with.KeptFolders[path] && !with.ReadyFolders[path] {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// identityContainers is the composed ids of module m's containers that declare `identity`, whether or not it is sent.
|
||
func identityContainers(m Manifest) map[string]bool {
|
||
out := map[string]bool{}
|
||
for _, r := range m.Resources {
|
||
if _, declares := r[IdentityField]; declares && fmt.Sprint(r["type"]) == "container" {
|
||
out[m.Module+"."+fmt.Sprint(r["id"])] = true
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// dirsMountedByIdentity is the ids of module m's directories one of its containers declaring `identity` mounts.
|
||
func dirsMountedByIdentity(m Manifest) map[string]bool {
|
||
out := map[string]bool{}
|
||
for _, r := range m.Resources {
|
||
if fmt.Sprint(r["type"]) != "container" {
|
||
continue
|
||
}
|
||
if _, has := r[IdentityField]; !has {
|
||
continue
|
||
}
|
||
dirs, _ := containerMounts(r)
|
||
for _, d := range dirs {
|
||
out[d] = true
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// identitiesOf is the declaration's `identities`, by module.
|
||
func identitiesOf(ids []ModuleIdentity) []map[string]any {
|
||
sort.Slice(ids, func(i, j int) bool { return ids[i].Module < ids[j].Module })
|
||
out := make([]map[string]any, 0, len(ids))
|
||
for _, id := range ids {
|
||
out = append(out, map[string]any{"module": id.Module, "name": IdentityName(id.Module), "uid": id.UID,
|
||
"gid": id.GID})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// usersInto writes `users` and `taken` on every access of module m among the resources composed so far: its identity
|
||
// where a container of it that declares `identity` mounts the access's path, and the tool runner's account where the
|
||
// module's tools run as it, each at the access's use (§4: the engine keeps the widest).
|
||
func usersInto(out []map[string]any, owner map[string]string, m Manifest, id ModuleIdentity, has bool, account string,
|
||
with Rendering) {
|
||
// The containers that declare identity, whether or not it is sent yet: the take adds the entries of the identity
|
||
// to come, so they exist before the switch.
|
||
declaring := identityContainers(m)
|
||
var containers []map[string]any
|
||
for _, res := range out {
|
||
if res["type"] == "container" && owner[fmt.Sprint(res["id"])] == m.Module && declaring[fmt.Sprint(res["id"])] && has {
|
||
containers = append(containers, res)
|
||
}
|
||
}
|
||
for _, res := range out {
|
||
if res["type"] != "access" || owner[fmt.Sprint(res["id"])] != m.Module {
|
||
continue
|
||
}
|
||
path, _ := res["path"].(string)
|
||
mode, _ := res["mode"].(string)
|
||
if mode == "" {
|
||
mode = AccessRead
|
||
}
|
||
users := []AccessUser{}
|
||
for _, c := range containers {
|
||
if mounts(c, path) {
|
||
users = append(users, AccessUser{Identity: IdentityName(m.Module), UID: id.UID, Use: mode, From: FromContainer})
|
||
break // one identity per module, whichever of its containers mounts it
|
||
}
|
||
}
|
||
if account != "" {
|
||
users = append(users, AccessUser{Identity: account, Use: mode, From: FromToolRunner})
|
||
}
|
||
if len(users) > 0 {
|
||
res["users"] = users
|
||
}
|
||
res["taken"] = with.KeptFolders[filepath.Clean(path)]
|
||
}
|
||
}
|
||
|
||
// mounts says whether a composed container mounts path, or something below it, from the machine. A path that merely
|
||
// begins with the same letters — /storage/media2 beside /storage/media — is another path.
|
||
func mounts(container map[string]any, path string) bool {
|
||
volumes, _ := container["volumes"].([]any)
|
||
path = filepath.Clean(path)
|
||
for _, v := range volumes {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
continue
|
||
}
|
||
source, _, _ := strings.Cut(s, ":")
|
||
if !strings.HasPrefix(source, "/") {
|
||
continue // a named volume: the runtime's, not the operator's path
|
||
}
|
||
source = filepath.Clean(source)
|
||
if source == path || strings.HasPrefix(source, path+"/") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 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
|
||
}
|