Judge a definition's resolved paths where the definition is judged, by the node-engine's own rules, so a refusal never freezes a machine (review of #228, issue 496)
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
package apply
|
||||
|
||||
// Where the node-engine places nothing and mounts nothing, whoever asks (novox/hq issue 339).
|
||||
//
|
||||
// A directory names its path, and the controller resolves part of that path from what was set for the
|
||||
// module: its `places` setting moves a directory anywhere, with an owner it names, and its `accesses` setting
|
||||
// says which of the machine's paths are mounted into its container. The engine runs as root, so a path it
|
||||
// accepts blindly is a path anyone who could change those settings hands to any account: a directory at /etc
|
||||
// owned by a caller's account gives it /etc, and an access at / mounts the machine's root into a container.
|
||||
// The controller refuses both where a setting is made; the engine refuses them again where it applies,
|
||||
// because a guard in one place is a guard one change away from gone. Whatever the declaration says:
|
||||
//
|
||||
// 1. **No directory, access or mount source is one of the machine's own roots, or holds one**: /, /etc,
|
||||
// /usr, /var, /var/lib, /home, /run and the rest of protectedRoots. Modules place directories BELOW /etc
|
||||
// or /var/lib, never the root itself; owning one is owning everything in it.
|
||||
// 2. **Nothing is placed in /proc, /sys, /dev, /boot, /root, /var/spool, /opt or the container runtimes' data
|
||||
// (/var/lib/docker, /var/lib/containers, /var/lib/containerd, and where the runtimes' configuration moves
|
||||
// them: runtimeDataRoots), nor in the node-engine's
|
||||
// own trees** (its state, its identity, its installed builds) but by its own module; nothing is mounted from
|
||||
// those but /proc, /sys and /dev. A mount of a kernel file, a device or the clock is the plumbing
|
||||
// systemPath names.
|
||||
// 2a. **An account's .ssh is never an access or a mount**, and is a directory only below its account's home,
|
||||
// as that account's (rule 4).
|
||||
// 3. **A directory below /etc, /usr or /run is root's.** The machine's configuration and programs are read
|
||||
// as root's word; a directory there owned by another account is that account writing root's word. An
|
||||
// access is never there at all: the operator's data is not the machine's configuration.
|
||||
// 4. **Below a person's or an agent's home, a directory is that account's.** Root's or another account's
|
||||
// directory there is one the account does not control in a tree whose every parent it does. A home
|
||||
// itself may be a module's directory (a backup repository kept as an account's home is one), and as
|
||||
// every directory the mesh did not make, it is used as found: never chowned or chmodded (applyDirectory).
|
||||
// 5. **A mount source that is a refused directory or a refused access is refused with it**: the container
|
||||
// would otherwise bind the very path the engine would not place, and the runtime creates a missing one
|
||||
// as root.
|
||||
//
|
||||
// Each is a failed resource with its reason in the node's report; nothing is touched. Paths are judged as
|
||||
// declared and again with every link in them resolved, so a link at /srv/x pointing at /etc places nothing.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// protectedRoots are paths no directory, access or mount source may be, nor hold.
|
||||
var protectedRoots = []string{"/", "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib32", "/lib64",
|
||||
"/media", "/mnt", "/opt", "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", "/usr/bin",
|
||||
"/usr/lib", "/usr/lib64", "/usr/local", "/usr/local/bin", "/usr/local/lib", "/usr/local/sbin", "/usr/sbin",
|
||||
"/usr/share", "/var", "/var/cache", "/var/lib", "/var/lib/mesh", "/var/log", "/var/run", "/var/tmp",
|
||||
"/var/spool"}
|
||||
|
||||
// forbiddenBelow are trees nothing is placed in or mounted from: the kernel's, the boot loader's and root's
|
||||
// home. engineTrees are the node-engine's own, which only its own module places in.
|
||||
var (
|
||||
forbiddenBelow = []string{"/proc", "/sys", "/dev", "/boot", "/root", "/var/spool", "/opt", "/var/lib/docker",
|
||||
"/var/lib/containers", "/var/lib/containerd"}
|
||||
// forbiddenBelowMount are the trees no container mounts from: a mount of the kernel's files and devices is
|
||||
// the plumbing a container may need (systemPath); root's home and the boot loader's are no plumbing.
|
||||
forbiddenBelowMount = []string{"/boot", "/root", "/var/spool", "/opt", "/var/lib/docker", "/var/lib/containers",
|
||||
"/var/lib/containerd"}
|
||||
engineTrees = []string{"/var/lib/mesh-host", "/usr/lib/nox-mesh-host"}
|
||||
// rootsOnly are trees a directory below is root's, and an access is never in.
|
||||
rootsOnly = []string{"/etc", "/usr", "/run", "/var/run"}
|
||||
)
|
||||
|
||||
// runtimeFiles are where the container runtimes say where they keep their data: dockerd's daemon.json, its
|
||||
// unit and the unit's drop-ins (an ExecStart with --data-root, or the older -g/--graph, continued over lines,
|
||||
// quoted, through Environment= or EnvironmentFile=, or a --config-file naming another daemon.json), and podman's
|
||||
// storage.conf (graphroot, a basic or a literal string). The running runtimes are asked first. containerd keeps its own under /var/lib/containerd, which forbiddenBelow names; a
|
||||
// containerd configured elsewhere, and podman's rootless stores under each account's home, are not read: the
|
||||
// first is no runtime this mesh runs, and the second is below a home, which rule 4 already keeps for its
|
||||
// account. A variable so a test names its own.
|
||||
type runtimeFiles struct {
|
||||
daemonJSON string
|
||||
units []string
|
||||
dropInDirs []string
|
||||
storageConf string
|
||||
}
|
||||
|
||||
var runtimeConfigs = runtimeFiles{
|
||||
daemonJSON: "/etc/docker/daemon.json",
|
||||
units: []string{"/etc/systemd/system/docker.service", "/usr/lib/systemd/system/docker.service", "/lib/systemd/system/docker.service"},
|
||||
dropInDirs: []string{"/etc/systemd/system/docker.service.d", "/run/systemd/system/docker.service.d", "/usr/lib/systemd/system/docker.service.d"},
|
||||
storageConf: "/etc/containers/storage.conf",
|
||||
}
|
||||
|
||||
var (
|
||||
dataRootFlag = regexp.MustCompile(`(?:--data-root|--graph|-g)(?:=|\s+)(\S+)`)
|
||||
configFlag = regexp.MustCompile(`--config-file(?:=|\s+)(\S+)`)
|
||||
graphRoot = regexp.MustCompile(`(?m)^\s*graphroot\s*=\s*(?:"([^"]+)"|'([^']+)')`)
|
||||
envVar = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)`)
|
||||
)
|
||||
|
||||
// runtimeRoots is where the container runtimes keep their data, as the last apply read it (readRuntimeRoots);
|
||||
// nil until an apply has read it, when the guard reads the files itself.
|
||||
var (
|
||||
runtimeRootsMu sync.Mutex
|
||||
runtimeRoots []string
|
||||
)
|
||||
|
||||
// AskRuntimes is how the engine asks the running container runtimes where they keep their data: set by the engine
|
||||
// to run the commands, nil in a test, which then reads the files alone. Its own, never the apply's runner, whose
|
||||
// commands a test reads back as what the apply did.
|
||||
var AskRuntimes Runner
|
||||
|
||||
// refreshRuntimeRoots reads where the runtimes keep their data once, at the start of an apply.
|
||||
func refreshRuntimeRoots(ctx context.Context) {
|
||||
roots := readRuntimeRoots(ctx, AskRuntimes)
|
||||
runtimeRootsMu.Lock()
|
||||
runtimeRoots = roots
|
||||
runtimeRootsMu.Unlock()
|
||||
}
|
||||
|
||||
// runtimeDataRoots is every place the container runtimes keep their data, beyond the default trees forbiddenBelow
|
||||
// names: every container's filesystem is there.
|
||||
func runtimeDataRoots() []string {
|
||||
runtimeRootsMu.Lock()
|
||||
roots := runtimeRoots
|
||||
runtimeRootsMu.Unlock()
|
||||
if roots != nil {
|
||||
return roots
|
||||
}
|
||||
return readRuntimeRoots(context.Background(), nil)
|
||||
}
|
||||
|
||||
// readRuntimeRoots asks the running runtimes where they keep their data, when run is given (`docker info`,
|
||||
// `podman info`), and reads their configuration besides: an answer from a runtime that is running is what it
|
||||
// really does, and the files say what it will do when it starts again. Both count. A runtime that is not running
|
||||
// or not installed answers nothing, which is no error.
|
||||
func readRuntimeRoots(ctx context.Context, run Runner) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
add := func(p string) {
|
||||
p = strings.Trim(strings.TrimSpace(p), `"'`)
|
||||
if !filepath.IsAbs(p) {
|
||||
return
|
||||
}
|
||||
p = filepath.Clean(p)
|
||||
if !seen[p] {
|
||||
seen[p] = true
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
if run != nil {
|
||||
// Each runtime has its own ten seconds: one that hangs costs the other nothing.
|
||||
for _, q := range [][]string{{"docker", "info", "--format", "{{.DockerRootDir}}"},
|
||||
{"podman", "info", "--format", "{{.Store.GraphRoot}}"}} {
|
||||
ask, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
if root, err := run(ask, q[0], q[1:]...); err == nil {
|
||||
add(root)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
daemonJSON := func(path string) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var c struct {
|
||||
DataRoot string `json:"data-root"`
|
||||
Graph string `json:"graph"`
|
||||
}
|
||||
if json.Unmarshal(raw, &c) == nil {
|
||||
add(c.DataRoot)
|
||||
add(c.Graph)
|
||||
}
|
||||
}
|
||||
daemonJSON(runtimeConfigs.daemonJSON)
|
||||
units := append([]string(nil), runtimeConfigs.units...)
|
||||
for _, dir := range runtimeConfigs.dropInDirs {
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, "*.conf"))
|
||||
units = append(units, matches...)
|
||||
}
|
||||
// The unit and its drop-ins are one unit to systemd: a variable set in one is seen by an ExecStart in another.
|
||||
env := map[string]string{}
|
||||
var execs []string
|
||||
for _, u := range units {
|
||||
raw, err := os.ReadFile(u)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
e, x := unitLines(string(raw))
|
||||
for k, v := range e {
|
||||
env[k] = v
|
||||
}
|
||||
execs = append(execs, x...)
|
||||
}
|
||||
for _, line := range execs {
|
||||
line = envVar.ReplaceAllStringFunc(line, func(ref string) string {
|
||||
m := envVar.FindStringSubmatch(ref)
|
||||
if v, ok := env[m[1]+m[2]]; ok {
|
||||
return v
|
||||
}
|
||||
return ref
|
||||
})
|
||||
for _, m := range dataRootFlag.FindAllStringSubmatch(line, -1) {
|
||||
add(m[1])
|
||||
}
|
||||
for _, m := range configFlag.FindAllStringSubmatch(line, -1) {
|
||||
daemonJSON(strings.Trim(m[1], `"'`))
|
||||
}
|
||||
}
|
||||
if raw, err := os.ReadFile(runtimeConfigs.storageConf); err == nil {
|
||||
for _, m := range graphRoot.FindAllStringSubmatch(string(raw), -1) {
|
||||
add(m[1] + m[2])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// unitLines reads a unit file as systemd does for what matters here: a line ending in a backslash continues on the
|
||||
// next, Environment= sets variables (quoted or not, several to a line), EnvironmentFile= (a leading - says it may
|
||||
// be missing) reads KEY=value lines, and every ExecStart line is returned whole.
|
||||
func unitLines(text string) (map[string]string, []string) {
|
||||
var joined []string
|
||||
var cur strings.Builder
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
trimmed := strings.TrimRight(line, " \t")
|
||||
if strings.HasSuffix(trimmed, "\\") {
|
||||
cur.WriteString(strings.TrimSuffix(trimmed, "\\") + " ")
|
||||
continue
|
||||
}
|
||||
cur.WriteString(line)
|
||||
joined = append(joined, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
joined = append(joined, cur.String())
|
||||
}
|
||||
env := map[string]string{}
|
||||
setPairs := func(s string) {
|
||||
for _, f := range splitQuoted(s) {
|
||||
if k, v, ok := strings.Cut(f, "="); ok {
|
||||
env[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), `"'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
var execs []string
|
||||
for _, line := range joined {
|
||||
l := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(l, "Environment="):
|
||||
setPairs(strings.TrimPrefix(l, "Environment="))
|
||||
case strings.HasPrefix(l, "EnvironmentFile="):
|
||||
path := strings.TrimPrefix(strings.TrimSpace(strings.TrimPrefix(l, "EnvironmentFile=")), "-")
|
||||
if raw, err := os.ReadFile(path); err == nil {
|
||||
for _, kv := range strings.Split(string(raw), "\n") {
|
||||
kv = strings.TrimSpace(kv)
|
||||
if kv == "" || strings.HasPrefix(kv, "#") {
|
||||
continue
|
||||
}
|
||||
setPairs(kv)
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(l, "ExecStart"):
|
||||
execs = append(execs, l)
|
||||
}
|
||||
}
|
||||
return env, execs
|
||||
}
|
||||
|
||||
// splitQuoted splits on blanks outside double or single quotes, keeping the quotes' contents whole.
|
||||
func splitQuoted(s string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
var quote rune
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case quote != 0 && r == quote:
|
||||
quote = 0
|
||||
case quote == 0 && (r == '"' || r == '\''):
|
||||
quote = r
|
||||
case quote == 0 && (r == ' ' || r == '\t'):
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// engineModule is the module whose resources may place in the engine's own trees.
|
||||
const engineModule = "mesh-host"
|
||||
|
||||
// passwdFile is the user database homes are read from. A variable so a test names its own.
|
||||
var passwdFile = "/etc/passwd"
|
||||
|
||||
// PlacementRefusedError is a resource the engine will not place, or mount, where it says.
|
||||
type PlacementRefusedError struct {
|
||||
Path, Why string
|
||||
}
|
||||
|
||||
func (e *PlacementRefusedError) Error() string {
|
||||
return fmt.Sprintf("%s is not placed: %s (novox/hq issue 339); nothing was touched", e.Path, e.Why)
|
||||
}
|
||||
|
||||
// homeAccount is a person's or an agent's account and its home.
|
||||
type homeAccount struct {
|
||||
Name string
|
||||
UID int
|
||||
}
|
||||
|
||||
// accountsOfHomes is each person's or agent's home and the account it belongs to, from the user database: an
|
||||
// account with a uid of 1000 or more, or a home under /home. A variable so a test names its own.
|
||||
var accountsOfHomes = func() map[string]homeAccount {
|
||||
f, err := os.Open(passwdFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
out := map[string]homeAccount{}
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
fields := strings.Split(sc.Text(), ":")
|
||||
if len(fields) < 6 {
|
||||
continue
|
||||
}
|
||||
uid, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
home := filepath.Clean(fields[5])
|
||||
if home == "/" || home == "." || home == "" || home == "/nonexistent" {
|
||||
continue
|
||||
}
|
||||
if (uid >= 1000 && uid != 65534) || strings.HasPrefix(home, "/home/") {
|
||||
out[home] = homeAccount{Name: fields[0], UID: uid}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// below says whether path is strictly below dir.
|
||||
func below(path, dir string) bool {
|
||||
if dir == "/" {
|
||||
return path != "/"
|
||||
}
|
||||
return strings.HasPrefix(path, dir+"/")
|
||||
}
|
||||
|
||||
// atOrBelow says whether path is dir or below it.
|
||||
func atOrBelow(path, dir string) bool { return path == dir || below(path, dir) }
|
||||
|
||||
// resolved is a path with every link in it followed, as far as the path exists, and the rest as declared.
|
||||
func resolved(path string) string {
|
||||
rest := ""
|
||||
for p := path; ; p = filepath.Dir(p) {
|
||||
if real, err := filepath.EvalSymlinks(p); err == nil {
|
||||
return filepath.Clean(filepath.Join(real, rest))
|
||||
}
|
||||
if filepath.Dir(p) == p {
|
||||
return path
|
||||
}
|
||||
rest = filepath.Join(filepath.Base(p), rest)
|
||||
}
|
||||
}
|
||||
|
||||
// ownedByAccount says whether a declared owner is that account: by name, or by its uid ("1001", "1001:1001").
|
||||
func ownedByAccount(owner string, a homeAccount) bool {
|
||||
if owner == a.Name {
|
||||
return true
|
||||
}
|
||||
user, _, _ := strings.Cut(owner, ":")
|
||||
if uid, err := strconv.Atoi(user); err == nil {
|
||||
return uid == a.UID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// rootOwner says whether a declared owner is root: none, "root", or uid 0.
|
||||
func rootOwner(owner string) bool {
|
||||
if owner == "" || owner == "root" {
|
||||
return true
|
||||
}
|
||||
user, _, _ := strings.Cut(owner, ":")
|
||||
return user == "0"
|
||||
}
|
||||
|
||||
// what a guarded path is, for the words of a refusal.
|
||||
type placing int
|
||||
|
||||
const (
|
||||
placingDirectory placing = iota
|
||||
placingAccess
|
||||
placingMount
|
||||
)
|
||||
|
||||
// refusePath says why a path is not placed or mounted; nil when it may be. module is the resource's module,
|
||||
// owner a directory's declared owner.
|
||||
func refusePath(path string, kind placing, module, owner string) error {
|
||||
clean := filepath.Clean(path)
|
||||
if !filepath.IsAbs(clean) {
|
||||
return nil // the declaration refuses a relative path already; a named volume is not a path
|
||||
}
|
||||
for _, p := range []string{clean, resolved(clean)} {
|
||||
if err := refuseOne(p, kind, module, owner); err != nil {
|
||||
if p != clean {
|
||||
err.Why = fmt.Sprintf("through a link, it is %s, and %s", p, err.Why)
|
||||
err.Path = clean
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refuseOne(path string, kind placing, module, owner string) *PlacementRefusedError {
|
||||
for _, root := range protectedRoots {
|
||||
if path == root || below(root, path) {
|
||||
return &PlacementRefusedError{Path: path, Why: root + " is one of the machine's own directories, " +
|
||||
"and owning or mounting it would be owning or mounting everything in it"}
|
||||
}
|
||||
}
|
||||
trees := append([]string(nil), forbiddenBelow...)
|
||||
if kind == placingMount {
|
||||
// A mount is the machine's plumbing as often as a module's data — the clock, a kernel file, /dev/null
|
||||
// (systemPath) — and what a setting can mount at all is a directory or an access, refused above it.
|
||||
trees = append([]string(nil), forbiddenBelowMount...)
|
||||
}
|
||||
// The container runtimes' data, wherever the machine keeps it: every container's filesystem is there.
|
||||
trees = append(trees, runtimeDataRoots()...)
|
||||
for _, tree := range trees {
|
||||
if atOrBelow(path, tree) {
|
||||
return &PlacementRefusedError{Path: path, Why: "nothing is placed in or mounted from " + tree}
|
||||
}
|
||||
}
|
||||
if module != engineModule {
|
||||
for _, tree := range engineTrees {
|
||||
if atOrBelow(path, tree) {
|
||||
return &PlacementRefusedError{Path: path, Why: tree + " is the node-engine's own, placed in by " +
|
||||
"its own module alone"}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tree := range rootsOnly {
|
||||
if !below(path, tree) {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case kind == placingAccess:
|
||||
return &PlacementRefusedError{Path: path, Why: "an access is the operator's data, and " + tree +
|
||||
" is the machine's own"}
|
||||
case kind == placingDirectory && !rootOwner(owner):
|
||||
return &PlacementRefusedError{Path: path, Why: fmt.Sprintf("a directory below %s is root's, and this "+
|
||||
"one is declared %s's", tree, owner)}
|
||||
}
|
||||
}
|
||||
ssh := false
|
||||
for _, part := range strings.Split(path, "/") {
|
||||
ssh = ssh || part == ".ssh"
|
||||
}
|
||||
if ssh && kind != placingDirectory {
|
||||
return &PlacementRefusedError{Path: path, Why: "it is an account's .ssh, which holds its keys and who may " +
|
||||
"log in as it, and is never an access or a mount"}
|
||||
}
|
||||
if kind != placingDirectory {
|
||||
return nil
|
||||
}
|
||||
homes := accountsOfHomes()
|
||||
var deepest string
|
||||
for home := range homes {
|
||||
if below(path, home) && len(home) > len(deepest) {
|
||||
deepest = home
|
||||
}
|
||||
}
|
||||
if ssh && deepest == "" {
|
||||
return &PlacementRefusedError{Path: path, Why: "a .ssh directory is placed only below its account's home, " +
|
||||
"as that account's"}
|
||||
}
|
||||
if deepest != "" {
|
||||
if a := homes[deepest]; !ownedByAccount(owner, a) {
|
||||
if owner == "" {
|
||||
owner = "root"
|
||||
}
|
||||
return &PlacementRefusedError{Path: path, Why: fmt.Sprintf("it is below %s's home and declared %s's; "+
|
||||
"below a home only that account's directories are placed", a.Name, owner)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// moduleOfID is the module a resource id names, or "".
|
||||
func moduleOfID(id string) string {
|
||||
module, _ := moduleOf(id)
|
||||
return module
|
||||
}
|
||||
|
||||
// refusedPlaces judges every directory and access of a declaration before anything is applied, and answers
|
||||
// each refusal by path: what is refused is refused again as a container's mount source.
|
||||
func refusedPlaces(resources []declaration.Resource) map[string]error {
|
||||
out := map[string]error{}
|
||||
for _, r := range resources {
|
||||
var err error
|
||||
switch res := r.(type) {
|
||||
case *declaration.Directory:
|
||||
err = refusePath(res.Path, placingDirectory, moduleOfID(res.ID), res.Owner)
|
||||
case *declaration.Access:
|
||||
err = refusePath(res.Path, placingAccess, moduleOfID(res.ID), "")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
out[filepath.Clean(r.Target())] = err
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// refuseMounts says why a container's mounts are not made; nil when they may be.
|
||||
func refuseMounts(c *declaration.Container, refused map[string]error) error {
|
||||
for _, v := range c.Volumes {
|
||||
src := mountSource(v)
|
||||
if !strings.HasPrefix(src, "/") {
|
||||
continue // a named volume, which the runtime keeps in its own tree
|
||||
}
|
||||
src = filepath.Clean(src)
|
||||
for path, why := range refused {
|
||||
if atOrBelow(src, path) {
|
||||
var refusal *PlacementRefusedError
|
||||
if errors.As(why, &refusal) {
|
||||
return &PlacementRefusedError{Path: src, Why: "it is mounted from " + path +
|
||||
", which is refused: " + refusal.Why}
|
||||
}
|
||||
return why
|
||||
}
|
||||
}
|
||||
if err := refusePath(src, placingMount, moduleOfID(c.ID), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user