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