github.com/metacubex/gvisor@v0.0.0-20240320004321-933faba989ec/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  }