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{}