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