gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/dockerutil/exec.go (about)

     1  // Copyright 2020 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dockerutil
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/docker/docker/api/types"
    24  	"github.com/moby/moby/pkg/stdcopy"
    25  )
    26  
    27  // ExecOpts holds arguments for Exec calls.
    28  type ExecOpts struct {
    29  	// Env are additional environment variables.
    30  	Env []string
    31  
    32  	// Privileged enables privileged mode.
    33  	Privileged bool
    34  
    35  	// User is the user to use.
    36  	User string
    37  
    38  	// Enables Tty and stdin for the created process.
    39  	UseTTY bool
    40  
    41  	// WorkDir is the working directory of the process.
    42  	WorkDir string
    43  }
    44  
    45  // ExecError is returned when a process terminated with a non-zero exit status.
    46  // It implements `error`.
    47  type ExecError struct {
    48  	ExitStatus int
    49  }
    50  
    51  // Error implements `error.Error`.
    52  func (ee *ExecError) Error() string {
    53  	return fmt.Sprintf("process terminated with status: %d", ee.ExitStatus)
    54  }
    55  
    56  // Exec creates a process inside the container.
    57  // If the process exits with a non-zero error code, the error will be of
    58  // type `ExecError`.
    59  func (c *Container) Exec(ctx context.Context, opts ExecOpts, args ...string) (string, error) {
    60  	p, err := c.doExec(ctx, opts, args)
    61  	if err != nil {
    62  		return "", err
    63  	}
    64  	done := make(chan struct{})
    65  	var (
    66  		out    string
    67  		outErr error
    68  	)
    69  	// Read logs from another go-routine to be sure that it doesn't block on
    70  	// writing into standard file descriptors.
    71  	go func() {
    72  		out, outErr = p.Logs()
    73  		close(done)
    74  	}()
    75  
    76  	if exitStatus, err := p.WaitExitStatus(ctx); err != nil {
    77  		return "", err
    78  	} else if exitStatus != 0 {
    79  		<-done
    80  		return out, &ExecError{exitStatus}
    81  	}
    82  
    83  	<-done
    84  	return out, outErr
    85  }
    86  
    87  // ExecProcess creates a process inside the container and returns a process struct
    88  // for the caller to use.
    89  func (c *Container) ExecProcess(ctx context.Context, opts ExecOpts, args ...string) (Process, error) {
    90  	return c.doExec(ctx, opts, args)
    91  }
    92  
    93  func (c *Container) doExec(ctx context.Context, r ExecOpts, args []string) (Process, error) {
    94  	config := c.execConfig(r, args)
    95  	resp, err := c.client.ContainerExecCreate(ctx, c.id, config)
    96  	if err != nil {
    97  		return Process{}, fmt.Errorf("exec create failed with err: %v", err)
    98  	}
    99  
   100  	hijack, err := c.client.ContainerExecAttach(ctx, resp.ID, types.ExecStartCheck{})
   101  	if err != nil {
   102  		return Process{}, fmt.Errorf("exec attach failed with err: %v", err)
   103  	}
   104  
   105  	return Process{
   106  		container: c,
   107  		execid:    resp.ID,
   108  		conn:      hijack,
   109  	}, nil
   110  }
   111  
   112  func (c *Container) execConfig(r ExecOpts, cmd []string) types.ExecConfig {
   113  	env := append(r.Env, fmt.Sprintf("RUNSC_TEST_NAME=%s", c.Name))
   114  	return types.ExecConfig{
   115  		AttachStdin:  r.UseTTY,
   116  		AttachStderr: true,
   117  		AttachStdout: true,
   118  		Cmd:          cmd,
   119  		Privileged:   r.Privileged,
   120  		WorkingDir:   r.WorkDir,
   121  		Env:          env,
   122  		Tty:          r.UseTTY,
   123  		User:         r.User,
   124  	}
   125  }
   126  
   127  // Process represents a containerized process.
   128  type Process struct {
   129  	container *Container
   130  	execid    string
   131  	conn      types.HijackedResponse
   132  }
   133  
   134  // Write writes buf to the process's stdin.
   135  func (p *Process) Write(timeout time.Duration, buf []byte) (int, error) {
   136  	p.conn.Conn.SetDeadline(time.Now().Add(timeout))
   137  	return p.conn.Conn.Write(buf)
   138  }
   139  
   140  // Read returns process's stdout and stderr.
   141  func (p *Process) Read() (string, string, error) {
   142  	var stdout, stderr bytes.Buffer
   143  	if err := p.read(&stdout, &stderr); err != nil {
   144  		return "", "", err
   145  	}
   146  	return stdout.String(), stderr.String(), nil
   147  }
   148  
   149  // Logs returns combined stdout/stderr from the process.
   150  func (p *Process) Logs() (string, error) {
   151  	var out bytes.Buffer
   152  	if err := p.read(&out, &out); err != nil {
   153  		return "", err
   154  	}
   155  	return out.String(), nil
   156  }
   157  
   158  func (p *Process) read(stdout, stderr *bytes.Buffer) error {
   159  	_, err := stdcopy.StdCopy(stdout, stderr, p.conn.Reader)
   160  	return err
   161  }
   162  
   163  // ExitCode returns the process's exit code.
   164  func (p *Process) ExitCode(ctx context.Context) (int, error) {
   165  	_, exitCode, err := p.runningExitCode(ctx)
   166  	return exitCode, err
   167  }
   168  
   169  // IsRunning checks if the process is running.
   170  func (p *Process) IsRunning(ctx context.Context) (bool, error) {
   171  	running, _, err := p.runningExitCode(ctx)
   172  	return running, err
   173  }
   174  
   175  // WaitExitStatus until process completes and returns exit status.
   176  func (p *Process) WaitExitStatus(ctx context.Context) (int, error) {
   177  	waitChan := make(chan (int))
   178  	errChan := make(chan (error))
   179  
   180  	go func() {
   181  		for {
   182  			running, exitcode, err := p.runningExitCode(ctx)
   183  			if err != nil {
   184  				errChan <- fmt.Errorf("error waiting process %s: container %v", p.execid, p.container.Name)
   185  			}
   186  			if !running {
   187  				waitChan <- exitcode
   188  			}
   189  			time.Sleep(time.Millisecond * 500)
   190  		}
   191  	}()
   192  
   193  	select {
   194  	case ws := <-waitChan:
   195  		return ws, nil
   196  	case err := <-errChan:
   197  		return -1, err
   198  	}
   199  }
   200  
   201  // runningExitCode collects if the process is running and the exit code.
   202  // The exit code is only valid if the process has exited.
   203  func (p *Process) runningExitCode(ctx context.Context) (bool, int, error) {
   204  	// If execid is not empty, this is a execed process.
   205  	if p.execid != "" {
   206  		status, err := p.container.client.ContainerExecInspect(ctx, p.execid)
   207  		return status.Running, status.ExitCode, err
   208  	}
   209  	// else this is the root process.
   210  	status, err := p.container.Status(ctx)
   211  	return status.Running, status.ExitCode, err
   212  }