123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- // +build linux
- /*
- Copyright 2017 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package nsenter
- import (
- "context"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "k8s.io/klog"
- "k8s.io/utils/exec"
- )
- const (
- // DefaultHostRootFsPath is path to host's filesystem mounted into container
- // with kubelet.
- DefaultHostRootFsPath = "/rootfs"
- // mountNsPath is the default mount namespace of the host
- mountNsPath = "/proc/1/ns/mnt"
- // nsenterPath is the default nsenter command
- nsenterPath = "nsenter"
- )
- // Nsenter is a type alias for backward compatibility
- type Nsenter = NSEnter
- // NSEnter is part of experimental support for running the kubelet
- // in a container.
- //
- // NSEnter requires:
- //
- // 1. Docker >= 1.6 due to the dependency on the slave propagation mode
- // of the bind-mount of the kubelet root directory in the container.
- // Docker 1.5 used a private propagation mode for bind-mounts, so mounts
- // performed in the host's mount namespace do not propagate out to the
- // bind-mount in this docker version.
- // 2. The host's root filesystem must be available at /rootfs
- // 3. The nsenter binary must be on the Kubelet process' PATH in the container's
- // filesystem.
- // 4. The Kubelet process must have CAP_SYS_ADMIN (required by nsenter); at
- // the present, this effectively means that the kubelet is running in a
- // privileged container.
- // 5. The volume path used by the Kubelet must be the same inside and outside
- // the container and be writable by the container (to initialize volume)
- // contents. TODO: remove this requirement.
- // 6. The host image must have "mount", "findmnt", "umount", "stat", "touch",
- // "mkdir", "ls", "sh" and "chmod" binaries in /bin, /usr/sbin, or /usr/bin
- // 7. The host image should have systemd-run in /bin, /usr/sbin, or /usr/bin if
- // systemd is installed/enabled in the operating system.
- // For more information about mount propagation modes, see:
- // https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
- type NSEnter struct {
- // a map of commands to their paths on the host filesystem
- paths map[string]string
- // Path to the host filesystem, typically "/rootfs". Used only for testing.
- hostRootFsPath string
- // Exec implementation
- executor exec.Interface
- }
- // NewNsenter constructs a new instance of NSEnter
- func NewNsenter(hostRootFsPath string, executor exec.Interface) (*NSEnter, error) {
- ne := &NSEnter{
- hostRootFsPath: hostRootFsPath,
- executor: executor,
- }
- if err := ne.initPaths(); err != nil {
- return nil, err
- }
- return ne, nil
- }
- func (ne *NSEnter) initPaths() error {
- ne.paths = map[string]string{}
- binaries := []string{
- "mount",
- "findmnt",
- "umount",
- "systemd-run",
- "stat",
- "touch",
- "mkdir",
- "sh",
- "chmod",
- "realpath",
- }
- // search for the required commands in other locations besides /usr/bin
- for _, binary := range binaries {
- // check for binary under the following directories
- for _, path := range []string{"/", "/bin", "/usr/sbin", "/usr/bin"} {
- binPath := filepath.Join(path, binary)
- if _, err := os.Stat(filepath.Join(ne.hostRootFsPath, binPath)); err != nil {
- continue
- }
- ne.paths[binary] = binPath
- break
- }
- // systemd-run is optional, bailout if we don't find any of the other binaries
- if ne.paths[binary] == "" && binary != "systemd-run" {
- return fmt.Errorf("unable to find %v", binary)
- }
- }
- return nil
- }
- // Exec executes nsenter commands in hostProcMountNsPath mount namespace
- func (ne *NSEnter) Exec(cmd string, args []string) exec.Cmd {
- hostProcMountNsPath := filepath.Join(ne.hostRootFsPath, mountNsPath)
- fullArgs := append([]string{fmt.Sprintf("--mount=%s", hostProcMountNsPath), "--"},
- append([]string{ne.AbsHostPath(cmd)}, args...)...)
- klog.V(5).Infof("Running nsenter command: %v %v", nsenterPath, fullArgs)
- return ne.executor.Command(nsenterPath, fullArgs...)
- }
- // Command returns a command wrapped with nsenter
- func (ne *NSEnter) Command(cmd string, args ...string) exec.Cmd {
- return ne.Exec(cmd, args)
- }
- // CommandContext returns a CommandContext wrapped with nsenter
- func (ne *NSEnter) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
- hostProcMountNsPath := filepath.Join(ne.hostRootFsPath, mountNsPath)
- fullArgs := append([]string{fmt.Sprintf("--mount=%s", hostProcMountNsPath), "--"},
- append([]string{ne.AbsHostPath(cmd)}, args...)...)
- klog.V(5).Infof("Running nsenter command: %v %v", nsenterPath, fullArgs)
- return ne.executor.CommandContext(ctx, nsenterPath, fullArgs...)
- }
- // LookPath returns a LookPath wrapped with nsenter
- func (ne *NSEnter) LookPath(file string) (string, error) {
- return "", fmt.Errorf("not implemented, error looking up : %s", file)
- }
- // AbsHostPath returns the absolute runnable path for a specified command
- func (ne *NSEnter) AbsHostPath(command string) string {
- path, ok := ne.paths[command]
- if !ok {
- return command
- }
- return path
- }
- // SupportsSystemd checks whether command systemd-run exists
- func (ne *NSEnter) SupportsSystemd() (string, bool) {
- systemdRunPath, ok := ne.paths["systemd-run"]
- return systemdRunPath, ok && systemdRunPath != ""
- }
- // EvalSymlinks returns the path name on the host after evaluating symlinks on the
- // host.
- // mustExist makes EvalSymlinks to return error when the path does not
- // exist. When it's false, it evaluates symlinks of the existing part and
- // blindly adds the non-existing part:
- // pathname: /mnt/volume/non/existing/directory
- // /mnt/volume exists
- // non/existing/directory does not exist
- // -> It resolves symlinks in /mnt/volume to say /mnt/foo and returns
- // /mnt/foo/non/existing/directory.
- //
- // BEWARE! EvalSymlinks is not able to detect symlink looks with mustExist=false!
- // If /tmp/link is symlink to /tmp/link, EvalSymlinks(/tmp/link/foo) returns /tmp/link/foo.
- func (ne *NSEnter) EvalSymlinks(pathname string, mustExist bool) (string, error) {
- var args []string
- if mustExist {
- // "realpath -e: all components of the path must exist"
- args = []string{"-e", pathname}
- } else {
- // "realpath -m: no path components need exist or be a directory"
- args = []string{"-m", pathname}
- }
- outBytes, err := ne.Exec("realpath", args).CombinedOutput()
- if err != nil {
- klog.Infof("failed to resolve symbolic links on %s: %v", pathname, err)
- return "", err
- }
- return strings.TrimSpace(string(outBytes)), nil
- }
- // KubeletPath returns the path name that can be accessed by containerized
- // kubelet. It is recommended to resolve symlinks on the host by EvalSymlinks
- // before calling this function
- func (ne *NSEnter) KubeletPath(pathname string) string {
- return filepath.Join(ne.hostRootFsPath, pathname)
- }
- // NewFakeNsenter returns a NSEnter that does not run "nsenter --mount=... --",
- // but runs everything in the same mount namespace as the unit test binary.
- // rootfsPath is supposed to be a symlink, e.g. /tmp/xyz/rootfs -> /.
- // This fake NSEnter is enough for most operations, e.g. to resolve symlinks,
- // but it's not enough to call /bin/mount - unit tests don't run as root.
- func NewFakeNsenter(rootfsPath string) (*NSEnter, error) {
- executor := &fakeExec{
- rootfsPath: rootfsPath,
- }
- // prepare /rootfs/bin, usr/bin and usr/sbin
- bin := filepath.Join(rootfsPath, "bin")
- if err := os.Symlink("/bin", bin); err != nil {
- return nil, err
- }
- usr := filepath.Join(rootfsPath, "usr")
- if err := os.Mkdir(usr, 0755); err != nil {
- return nil, err
- }
- usrbin := filepath.Join(usr, "bin")
- if err := os.Symlink("/usr/bin", usrbin); err != nil {
- return nil, err
- }
- usrsbin := filepath.Join(usr, "sbin")
- if err := os.Symlink("/usr/sbin", usrsbin); err != nil {
- return nil, err
- }
- return NewNsenter(rootfsPath, executor)
- }
- type fakeExec struct {
- rootfsPath string
- }
- func (f fakeExec) Command(cmd string, args ...string) exec.Cmd {
- // This will intentionaly panic if NSEnter does not provide enough arguments.
- realCmd := args[2]
- realArgs := args[3:]
- return exec.New().Command(realCmd, realArgs...)
- }
- func (fakeExec) LookPath(file string) (string, error) {
- return "", errors.New("not implemented")
- }
- func (fakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
- return nil
- }
- var _ exec.Interface = fakeExec{}
- var _ exec.Interface = &NSEnter{}
|