github.com/containerd/Containerd@v1.4.13/runtime/v1/shim/client/client.go (about) 1 // +build !windows 2 3 /* 4 Copyright The containerd 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 client 20 21 import ( 22 "context" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "net" 27 "os" 28 "os/exec" 29 "path/filepath" 30 "strconv" 31 "strings" 32 "sync" 33 "syscall" 34 "time" 35 36 "golang.org/x/sys/unix" 37 38 "github.com/containerd/ttrpc" 39 "github.com/pkg/errors" 40 "github.com/sirupsen/logrus" 41 42 "github.com/containerd/containerd/events" 43 "github.com/containerd/containerd/log" 44 "github.com/containerd/containerd/pkg/dialer" 45 v1 "github.com/containerd/containerd/runtime/v1" 46 "github.com/containerd/containerd/runtime/v1/shim" 47 shimapi "github.com/containerd/containerd/runtime/v1/shim/v1" 48 "github.com/containerd/containerd/sys" 49 ptypes "github.com/gogo/protobuf/types" 50 ) 51 52 var empty = &ptypes.Empty{} 53 54 // Opt is an option for a shim client configuration 55 type Opt func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error) 56 57 // WithStart executes a new shim process 58 func WithStart(binary, address, daemonAddress, cgroup string, debug bool, exitHandler func()) Opt { 59 return func(ctx context.Context, config shim.Config) (_ shimapi.ShimService, _ io.Closer, err error) { 60 socket, err := newSocket(address) 61 if err != nil { 62 if !eaddrinuse(err) { 63 return nil, nil, err 64 } 65 if err := RemoveSocket(address); err != nil { 66 return nil, nil, errors.Wrap(err, "remove already used socket") 67 } 68 if socket, err = newSocket(address); err != nil { 69 return nil, nil, err 70 } 71 } 72 73 f, err := socket.File() 74 if err != nil { 75 return nil, nil, errors.Wrapf(err, "failed to get fd for socket %s", address) 76 } 77 defer f.Close() 78 79 stdoutCopy := ioutil.Discard 80 stderrCopy := ioutil.Discard 81 stdoutLog, err := v1.OpenShimStdoutLog(ctx, config.WorkDir) 82 if err != nil { 83 return nil, nil, errors.Wrapf(err, "failed to create stdout log") 84 } 85 86 stderrLog, err := v1.OpenShimStderrLog(ctx, config.WorkDir) 87 if err != nil { 88 return nil, nil, errors.Wrapf(err, "failed to create stderr log") 89 } 90 if debug { 91 stdoutCopy = os.Stdout 92 stderrCopy = os.Stderr 93 } 94 95 go io.Copy(stdoutCopy, stdoutLog) 96 go io.Copy(stderrCopy, stderrLog) 97 98 cmd, err := newCommand(binary, daemonAddress, debug, config, f, stdoutLog, stderrLog) 99 if err != nil { 100 return nil, nil, err 101 } 102 if err := cmd.Start(); err != nil { 103 return nil, nil, errors.Wrapf(err, "failed to start shim") 104 } 105 defer func() { 106 if err != nil { 107 cmd.Process.Kill() 108 } 109 }() 110 go func() { 111 cmd.Wait() 112 exitHandler() 113 if stdoutLog != nil { 114 stdoutLog.Close() 115 } 116 if stderrLog != nil { 117 stderrLog.Close() 118 } 119 socket.Close() 120 RemoveSocket(address) 121 }() 122 log.G(ctx).WithFields(logrus.Fields{ 123 "pid": cmd.Process.Pid, 124 "address": address, 125 "debug": debug, 126 }).Infof("shim %s started", binary) 127 128 if err := writeFile(filepath.Join(config.Path, "address"), address); err != nil { 129 return nil, nil, err 130 } 131 if err := writeFile(filepath.Join(config.Path, "shim.pid"), strconv.Itoa(cmd.Process.Pid)); err != nil { 132 return nil, nil, err 133 } 134 // set shim in cgroup if it is provided 135 if cgroup != "" { 136 if err := setCgroup(cgroup, cmd); err != nil { 137 return nil, nil, err 138 } 139 log.G(ctx).WithFields(logrus.Fields{ 140 "pid": cmd.Process.Pid, 141 "address": address, 142 }).Infof("shim placed in cgroup %s", cgroup) 143 } 144 if err = setupOOMScore(cmd.Process.Pid); err != nil { 145 return nil, nil, err 146 } 147 c, clo, err := WithConnect(address, func() {})(ctx, config) 148 if err != nil { 149 return nil, nil, errors.Wrap(err, "failed to connect") 150 } 151 return c, clo, nil 152 } 153 } 154 155 func eaddrinuse(err error) bool { 156 cause := errors.Cause(err) 157 netErr, ok := cause.(*net.OpError) 158 if !ok { 159 return false 160 } 161 if netErr.Op != "listen" { 162 return false 163 } 164 syscallErr, ok := netErr.Err.(*os.SyscallError) 165 if !ok { 166 return false 167 } 168 errno, ok := syscallErr.Err.(syscall.Errno) 169 if !ok { 170 return false 171 } 172 return errno == syscall.EADDRINUSE 173 } 174 175 // setupOOMScore gets containerd's oom score and adds +1 to it 176 // to ensure a shim has a lower* score than the daemons 177 // if not already at the maximum OOM Score 178 func setupOOMScore(shimPid int) error { 179 pid := os.Getpid() 180 score, err := sys.GetOOMScoreAdj(pid) 181 if err != nil { 182 return errors.Wrap(err, "get daemon OOM score") 183 } 184 shimScore := score + 1 185 if shimScore > sys.OOMScoreAdjMax { 186 shimScore = sys.OOMScoreAdjMax 187 } 188 if err := sys.SetOOMScore(shimPid, shimScore); err != nil { 189 return errors.Wrap(err, "set shim OOM score") 190 } 191 return nil 192 } 193 194 func newCommand(binary, daemonAddress string, debug bool, config shim.Config, socket *os.File, stdout, stderr io.Writer) (*exec.Cmd, error) { 195 selfExe, err := os.Executable() 196 if err != nil { 197 return nil, err 198 } 199 args := []string{ 200 "-namespace", config.Namespace, 201 "-workdir", config.WorkDir, 202 "-address", daemonAddress, 203 "-containerd-binary", selfExe, 204 } 205 206 if config.Criu != "" { 207 args = append(args, "-criu-path", config.Criu) 208 } 209 if config.RuntimeRoot != "" { 210 args = append(args, "-runtime-root", config.RuntimeRoot) 211 } 212 if config.SystemdCgroup { 213 args = append(args, "-systemd-cgroup") 214 } 215 if debug { 216 args = append(args, "-debug") 217 } 218 219 cmd := exec.Command(binary, args...) 220 cmd.Dir = config.Path 221 // make sure the shim can be re-parented to system init 222 // and is cloned in a new mount namespace because the overlay/filesystems 223 // will be mounted by the shim 224 cmd.SysProcAttr = getSysProcAttr() 225 cmd.ExtraFiles = append(cmd.ExtraFiles, socket) 226 cmd.Env = append(os.Environ(), "GOMAXPROCS=2") 227 cmd.Stdout = stdout 228 cmd.Stderr = stderr 229 return cmd, nil 230 } 231 232 // writeFile writes a address file atomically 233 func writeFile(path, address string) error { 234 path, err := filepath.Abs(path) 235 if err != nil { 236 return err 237 } 238 tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path))) 239 f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666) 240 if err != nil { 241 return err 242 } 243 _, err = f.WriteString(address) 244 f.Close() 245 if err != nil { 246 return err 247 } 248 return os.Rename(tempPath, path) 249 } 250 251 const ( 252 abstractSocketPrefix = "\x00" 253 socketPathLimit = 106 254 ) 255 256 type socket string 257 258 func (s socket) isAbstract() bool { 259 return !strings.HasPrefix(string(s), "unix://") 260 } 261 262 func (s socket) path() string { 263 path := strings.TrimPrefix(string(s), "unix://") 264 // if there was no trim performed, we assume an abstract socket 265 if len(path) == len(s) { 266 path = abstractSocketPrefix + path 267 } 268 return path 269 } 270 271 func newSocket(address string) (*net.UnixListener, error) { 272 if len(address) > socketPathLimit { 273 return nil, errors.Errorf("%q: unix socket path too long (> %d)", address, socketPathLimit) 274 } 275 var ( 276 sock = socket(address) 277 path = sock.path() 278 ) 279 if !sock.isAbstract() { 280 if err := os.MkdirAll(filepath.Dir(path), 0600); err != nil { 281 return nil, errors.Wrapf(err, "%s", path) 282 } 283 } 284 l, err := net.Listen("unix", path) 285 if err != nil { 286 return nil, errors.Wrapf(err, "failed to listen to unix socket %q (abstract: %t)", address, sock.isAbstract()) 287 } 288 if err := os.Chmod(path, 0600); err != nil { 289 l.Close() 290 return nil, err 291 } 292 293 return l.(*net.UnixListener), nil 294 } 295 296 // RemoveSocket removes the socket at the specified address if 297 // it exists on the filesystem 298 func RemoveSocket(address string) error { 299 sock := socket(address) 300 if !sock.isAbstract() { 301 return os.Remove(sock.path()) 302 } 303 return nil 304 } 305 306 func connect(address string, d func(string, time.Duration) (net.Conn, error)) (net.Conn, error) { 307 return d(address, 100*time.Second) 308 } 309 310 func anonDialer(address string, timeout time.Duration) (net.Conn, error) { 311 return dialer.Dialer(socket(address).path(), timeout) 312 } 313 314 // WithConnect connects to an existing shim 315 func WithConnect(address string, onClose func()) Opt { 316 return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) { 317 conn, err := connect(address, anonDialer) 318 if err != nil { 319 return nil, nil, err 320 } 321 client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose)) 322 return shimapi.NewShimClient(client), conn, nil 323 } 324 } 325 326 // WithLocal uses an in process shim 327 func WithLocal(publisher events.Publisher) func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error) { 328 return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) { 329 service, err := shim.NewService(config, publisher) 330 if err != nil { 331 return nil, nil, err 332 } 333 return shim.NewLocal(service), nil, nil 334 } 335 } 336 337 // New returns a new shim client 338 func New(ctx context.Context, config shim.Config, opt Opt) (*Client, error) { 339 s, c, err := opt(ctx, config) 340 if err != nil { 341 return nil, err 342 } 343 return &Client{ 344 ShimService: s, 345 c: c, 346 exitCh: make(chan struct{}), 347 }, nil 348 } 349 350 // Client is a shim client containing the connection to a shim 351 type Client struct { 352 shimapi.ShimService 353 354 c io.Closer 355 exitCh chan struct{} 356 exitOnce sync.Once 357 } 358 359 // IsAlive returns true if the shim can be contacted. 360 // NOTE: a negative answer doesn't mean that the process is gone. 361 func (c *Client) IsAlive(ctx context.Context) (bool, error) { 362 _, err := c.ShimInfo(ctx, empty) 363 if err != nil { 364 // TODO(stevvooe): There are some error conditions that need to be 365 // handle with unix sockets existence to give the right answer here. 366 return false, err 367 } 368 return true, nil 369 } 370 371 // StopShim signals the shim to exit and wait for the process to disappear 372 func (c *Client) StopShim(ctx context.Context) error { 373 return c.signalShim(ctx, unix.SIGTERM) 374 } 375 376 // KillShim kills the shim forcefully and wait for the process to disappear 377 func (c *Client) KillShim(ctx context.Context) error { 378 return c.signalShim(ctx, unix.SIGKILL) 379 } 380 381 // Close the client connection 382 func (c *Client) Close() error { 383 if c.c == nil { 384 return nil 385 } 386 return c.c.Close() 387 } 388 389 func (c *Client) signalShim(ctx context.Context, sig syscall.Signal) error { 390 info, err := c.ShimInfo(ctx, empty) 391 if err != nil { 392 return err 393 } 394 pid := int(info.ShimPid) 395 // make sure we don't kill ourselves if we are running a local shim 396 if os.Getpid() == pid { 397 return nil 398 } 399 if err := unix.Kill(pid, sig); err != nil && err != unix.ESRCH { 400 return err 401 } 402 // wait for shim to die after being signaled 403 select { 404 case <-ctx.Done(): 405 return ctx.Err() 406 case <-c.waitForExit(ctx, pid): 407 return nil 408 } 409 } 410 411 func (c *Client) waitForExit(ctx context.Context, pid int) <-chan struct{} { 412 go c.exitOnce.Do(func() { 413 defer close(c.exitCh) 414 415 ticker := time.NewTicker(10 * time.Millisecond) 416 defer ticker.Stop() 417 418 for { 419 // use kill(pid, 0) here because the shim could have been reparented 420 // and we are no longer able to waitpid(pid, ...) on the shim 421 if err := unix.Kill(pid, 0); err == unix.ESRCH { 422 return 423 } 424 425 select { 426 case <-ticker.C: 427 case <-ctx.Done(): 428 log.G(ctx).WithField("pid", pid).Warn("timed out while waiting for shim to exit") 429 return 430 } 431 } 432 }) 433 return c.exitCh 434 }