github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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 return nil, nil, err 63 } 64 defer socket.Close() 65 f, err := socket.File() 66 if err != nil { 67 return nil, nil, errors.Wrapf(err, "failed to get fd for socket %s", address) 68 } 69 defer f.Close() 70 71 stdoutCopy := ioutil.Discard 72 stderrCopy := ioutil.Discard 73 stdoutLog, err := v1.OpenShimStdoutLog(ctx, config.WorkDir) 74 if err != nil { 75 return nil, nil, errors.Wrapf(err, "failed to create stdout log") 76 } 77 78 stderrLog, err := v1.OpenShimStderrLog(ctx, config.WorkDir) 79 if err != nil { 80 return nil, nil, errors.Wrapf(err, "failed to create stderr log") 81 } 82 if debug { 83 stdoutCopy = os.Stdout 84 stderrCopy = os.Stderr 85 } 86 87 go io.Copy(stdoutCopy, stdoutLog) 88 go io.Copy(stderrCopy, stderrLog) 89 90 cmd, err := newCommand(binary, daemonAddress, debug, config, f, stdoutLog, stderrLog) 91 if err != nil { 92 return nil, nil, err 93 } 94 if err := cmd.Start(); err != nil { 95 return nil, nil, errors.Wrapf(err, "failed to start shim") 96 } 97 defer func() { 98 if err != nil { 99 cmd.Process.Kill() 100 } 101 }() 102 go func() { 103 cmd.Wait() 104 exitHandler() 105 if stdoutLog != nil { 106 stdoutLog.Close() 107 } 108 if stderrLog != nil { 109 stderrLog.Close() 110 } 111 }() 112 log.G(ctx).WithFields(logrus.Fields{ 113 "pid": cmd.Process.Pid, 114 "address": address, 115 "debug": debug, 116 }).Infof("shim %s started", binary) 117 118 if err := writeFile(filepath.Join(config.Path, "address"), address); err != nil { 119 return nil, nil, err 120 } 121 if err := writeFile(filepath.Join(config.Path, "shim.pid"), strconv.Itoa(cmd.Process.Pid)); err != nil { 122 return nil, nil, err 123 } 124 // set shim in cgroup if it is provided 125 if cgroup != "" { 126 if err := setCgroup(cgroup, cmd); err != nil { 127 return nil, nil, err 128 } 129 log.G(ctx).WithFields(logrus.Fields{ 130 "pid": cmd.Process.Pid, 131 "address": address, 132 }).Infof("shim placed in cgroup %s", cgroup) 133 } 134 if err = setupOOMScore(cmd.Process.Pid); err != nil { 135 return nil, nil, err 136 } 137 c, clo, err := WithConnect(address, func() {})(ctx, config) 138 if err != nil { 139 return nil, nil, errors.Wrap(err, "failed to connect") 140 } 141 return c, clo, nil 142 } 143 } 144 145 // setupOOMScore gets containerd's oom score and adds +1 to it 146 // to ensure a shim has a lower* score than the daemons 147 func setupOOMScore(shimPid int) error { 148 pid := os.Getpid() 149 score, err := sys.GetOOMScoreAdj(pid) 150 if err != nil { 151 return errors.Wrap(err, "get daemon OOM score") 152 } 153 shimScore := score + 1 154 if err := sys.SetOOMScore(shimPid, shimScore); err != nil { 155 return errors.Wrap(err, "set shim OOM score") 156 } 157 return nil 158 } 159 160 func newCommand(binary, daemonAddress string, debug bool, config shim.Config, socket *os.File, stdout, stderr io.Writer) (*exec.Cmd, error) { 161 selfExe, err := os.Executable() 162 if err != nil { 163 return nil, err 164 } 165 args := []string{ 166 "-namespace", config.Namespace, 167 "-workdir", config.WorkDir, 168 "-address", daemonAddress, 169 "-containerd-binary", selfExe, 170 } 171 172 if config.Criu != "" { 173 args = append(args, "-criu-path", config.Criu) 174 } 175 if config.RuntimeRoot != "" { 176 args = append(args, "-runtime-root", config.RuntimeRoot) 177 } 178 if config.SystemdCgroup { 179 args = append(args, "-systemd-cgroup") 180 } 181 if debug { 182 args = append(args, "-debug") 183 } 184 185 cmd := exec.Command(binary, args...) 186 cmd.Dir = config.Path 187 // make sure the shim can be re-parented to system init 188 // and is cloned in a new mount namespace because the overlay/filesystems 189 // will be mounted by the shim 190 cmd.SysProcAttr = getSysProcAttr() 191 cmd.ExtraFiles = append(cmd.ExtraFiles, socket) 192 cmd.Env = append(os.Environ(), "GOMAXPROCS=2") 193 cmd.Stdout = stdout 194 cmd.Stderr = stderr 195 return cmd, nil 196 } 197 198 // writeFile writes a address file atomically 199 func writeFile(path, address string) error { 200 path, err := filepath.Abs(path) 201 if err != nil { 202 return err 203 } 204 tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path))) 205 f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666) 206 if err != nil { 207 return err 208 } 209 _, err = f.WriteString(address) 210 f.Close() 211 if err != nil { 212 return err 213 } 214 return os.Rename(tempPath, path) 215 } 216 217 func newSocket(address string) (*net.UnixListener, error) { 218 if len(address) > 106 { 219 return nil, errors.Errorf("%q: unix socket path too long (> 106)", address) 220 } 221 l, err := net.Listen("unix", "\x00"+address) 222 if err != nil { 223 return nil, errors.Wrapf(err, "failed to listen to abstract unix socket %q", address) 224 } 225 226 return l.(*net.UnixListener), nil 227 } 228 229 func connect(address string, d func(string, time.Duration) (net.Conn, error)) (net.Conn, error) { 230 return d(address, 100*time.Second) 231 } 232 233 func annonDialer(address string, timeout time.Duration) (net.Conn, error) { 234 address = strings.TrimPrefix(address, "unix://") 235 return dialer.Dialer("\x00"+address, timeout) 236 } 237 238 // WithConnect connects to an existing shim 239 func WithConnect(address string, onClose func()) Opt { 240 return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) { 241 conn, err := connect(address, annonDialer) 242 if err != nil { 243 return nil, nil, err 244 } 245 client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose)) 246 return shimapi.NewShimClient(client), conn, nil 247 } 248 } 249 250 // WithLocal uses an in process shim 251 func WithLocal(publisher events.Publisher) func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error) { 252 return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) { 253 service, err := shim.NewService(config, publisher) 254 if err != nil { 255 return nil, nil, err 256 } 257 return shim.NewLocal(service), nil, nil 258 } 259 } 260 261 // New returns a new shim client 262 func New(ctx context.Context, config shim.Config, opt Opt) (*Client, error) { 263 s, c, err := opt(ctx, config) 264 if err != nil { 265 return nil, err 266 } 267 return &Client{ 268 ShimService: s, 269 c: c, 270 exitCh: make(chan struct{}), 271 }, nil 272 } 273 274 // Client is a shim client containing the connection to a shim 275 type Client struct { 276 shimapi.ShimService 277 278 c io.Closer 279 exitCh chan struct{} 280 exitOnce sync.Once 281 } 282 283 // IsAlive returns true if the shim can be contacted. 284 // NOTE: a negative answer doesn't mean that the process is gone. 285 func (c *Client) IsAlive(ctx context.Context) (bool, error) { 286 _, err := c.ShimInfo(ctx, empty) 287 if err != nil { 288 // TODO(stevvooe): There are some error conditions that need to be 289 // handle with unix sockets existence to give the right answer here. 290 return false, err 291 } 292 return true, nil 293 } 294 295 // StopShim signals the shim to exit and wait for the process to disappear 296 func (c *Client) StopShim(ctx context.Context) error { 297 return c.signalShim(ctx, unix.SIGTERM) 298 } 299 300 // KillShim kills the shim forcefully and wait for the process to disappear 301 func (c *Client) KillShim(ctx context.Context) error { 302 return c.signalShim(ctx, unix.SIGKILL) 303 } 304 305 // Close the client connection 306 func (c *Client) Close() error { 307 if c.c == nil { 308 return nil 309 } 310 return c.c.Close() 311 } 312 313 func (c *Client) signalShim(ctx context.Context, sig syscall.Signal) error { 314 info, err := c.ShimInfo(ctx, empty) 315 if err != nil { 316 return err 317 } 318 pid := int(info.ShimPid) 319 // make sure we don't kill ourselves if we are running a local shim 320 if os.Getpid() == pid { 321 return nil 322 } 323 if err := unix.Kill(pid, sig); err != nil && err != unix.ESRCH { 324 return err 325 } 326 // wait for shim to die after being signaled 327 select { 328 case <-ctx.Done(): 329 return ctx.Err() 330 case <-c.waitForExit(ctx, pid): 331 return nil 332 } 333 } 334 335 func (c *Client) waitForExit(ctx context.Context, pid int) <-chan struct{} { 336 go c.exitOnce.Do(func() { 337 defer close(c.exitCh) 338 339 ticker := time.NewTicker(10 * time.Millisecond) 340 defer ticker.Stop() 341 342 for { 343 // use kill(pid, 0) here because the shim could have been reparented 344 // and we are no longer able to waitpid(pid, ...) on the shim 345 if err := unix.Kill(pid, 0); err == unix.ESRCH { 346 return 347 } 348 349 select { 350 case <-ticker.C: 351 case <-ctx.Done(): 352 log.G(ctx).WithField("pid", pid).Warn("timed out while waiting for shim to exit") 353 return 354 } 355 } 356 }) 357 return c.exitCh 358 }