github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/pkg/process/io.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 process
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"io"
    25  	"net/url"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"sync"
    30  	"sync/atomic"
    31  	"syscall"
    32  	"time"
    33  
    34  	"github.com/containerd/containerd/log"
    35  	"github.com/containerd/containerd/namespaces"
    36  	"github.com/containerd/containerd/pkg/stdio"
    37  	"github.com/containerd/containerd/sys"
    38  	"github.com/containerd/fifo"
    39  	runc "github.com/containerd/go-runc"
    40  	"github.com/hashicorp/go-multierror"
    41  	"github.com/pkg/errors"
    42  )
    43  
    44  const binaryIOProcTermTimeout = 12 * time.Second // Give logger process solid 10 seconds for cleanup
    45  
    46  var bufPool = sync.Pool{
    47  	New: func() interface{} {
    48  		// setting to 4096 to align with PIPE_BUF
    49  		// http://man7.org/linux/man-pages/man7/pipe.7.html
    50  		buffer := make([]byte, 4096)
    51  		return &buffer
    52  	},
    53  }
    54  
    55  type processIO struct {
    56  	io runc.IO
    57  
    58  	uri   *url.URL
    59  	copy  bool
    60  	stdio stdio.Stdio
    61  }
    62  
    63  func (p *processIO) Close() error {
    64  	if p.io != nil {
    65  		return p.io.Close()
    66  	}
    67  	return nil
    68  }
    69  
    70  func (p *processIO) IO() runc.IO {
    71  	return p.io
    72  }
    73  
    74  func (p *processIO) Copy(ctx context.Context, wg *sync.WaitGroup) error {
    75  	if !p.copy {
    76  		return nil
    77  	}
    78  	var cwg sync.WaitGroup
    79  	if err := copyPipes(ctx, p.IO(), p.stdio.Stdin, p.stdio.Stdout, p.stdio.Stderr, wg, &cwg); err != nil {
    80  		return errors.Wrap(err, "unable to copy pipes")
    81  	}
    82  	cwg.Wait()
    83  	return nil
    84  }
    85  
    86  func createIO(ctx context.Context, id string, ioUID, ioGID int, stdio stdio.Stdio) (*processIO, error) {
    87  	pio := &processIO{
    88  		stdio: stdio,
    89  	}
    90  	if stdio.IsNull() {
    91  		i, err := runc.NewNullIO()
    92  		if err != nil {
    93  			return nil, err
    94  		}
    95  		pio.io = i
    96  		return pio, nil
    97  	}
    98  	u, err := url.Parse(stdio.Stdout)
    99  	if err != nil {
   100  		return nil, errors.Wrap(err, "unable to parse stdout uri")
   101  	}
   102  	if u.Scheme == "" {
   103  		u.Scheme = "fifo"
   104  	}
   105  	pio.uri = u
   106  	switch u.Scheme {
   107  	case "fifo":
   108  		pio.copy = true
   109  		pio.io, err = runc.NewPipeIO(ioUID, ioGID, withConditionalIO(stdio))
   110  	case "binary":
   111  		pio.io, err = NewBinaryIO(ctx, id, u)
   112  	case "file":
   113  		filePath := u.Path
   114  		if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
   115  			return nil, err
   116  		}
   117  		var f *os.File
   118  		f, err = os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		f.Close()
   123  		pio.stdio.Stdout = filePath
   124  		pio.stdio.Stderr = filePath
   125  		pio.copy = true
   126  		pio.io, err = runc.NewPipeIO(ioUID, ioGID, withConditionalIO(stdio))
   127  	default:
   128  		return nil, errors.Errorf("unknown STDIO scheme %s", u.Scheme)
   129  	}
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	return pio, nil
   134  }
   135  
   136  func copyPipes(ctx context.Context, rio runc.IO, stdin, stdout, stderr string, wg, cwg *sync.WaitGroup) error {
   137  	var sameFile *countingWriteCloser
   138  	for _, i := range []struct {
   139  		name string
   140  		dest func(wc io.WriteCloser, rc io.Closer)
   141  	}{
   142  		{
   143  			name: stdout,
   144  			dest: func(wc io.WriteCloser, rc io.Closer) {
   145  				wg.Add(1)
   146  				cwg.Add(1)
   147  				go func() {
   148  					cwg.Done()
   149  					p := bufPool.Get().(*[]byte)
   150  					defer bufPool.Put(p)
   151  					if _, err := io.CopyBuffer(wc, rio.Stdout(), *p); err != nil {
   152  						log.G(ctx).Warn("error copying stdout")
   153  					}
   154  					wg.Done()
   155  					wc.Close()
   156  					if rc != nil {
   157  						rc.Close()
   158  					}
   159  				}()
   160  			},
   161  		}, {
   162  			name: stderr,
   163  			dest: func(wc io.WriteCloser, rc io.Closer) {
   164  				wg.Add(1)
   165  				cwg.Add(1)
   166  				go func() {
   167  					cwg.Done()
   168  					p := bufPool.Get().(*[]byte)
   169  					defer bufPool.Put(p)
   170  					if _, err := io.CopyBuffer(wc, rio.Stderr(), *p); err != nil {
   171  						log.G(ctx).Warn("error copying stderr")
   172  					}
   173  					wg.Done()
   174  					wc.Close()
   175  					if rc != nil {
   176  						rc.Close()
   177  					}
   178  				}()
   179  			},
   180  		},
   181  	} {
   182  		ok, err := sys.IsFifo(i.name)
   183  		if err != nil {
   184  			return err
   185  		}
   186  		var (
   187  			fw io.WriteCloser
   188  			fr io.Closer
   189  		)
   190  		if ok {
   191  			if fw, err = fifo.OpenFifo(ctx, i.name, syscall.O_WRONLY, 0); err != nil {
   192  				return errors.Wrapf(err, "containerd-shim: opening w/o fifo %q failed", i.name)
   193  			}
   194  			if fr, err = fifo.OpenFifo(ctx, i.name, syscall.O_RDONLY, 0); err != nil {
   195  				return errors.Wrapf(err, "containerd-shim: opening r/o fifo %q failed", i.name)
   196  			}
   197  		} else {
   198  			if sameFile != nil {
   199  				sameFile.count++
   200  				i.dest(sameFile, nil)
   201  				continue
   202  			}
   203  			if fw, err = os.OpenFile(i.name, syscall.O_WRONLY|syscall.O_APPEND, 0); err != nil {
   204  				return errors.Wrapf(err, "containerd-shim: opening file %q failed", i.name)
   205  			}
   206  			if stdout == stderr {
   207  				sameFile = &countingWriteCloser{
   208  					WriteCloser: fw,
   209  					count:       1,
   210  				}
   211  			}
   212  		}
   213  		i.dest(fw, fr)
   214  	}
   215  	if stdin == "" {
   216  		return nil
   217  	}
   218  	f, err := fifo.OpenFifo(context.Background(), stdin, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
   219  	if err != nil {
   220  		return fmt.Errorf("containerd-shim: opening %s failed: %s", stdin, err)
   221  	}
   222  	cwg.Add(1)
   223  	go func() {
   224  		cwg.Done()
   225  		p := bufPool.Get().(*[]byte)
   226  		defer bufPool.Put(p)
   227  
   228  		io.CopyBuffer(rio.Stdin(), f, *p)
   229  		rio.Stdin().Close()
   230  		f.Close()
   231  	}()
   232  	return nil
   233  }
   234  
   235  // countingWriteCloser masks io.Closer() until close has been invoked a certain number of times.
   236  type countingWriteCloser struct {
   237  	io.WriteCloser
   238  	count int64
   239  }
   240  
   241  func (c *countingWriteCloser) Close() error {
   242  	if atomic.AddInt64(&c.count, -1) > 0 {
   243  		return nil
   244  	}
   245  	return c.WriteCloser.Close()
   246  }
   247  
   248  // NewBinaryIO runs a custom binary process for pluggable shim logging
   249  func NewBinaryIO(ctx context.Context, id string, uri *url.URL) (_ runc.IO, err error) {
   250  	ns, err := namespaces.NamespaceRequired(ctx)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	var args []string
   256  	for k, vs := range uri.Query() {
   257  		args = append(args, k)
   258  		if len(vs) > 0 {
   259  			args = append(args, vs[0])
   260  		}
   261  	}
   262  
   263  	var closers []func() error
   264  	defer func() {
   265  		if err == nil {
   266  			return
   267  		}
   268  		result := multierror.Append(err)
   269  		for _, fn := range closers {
   270  			result = multierror.Append(result, fn())
   271  		}
   272  		err = multierror.Flatten(result)
   273  	}()
   274  
   275  	out, err := newPipe()
   276  	if err != nil {
   277  		return nil, errors.Wrap(err, "failed to create stdout pipes")
   278  	}
   279  	closers = append(closers, out.Close)
   280  
   281  	serr, err := newPipe()
   282  	if err != nil {
   283  		return nil, errors.Wrap(err, "failed to create stderr pipes")
   284  	}
   285  	closers = append(closers, serr.Close)
   286  
   287  	r, w, err := os.Pipe()
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	closers = append(closers, r.Close, w.Close)
   292  
   293  	cmd := exec.Command(uri.Path, args...)
   294  	cmd.Env = append(cmd.Env,
   295  		"CONTAINER_ID="+id,
   296  		"CONTAINER_NAMESPACE="+ns,
   297  	)
   298  
   299  	cmd.ExtraFiles = append(cmd.ExtraFiles, out.r, serr.r, w)
   300  	// don't need to register this with the reaper or wait when
   301  	// running inside a shim
   302  	if err := cmd.Start(); err != nil {
   303  		return nil, errors.Wrap(err, "failed to start binary process")
   304  	}
   305  	closers = append(closers, func() error { return cmd.Process.Kill() })
   306  
   307  	// close our side of the pipe after start
   308  	if err := w.Close(); err != nil {
   309  		return nil, errors.Wrap(err, "failed to close write pipe after start")
   310  	}
   311  
   312  	// wait for the logging binary to be ready
   313  	b := make([]byte, 1)
   314  	if _, err := r.Read(b); err != nil && err != io.EOF {
   315  		return nil, errors.Wrap(err, "failed to read from logging binary")
   316  	}
   317  
   318  	return &binaryIO{
   319  		cmd: cmd,
   320  		out: out,
   321  		err: serr,
   322  	}, nil
   323  }
   324  
   325  type binaryIO struct {
   326  	cmd      *exec.Cmd
   327  	out, err *pipe
   328  }
   329  
   330  func (b *binaryIO) CloseAfterStart() error {
   331  	var (
   332  		result *multierror.Error
   333  	)
   334  
   335  	for _, v := range []*pipe{b.out, b.err} {
   336  		if v != nil {
   337  			if err := v.r.Close(); err != nil {
   338  				result = multierror.Append(result, err)
   339  			}
   340  		}
   341  	}
   342  
   343  	return result.ErrorOrNil()
   344  }
   345  
   346  func (b *binaryIO) Close() error {
   347  	var (
   348  		result *multierror.Error
   349  	)
   350  
   351  	for _, v := range []*pipe{b.out, b.err} {
   352  		if v != nil {
   353  			if err := v.Close(); err != nil {
   354  				result = multierror.Append(result, err)
   355  			}
   356  		}
   357  	}
   358  
   359  	if err := b.cancel(); err != nil {
   360  		result = multierror.Append(result, err)
   361  	}
   362  
   363  	return result.ErrorOrNil()
   364  }
   365  
   366  func (b *binaryIO) cancel() error {
   367  	if b.cmd == nil || b.cmd.Process == nil {
   368  		return nil
   369  	}
   370  
   371  	// Send SIGTERM first, so logger process has a chance to flush and exit properly
   372  	if err := b.cmd.Process.Signal(syscall.SIGTERM); err != nil {
   373  		result := multierror.Append(errors.Wrap(err, "failed to send SIGTERM"))
   374  
   375  		log.L.WithError(err).Warn("failed to send SIGTERM signal, killing logging shim")
   376  
   377  		if err := b.cmd.Process.Kill(); err != nil {
   378  			result = multierror.Append(result, errors.Wrap(err, "failed to kill process after faulty SIGTERM"))
   379  		}
   380  
   381  		return result.ErrorOrNil()
   382  	}
   383  
   384  	done := make(chan error, 1)
   385  	go func() {
   386  		done <- b.cmd.Wait()
   387  	}()
   388  
   389  	select {
   390  	case err := <-done:
   391  		return err
   392  	case <-time.After(binaryIOProcTermTimeout):
   393  		log.L.Warn("failed to wait for shim logger process to exit, killing")
   394  
   395  		err := b.cmd.Process.Kill()
   396  		if err != nil {
   397  			return errors.Wrap(err, "failed to kill shim logger process")
   398  		}
   399  
   400  		return nil
   401  	}
   402  }
   403  
   404  func (b *binaryIO) Stdin() io.WriteCloser {
   405  	return nil
   406  }
   407  
   408  func (b *binaryIO) Stdout() io.ReadCloser {
   409  	return nil
   410  }
   411  
   412  func (b *binaryIO) Stderr() io.ReadCloser {
   413  	return nil
   414  }
   415  
   416  func (b *binaryIO) Set(cmd *exec.Cmd) {
   417  	if b.out != nil {
   418  		cmd.Stdout = b.out.w
   419  	}
   420  	if b.err != nil {
   421  		cmd.Stderr = b.err.w
   422  	}
   423  }
   424  
   425  func newPipe() (*pipe, error) {
   426  	r, w, err := os.Pipe()
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  	return &pipe{
   431  		r: r,
   432  		w: w,
   433  	}, nil
   434  }
   435  
   436  type pipe struct {
   437  	r *os.File
   438  	w *os.File
   439  }
   440  
   441  func (p *pipe) Close() error {
   442  	var result *multierror.Error
   443  
   444  	if err := p.w.Close(); err != nil {
   445  		result = multierror.Append(result, errors.Wrap(err, "failed to close write pipe"))
   446  	}
   447  
   448  	if err := p.r.Close(); err != nil {
   449  		result = multierror.Append(result, errors.Wrap(err, "failed to close read pipe"))
   450  	}
   451  
   452  	return multierror.Prefix(result.ErrorOrNil(), "pipe:")
   453  }