github.com/kubernetes/utils@v0.0.0-20190308190857-21c4ce38f2a7/nsenter/nsenter.go (about)

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