gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/shim/runsc/runsc.go (about) 1 // Copyright 2018 The containerd Authors. 2 // Copyright 2018 The gVisor Authors. 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // https://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 // Package runsc provides an API to interact with runsc command line. 17 package runsc 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "strconv" 30 "time" 31 32 "github.com/containerd/containerd/log" 33 runc "github.com/containerd/go-runc" 34 specs "github.com/opencontainers/runtime-spec/specs-go" 35 "golang.org/x/sys/unix" 36 ) 37 38 // DefaultCommand is the default command for Runsc. 39 const DefaultCommand = "runsc" 40 41 // ProcessMonitor is a subset of runc.ProcessMonitor. It does not include 42 // StartLocked(), which was added in containerd/runc v1.1.1. This is so that 43 // we can continue using containerd/containerd v1.4.13 with newer 44 // containerd/runc versions without breaking build. 45 type ProcessMonitor interface { 46 Start(cmd *exec.Cmd) (chan runc.Exit, error) 47 Wait(cmd *exec.Cmd, ch chan runc.Exit) (int, error) 48 } 49 50 // Monitor is the default process monitor to be used by runsc. 51 var Monitor ProcessMonitor = &LogMonitor{Next: runc.Monitor} 52 53 // LogMonitor implements the runc.ProcessMonitor interface, logging the command 54 // that is getting executed, and then forwarding the call to another 55 // implementation. 56 type LogMonitor struct { 57 Next ProcessMonitor 58 } 59 60 // Start implements runc.ProcessMonitor. 61 func (l *LogMonitor) Start(cmd *exec.Cmd) (chan runc.Exit, error) { 62 log.L.Debugf("Executing: %s", cmd.Args) 63 return l.Next.Start(cmd) 64 } 65 66 // Wait implements runc.ProcessMonitor. 67 func (l *LogMonitor) Wait(cmd *exec.Cmd, ch chan runc.Exit) (int, error) { 68 status, err := l.Next.Wait(cmd, ch) 69 log.L.Debugf("Command exit code: %d, err: %v", status, err) 70 return status, err 71 } 72 73 // Runsc is the client to the runsc cli. 74 type Runsc struct { 75 Command string 76 PdeathSignal unix.Signal 77 Setpgid bool 78 Root string 79 Log string 80 LogFormat runc.Format 81 PanicLog string 82 Config map[string]string 83 } 84 85 // List returns all containers created inside the provided runsc root directory. 86 func (r *Runsc) List(context context.Context) ([]*runc.Container, error) { 87 data, stderr, err := cmdOutput(r.command(context, "list", "--format=json"), false) 88 if err != nil { 89 return nil, fmt.Errorf("%w: %s", err, stderr) 90 } 91 var out []*runc.Container 92 if err := json.Unmarshal(data, &out); err != nil { 93 return nil, err 94 } 95 return out, nil 96 } 97 98 // State returns the state for the container provided by id. 99 func (r *Runsc) State(context context.Context, id string) (*runc.Container, error) { 100 data, stderr, err := cmdOutput(r.command(context, "state", id), false) 101 if err != nil { 102 return nil, fmt.Errorf("%w: %s", err, stderr) 103 } 104 var c runc.Container 105 if err := json.Unmarshal(data, &c); err != nil { 106 return nil, err 107 } 108 return &c, nil 109 } 110 111 // CreateOpts is a set of options to Runsc.Create(). 112 type CreateOpts struct { 113 runc.IO 114 ConsoleSocket runc.ConsoleSocket 115 116 // PidFile is a path to where a pid file should be created. 117 PidFile string 118 119 // UserLog is a path to where runsc user log should be generated. 120 UserLog string 121 } 122 123 func (o *CreateOpts) args() (out []string, err error) { 124 if o.PidFile != "" { 125 abs, err := filepath.Abs(o.PidFile) 126 if err != nil { 127 return nil, err 128 } 129 out = append(out, "--pid-file", abs) 130 } 131 if o.ConsoleSocket != nil { 132 out = append(out, "--console-socket", o.ConsoleSocket.Path()) 133 } 134 if o.UserLog != "" { 135 out = append(out, "--user-log", o.UserLog) 136 } 137 return out, nil 138 } 139 140 // Create creates a new container and returns its pid if it was created successfully. 141 func (r *Runsc) Create(context context.Context, id, bundle string, opts *CreateOpts) error { 142 args := []string{"create", "--bundle", bundle} 143 if opts != nil { 144 oargs, err := opts.args() 145 if err != nil { 146 return err 147 } 148 args = append(args, oargs...) 149 } 150 cmd := r.command(context, append(args, id)...) 151 if opts != nil && opts.IO != nil { 152 opts.Set(cmd) 153 } 154 155 if cmd.Stdout == nil && cmd.Stderr == nil { 156 out, _, err := cmdOutput(cmd, true) 157 if err != nil { 158 return fmt.Errorf("%w: %s", err, out) 159 } 160 return nil 161 } 162 ec, err := Monitor.Start(cmd) 163 if err != nil { 164 return err 165 } 166 if opts != nil && opts.IO != nil { 167 if c, ok := opts.IO.(runc.StartCloser); ok { 168 if err := c.CloseAfterStart(); err != nil { 169 return err 170 } 171 } 172 } 173 status, err := Monitor.Wait(cmd, ec) 174 if err == nil && status != 0 { 175 err = fmt.Errorf("%s did not terminate successfully", cmd.Args[0]) 176 } 177 178 return err 179 } 180 181 func (r *Runsc) Pause(context context.Context, id string) error { 182 if out, _, err := cmdOutput(r.command(context, "pause", id), true); err != nil { 183 return fmt.Errorf("unable to pause: %w: %s", err, out) 184 } 185 return nil 186 } 187 188 func (r *Runsc) Resume(context context.Context, id string) error { 189 if out, _, err := cmdOutput(r.command(context, "resume", id), true); err != nil { 190 return fmt.Errorf("unable to resume: %w: %s", err, out) 191 } 192 return nil 193 } 194 195 // Start will start an already created container. 196 func (r *Runsc) Start(context context.Context, id string, cio runc.IO) error { 197 cmd := r.command(context, "start", id) 198 if cio != nil { 199 cio.Set(cmd) 200 } 201 202 if cmd.Stdout == nil && cmd.Stderr == nil { 203 out, _, err := cmdOutput(cmd, true) 204 if err != nil { 205 return fmt.Errorf("%w: %s", err, out) 206 } 207 return nil 208 } 209 210 ec, err := Monitor.Start(cmd) 211 if err != nil { 212 return err 213 } 214 if cio != nil { 215 if c, ok := cio.(runc.StartCloser); ok { 216 if err := c.CloseAfterStart(); err != nil { 217 return err 218 } 219 } 220 } 221 status, err := Monitor.Wait(cmd, ec) 222 if err == nil && status != 0 { 223 err = fmt.Errorf("%s did not terminate successfully", cmd.Args[0]) 224 } 225 226 return err 227 } 228 229 type waitResult struct { 230 ID string `json:"id"` 231 ExitStatus int `json:"exitStatus"` 232 } 233 234 // Wait will wait for a running container, and return its exit status. 235 func (r *Runsc) Wait(context context.Context, id string) (int, error) { 236 data, stderr, err := cmdOutput(r.command(context, "wait", id), false) 237 if err != nil { 238 return 0, fmt.Errorf("%w: %s", err, stderr) 239 } 240 var res waitResult 241 if err := json.Unmarshal(data, &res); err != nil { 242 return 0, err 243 } 244 return res.ExitStatus, nil 245 } 246 247 // ExecOpts is a set of options to runsc.Exec(). 248 type ExecOpts struct { 249 runc.IO 250 PidFile string 251 InternalPidFile string 252 ConsoleSocket runc.ConsoleSocket 253 Detach bool 254 } 255 256 func (o *ExecOpts) args() (out []string, err error) { 257 if o.ConsoleSocket != nil { 258 out = append(out, "--console-socket", o.ConsoleSocket.Path()) 259 } 260 if o.Detach { 261 out = append(out, "--detach") 262 } 263 if o.PidFile != "" { 264 abs, err := filepath.Abs(o.PidFile) 265 if err != nil { 266 return nil, err 267 } 268 out = append(out, "--pid-file", abs) 269 } 270 if o.InternalPidFile != "" { 271 abs, err := filepath.Abs(o.InternalPidFile) 272 if err != nil { 273 return nil, err 274 } 275 out = append(out, "--internal-pid-file", abs) 276 } 277 return out, nil 278 } 279 280 // Exec executes an additional process inside the container based on a full OCI 281 // Process specification. 282 func (r *Runsc) Exec(context context.Context, id string, spec specs.Process, opts *ExecOpts) error { 283 f, err := ioutil.TempFile(os.Getenv("XDG_RUNTIME_DIR"), "runsc-process") 284 if err != nil { 285 return err 286 } 287 defer os.Remove(f.Name()) 288 err = json.NewEncoder(f).Encode(spec) 289 f.Close() 290 if err != nil { 291 return err 292 } 293 args := []string{"exec", "--process", f.Name()} 294 if opts != nil { 295 oargs, err := opts.args() 296 if err != nil { 297 return err 298 } 299 args = append(args, oargs...) 300 } 301 cmd := r.command(context, append(args, id)...) 302 if opts != nil && opts.IO != nil { 303 opts.Set(cmd) 304 } 305 if cmd.Stdout == nil && cmd.Stderr == nil { 306 out, _, err := cmdOutput(cmd, true) 307 if err != nil { 308 return fmt.Errorf("%w: %s", err, out) 309 } 310 return nil 311 } 312 ec, err := Monitor.Start(cmd) 313 if err != nil { 314 return err 315 } 316 if opts != nil && opts.IO != nil { 317 if c, ok := opts.IO.(runc.StartCloser); ok { 318 if err := c.CloseAfterStart(); err != nil { 319 return err 320 } 321 } 322 } 323 status, err := Monitor.Wait(cmd, ec) 324 if err == nil && status != 0 { 325 err = fmt.Errorf("%s did not terminate successfully", cmd.Args[0]) 326 } 327 return err 328 } 329 330 // Run runs the create, start, delete lifecycle of the container and returns 331 // its exit status after it has exited. 332 func (r *Runsc) Run(context context.Context, id, bundle string, opts *CreateOpts) (int, error) { 333 args := []string{"run", "--bundle", bundle} 334 if opts != nil { 335 oargs, err := opts.args() 336 if err != nil { 337 return -1, err 338 } 339 args = append(args, oargs...) 340 } 341 cmd := r.command(context, append(args, id)...) 342 if opts != nil && opts.IO != nil { 343 opts.Set(cmd) 344 } 345 ec, err := Monitor.Start(cmd) 346 if err != nil { 347 return -1, err 348 } 349 return Monitor.Wait(cmd, ec) 350 } 351 352 // DeleteOpts is a set of options to runsc.Delete(). 353 type DeleteOpts struct { 354 Force bool 355 } 356 357 func (o *DeleteOpts) args() (out []string) { 358 if o.Force { 359 out = append(out, "--force") 360 } 361 return out 362 } 363 364 // Delete deletes the container. 365 func (r *Runsc) Delete(context context.Context, id string, opts *DeleteOpts) error { 366 args := []string{"delete"} 367 if opts != nil { 368 args = append(args, opts.args()...) 369 } 370 return r.runOrError(r.command(context, append(args, id)...)) 371 } 372 373 // KillOpts specifies options for killing a container and its processes. 374 type KillOpts struct { 375 All bool 376 Pid int 377 } 378 379 func (o *KillOpts) args() (out []string) { 380 if o.All { 381 out = append(out, "--all") 382 } 383 if o.Pid != 0 { 384 out = append(out, "--pid", strconv.Itoa(o.Pid)) 385 } 386 return out 387 } 388 389 // Kill sends the specified signal to the container. 390 func (r *Runsc) Kill(context context.Context, id string, sig int, opts *KillOpts) error { 391 args := []string{ 392 "kill", 393 } 394 if opts != nil { 395 args = append(args, opts.args()...) 396 } 397 return r.runOrError(r.command(context, append(args, id, strconv.Itoa(sig))...)) 398 } 399 400 // Stats return the stats for a container like cpu, memory, and I/O. 401 func (r *Runsc) Stats(context context.Context, id string) (*runc.Stats, error) { 402 cmd := r.command(context, "events", "--stats", id) 403 data, stderr, err := cmdOutput(cmd, false) 404 if err != nil { 405 return nil, fmt.Errorf("%w: %s", err, stderr) 406 } 407 var e runc.Event 408 if err := json.Unmarshal(data, &e); err != nil { 409 log.L.Debugf("Parsing events error: %v", err) 410 return nil, err 411 } 412 log.L.Debugf("Stats returned, type: %s, stats: %+v", e.Type, e.Stats) 413 if e.Type != "stats" { 414 return nil, fmt.Errorf(`unexpected event type %q, wanted "stats"`, e.Type) 415 } 416 if e.Stats == nil { 417 return nil, fmt.Errorf(`"runsc events -stat" succeeded but no stat was provided`) 418 } 419 return e.Stats, nil 420 } 421 422 // Events returns an event stream from runsc for a container with stats and OOM notifications. 423 func (r *Runsc) Events(context context.Context, id string, interval time.Duration) (chan *runc.Event, error) { 424 cmd := r.command(context, "events", fmt.Sprintf("--interval=%ds", int(interval.Seconds())), id) 425 rd, err := cmd.StdoutPipe() 426 if err != nil { 427 return nil, err 428 } 429 ec, err := Monitor.Start(cmd) 430 if err != nil { 431 rd.Close() 432 return nil, err 433 } 434 var ( 435 dec = json.NewDecoder(rd) 436 c = make(chan *runc.Event, 128) 437 ) 438 go func() { 439 defer func() { 440 close(c) 441 rd.Close() 442 Monitor.Wait(cmd, ec) 443 }() 444 for { 445 var e runc.Event 446 if err := dec.Decode(&e); err != nil { 447 if err == io.EOF { 448 return 449 } 450 e = runc.Event{ 451 Type: "error", 452 Err: err, 453 } 454 } 455 c <- &e 456 } 457 }() 458 return c, nil 459 } 460 461 // Ps lists all the processes inside the container returning their pids. 462 func (r *Runsc) Ps(context context.Context, id string) ([]int, error) { 463 data, stderr, err := cmdOutput(r.command(context, "ps", "--format", "json", id), false) 464 if err != nil { 465 return nil, fmt.Errorf("%w: %s", err, stderr) 466 } 467 var pids []int 468 if err := json.Unmarshal(data, &pids); err != nil { 469 return nil, err 470 } 471 return pids, nil 472 } 473 474 // Top lists all the processes inside the container returning the full ps data. 475 func (r *Runsc) Top(context context.Context, id string) (*runc.TopResults, error) { 476 data, stderr, err := cmdOutput(r.command(context, "ps", "--format", "table", id), false) 477 if err != nil { 478 return nil, fmt.Errorf("%w: %s", err, stderr) 479 } 480 481 topResults, err := runc.ParsePSOutput(data) 482 if err != nil { 483 return nil, fmt.Errorf("%s: ", err) 484 } 485 return topResults, nil 486 } 487 488 func (r *Runsc) args() []string { 489 var args []string 490 if r.Root != "" { 491 args = append(args, fmt.Sprintf("--root=%s", r.Root)) 492 } 493 if r.Log != "" { 494 args = append(args, fmt.Sprintf("--log=%s", r.Log)) 495 } 496 if r.LogFormat != "" { 497 args = append(args, fmt.Sprintf("--log-format=%s", r.LogFormat)) 498 } 499 if r.PanicLog != "" { 500 args = append(args, fmt.Sprintf("--panic-log=%s", r.PanicLog)) 501 } 502 for k, v := range r.Config { 503 args = append(args, fmt.Sprintf("--%s=%s", k, v)) 504 } 505 return args 506 } 507 508 // runOrError will run the provided command. 509 // 510 // If an error is encountered and neither Stdout or Stderr was set the error 511 // will be returned in the format of <error>: <stderr>. 512 func (r *Runsc) runOrError(cmd *exec.Cmd) error { 513 if cmd.Stdout != nil || cmd.Stderr != nil { 514 ec, err := Monitor.Start(cmd) 515 if err != nil { 516 return err 517 } 518 status, err := Monitor.Wait(cmd, ec) 519 if err == nil && status != 0 { 520 err = fmt.Errorf("%s did not terminate successfully", cmd.Args[0]) 521 } 522 return err 523 } 524 out, _, err := cmdOutput(cmd, true) 525 if err != nil { 526 return fmt.Errorf("%w: %s", err, out) 527 } 528 return nil 529 } 530 531 func (r *Runsc) command(context context.Context, args ...string) *exec.Cmd { 532 command := r.Command 533 if command == "" { 534 command = DefaultCommand 535 } 536 cmd := exec.CommandContext(context, command, append(r.args(), args...)...) 537 cmd.SysProcAttr = &unix.SysProcAttr{ 538 Setpgid: r.Setpgid, 539 } 540 if r.PdeathSignal != 0 { 541 cmd.SysProcAttr.Pdeathsig = r.PdeathSignal 542 } 543 544 return cmd 545 } 546 547 func cmdOutput(cmd *exec.Cmd, combined bool) ([]byte, []byte, error) { 548 stdout := getBuf() 549 defer putBuf(stdout) 550 cmd.Stdout = stdout 551 cmd.Stderr = stdout 552 553 var stderr *bytes.Buffer 554 if !combined { 555 stderr = getBuf() 556 defer putBuf(stderr) 557 cmd.Stderr = stderr 558 } 559 ec, err := Monitor.Start(cmd) 560 if err != nil { 561 return nil, nil, err 562 } 563 564 status, err := Monitor.Wait(cmd, ec) 565 if err == nil && status != 0 { 566 err = fmt.Errorf("%q did not terminate successfully", cmd.Args[0]) 567 } 568 if stderr == nil { 569 return stdout.Bytes(), nil, err 570 } 571 return stdout.Bytes(), stderr.Bytes(), err 572 }