Files
nomad/client/lib/cgroupslib/mount.go
Tim Gross 682c8c0c81 cgroupslib: allow initial controller check with delegated cgroups v2 (#23803)
During Nomad client initialization with cgroups v2, we assert that the required
cgroup controllers are available in the root `cgroup.subtree_control` file by
idempotently writing to the file. But if Nomad is running with delegated
cgroups, this will fail file permissions checks even if the subtree control file
already has the controllers we need.

Update the initialization to first check if the controllers are missing before
attempting to write to them. This allows cgroup delegation so long as the
cluster administrator has pre-created a Nomad owned cgroups tree and set the
`Delegate` option in a systemd override. If not, initialization fails in the
existing way.

Although this is one small step along the way to supporting a rootless Nomad
client, running Nomad as non-root is still unsupported. I've intentionally not
documented setting up cgroup delegation in this PR, as this PR is insufficient
by itself to have a secure and properly-working rootless Nomad client.

Ref: https://github.com/hashicorp/nomad/issues/18211
Ref: https://github.com/hashicorp/nomad/issues/13669
2024-08-14 16:58:21 -04:00

88 lines
1.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build linux
package cgroupslib
import (
"bufio"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/hashicorp/go-set/v2"
)
// detect tries to detect which cgroups version we have by looking at the mount
// and whether Nomad owns the cgroup.
// - For cgroups v1 this requires root.
// - For cgroups v2 we look for root or whether we're the owner of the slice.
// - All other cases, including any file permission errors, return OFF.
func detect() Mode {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return OFF
}
defer func() {
_ = f.Close()
}()
mode := scan(f)
if mode == CG1 && os.Geteuid() > 0 {
return OFF
}
if mode == CG2 {
if !functionalCgroups2("cgroup.controllers") {
return OFF
}
uid := os.Geteuid()
if uid > 0 {
// allow for cgroup delegation if we own the slice
cgPath := filepathCG("nomad.slice")
fi, err := os.Stat(cgPath)
if err != nil {
return OFF
}
if uid != int(fi.Sys().(*syscall.Stat_t).Uid) {
return OFF
}
}
}
return mode
}
func functionalCgroups2(controllersFile string) bool {
requiredCgroup2Controllers := []string{"cpuset", "cpu", "io", "memory", "pids"}
controllersRootPath := filepath.Join(root, controllersFile)
content, err := os.ReadFile(controllersRootPath)
if err != nil {
return false
}
rootSubtreeControllers := set.From[string](strings.Fields(string(content)))
return rootSubtreeControllers.ContainsSlice(requiredCgroup2Controllers)
}
func scan(in io.Reader) Mode {
scanner := bufio.NewScanner(in)
for scanner.Scan() {
tokens := set.From(strings.Fields(scanner.Text()))
if tokens.Contains("/sys/fs/cgroup") {
if tokens.Contains("tmpfs") {
return CG1
}
if tokens.Contains("cgroup2") {
return CG2
}
}
}
return OFF
}