...

Source file src/k8s.io/utils/nsenter/nsenter.go

Documentation: k8s.io/utils/nsenter

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2017 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package nsenter
    21  
    22  import (
    23  	"context"
    24  	"errors"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"k8s.io/klog/v2"
    31  	"k8s.io/utils/exec"
    32  )
    33  
    34  const (
    35  	// DefaultHostRootFsPath is path to host's filesystem mounted into container
    36  	// with kubelet.
    37  	DefaultHostRootFsPath = "/rootfs"
    38  	// mountNsPath is the default mount namespace of the host
    39  	mountNsPath = "/proc/1/ns/mnt"
    40  	// nsenterPath is the default nsenter command
    41  	nsenterPath = "nsenter"
    42  )
    43  
    44  // Nsenter is a type alias for backward compatibility
    45  type Nsenter = NSEnter
    46  
    47  // NSEnter is part of experimental support for running the kubelet
    48  // in a container.
    49  //
    50  // NSEnter requires:
    51  //
    52  //  1. Docker >= 1.6 due to the dependency on the slave propagation mode
    53  //     of the bind-mount of the kubelet root directory in the container.
    54  //     Docker 1.5 used a private propagation mode for bind-mounts, so mounts
    55  //     performed in the host's mount namespace do not propagate out to the
    56  //     bind-mount in this docker version.
    57  //  2. The host's root filesystem must be available at /rootfs
    58  //  3. The nsenter binary must be on the Kubelet process' PATH in the container's
    59  //     filesystem.
    60  //  4. The Kubelet process must have CAP_SYS_ADMIN (required by nsenter); at
    61  //     the present, this effectively means that the kubelet is running in a
    62  //     privileged container.
    63  //  5. The volume path used by the Kubelet must be the same inside and outside
    64  //     the container and be writable by the container (to initialize volume)
    65  //     contents. TODO: remove this requirement.
    66  //  6. The host image must have "mount", "findmnt", "umount", "stat", "touch",
    67  //     "mkdir", "ls", "sh" and "chmod" binaries in /bin, /usr/sbin, or /usr/bin
    68  //  7. The host image should have systemd-run in /bin, /usr/sbin, or /usr/bin if
    69  //     systemd is installed/enabled in the operating system.
    70  //
    71  // For more information about mount propagation modes, see:
    72  //
    73  //	https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
    74  type NSEnter struct {
    75  	// a map of commands to their paths on the host filesystem
    76  	paths map[string]string
    77  
    78  	// Path to the host filesystem, typically "/rootfs". Used only for testing.
    79  	hostRootFsPath string
    80  
    81  	// Exec implementation
    82  	executor exec.Interface
    83  }
    84  
    85  // NewNsenter constructs a new instance of NSEnter
    86  func NewNsenter(hostRootFsPath string, executor exec.Interface) (*NSEnter, error) {
    87  	ne := &NSEnter{
    88  		hostRootFsPath: hostRootFsPath,
    89  		executor:       executor,
    90  	}
    91  	if err := ne.initPaths(); err != nil {
    92  		return nil, err
    93  	}
    94  	return ne, nil
    95  }
    96  
    97  func (ne *NSEnter) initPaths() error {
    98  	ne.paths = map[string]string{}
    99  	binaries := []string{
   100  		"mount",
   101  		"findmnt",
   102  		"umount",
   103  		"systemd-run",
   104  		"stat",
   105  		"touch",
   106  		"mkdir",
   107  		"sh",
   108  		"chmod",
   109  		"realpath",
   110  	}
   111  	// search for the required commands in other locations besides /usr/bin
   112  	for _, binary := range binaries {
   113  		// check for binary under the following directories
   114  		for _, path := range []string{"/", "/bin", "/usr/sbin", "/usr/bin"} {
   115  			binPath := filepath.Join(path, binary)
   116  			if _, err := os.Stat(filepath.Join(ne.hostRootFsPath, binPath)); err != nil {
   117  				continue
   118  			}
   119  			ne.paths[binary] = binPath
   120  			break
   121  		}
   122  		// systemd-run is optional, bailout if we don't find any of the other binaries
   123  		if ne.paths[binary] == "" && binary != "systemd-run" {
   124  			return fmt.Errorf("unable to find %v", binary)
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  // Exec executes nsenter commands in hostProcMountNsPath mount namespace
   131  func (ne *NSEnter) Exec(cmd string, args []string) exec.Cmd {
   132  	hostProcMountNsPath := filepath.Join(ne.hostRootFsPath, mountNsPath)
   133  	fullArgs := append([]string{fmt.Sprintf("--mount=%s", hostProcMountNsPath), "--"},
   134  		append([]string{ne.AbsHostPath(cmd)}, args...)...)
   135  	klog.V(5).Infof("Running nsenter command: %v %v", nsenterPath, fullArgs)
   136  	return ne.executor.Command(nsenterPath, fullArgs...)
   137  }
   138  
   139  // Command returns a command wrapped with nsenter
   140  func (ne *NSEnter) Command(cmd string, args ...string) exec.Cmd {
   141  	return ne.Exec(cmd, args)
   142  }
   143  
   144  // CommandContext returns a CommandContext wrapped with nsenter
   145  func (ne *NSEnter) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
   146  	hostProcMountNsPath := filepath.Join(ne.hostRootFsPath, mountNsPath)
   147  	fullArgs := append([]string{fmt.Sprintf("--mount=%s", hostProcMountNsPath), "--"},
   148  		append([]string{ne.AbsHostPath(cmd)}, args...)...)
   149  	klog.V(5).Infof("Running nsenter command: %v %v", nsenterPath, fullArgs)
   150  	return ne.executor.CommandContext(ctx, nsenterPath, fullArgs...)
   151  }
   152  
   153  // LookPath returns a LookPath wrapped with nsenter
   154  func (ne *NSEnter) LookPath(file string) (string, error) {
   155  	return "", fmt.Errorf("not implemented, error looking up : %s", file)
   156  }
   157  
   158  // AbsHostPath returns the absolute runnable path for a specified command
   159  func (ne *NSEnter) AbsHostPath(command string) string {
   160  	path, ok := ne.paths[command]
   161  	if !ok {
   162  		return command
   163  	}
   164  	return path
   165  }
   166  
   167  // SupportsSystemd checks whether command systemd-run exists
   168  func (ne *NSEnter) SupportsSystemd() (string, bool) {
   169  	systemdRunPath, ok := ne.paths["systemd-run"]
   170  	return systemdRunPath, ok && systemdRunPath != ""
   171  }
   172  
   173  // EvalSymlinks returns the path name on the host after evaluating symlinks on the
   174  // host.
   175  // mustExist makes EvalSymlinks to return error when the path does not
   176  // exist. When it's false, it evaluates symlinks of the existing part and
   177  // blindly adds the non-existing part:
   178  // pathname: /mnt/volume/non/existing/directory
   179  //
   180  //	/mnt/volume exists
   181  //	           non/existing/directory does not exist
   182  //
   183  // -> It resolves symlinks in /mnt/volume to say /mnt/foo and returns
   184  //
   185  //	/mnt/foo/non/existing/directory.
   186  //
   187  // BEWARE! EvalSymlinks is not able to detect symlink looks with mustExist=false!
   188  // If /tmp/link is symlink to /tmp/link, EvalSymlinks(/tmp/link/foo) returns /tmp/link/foo.
   189  func (ne *NSEnter) EvalSymlinks(pathname string, mustExist bool) (string, error) {
   190  	var args []string
   191  	if mustExist {
   192  		// "realpath -e: all components of the path must exist"
   193  		args = []string{"-e", pathname}
   194  	} else {
   195  		// "realpath -m: no path components need exist or be a directory"
   196  		args = []string{"-m", pathname}
   197  	}
   198  	outBytes, err := ne.Exec("realpath", args).CombinedOutput()
   199  	if err != nil {
   200  		klog.Infof("failed to resolve symbolic links on %s: %v", pathname, err)
   201  		return "", err
   202  	}
   203  	return strings.TrimSpace(string(outBytes)), nil
   204  }
   205  
   206  // KubeletPath returns the path name that can be accessed by containerized
   207  // kubelet. It is recommended to resolve symlinks on the host by EvalSymlinks
   208  // before calling this function
   209  func (ne *NSEnter) KubeletPath(pathname string) string {
   210  	return filepath.Join(ne.hostRootFsPath, pathname)
   211  }
   212  
   213  // NewFakeNsenter returns a NSEnter that does not run "nsenter --mount=... --",
   214  // but runs everything in the same mount namespace as the unit test binary.
   215  // rootfsPath is supposed to be a symlink, e.g. /tmp/xyz/rootfs -> /.
   216  // This fake NSEnter is enough for most operations, e.g. to resolve symlinks,
   217  // but it's not enough to call /bin/mount - unit tests don't run as root.
   218  func NewFakeNsenter(rootfsPath string) (*NSEnter, error) {
   219  	executor := &fakeExec{
   220  		rootfsPath: rootfsPath,
   221  	}
   222  	// prepare /rootfs/bin, usr/bin and usr/sbin
   223  	bin := filepath.Join(rootfsPath, "bin")
   224  	if err := os.Symlink("/bin", bin); err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	usr := filepath.Join(rootfsPath, "usr")
   229  	if err := os.Mkdir(usr, 0755); err != nil {
   230  		return nil, err
   231  	}
   232  	usrbin := filepath.Join(usr, "bin")
   233  	if err := os.Symlink("/usr/bin", usrbin); err != nil {
   234  		return nil, err
   235  	}
   236  	usrsbin := filepath.Join(usr, "sbin")
   237  	if err := os.Symlink("/usr/sbin", usrsbin); err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	return NewNsenter(rootfsPath, executor)
   242  }
   243  
   244  type fakeExec struct {
   245  	rootfsPath string
   246  }
   247  
   248  func (f fakeExec) Command(cmd string, args ...string) exec.Cmd {
   249  	// This will intentionaly panic if NSEnter does not provide enough arguments.
   250  	realCmd := args[2]
   251  	realArgs := args[3:]
   252  	return exec.New().Command(realCmd, realArgs...)
   253  }
   254  
   255  func (fakeExec) LookPath(file string) (string, error) {
   256  	return "", errors.New("not implemented")
   257  }
   258  
   259  func (fakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
   260  	return nil
   261  }
   262  
   263  var _ exec.Interface = fakeExec{}
   264  var _ exec.Interface = &NSEnter{}
   265  

View as plain text