github.com/containerd/nerdctl@v1.7.7/pkg/taskutil/taskutil.go (about)

     1  /*
     2     Copyright The containerd 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         http://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  
    17  package taskutil
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"io"
    23  	"net/url"
    24  	"os"
    25  	"runtime"
    26  	"slices"
    27  	"strings"
    28  	"sync"
    29  	"syscall"
    30  
    31  	"github.com/Masterminds/semver/v3"
    32  	"github.com/containerd/console"
    33  	"github.com/containerd/containerd"
    34  	"github.com/containerd/containerd/cio"
    35  	"github.com/containerd/log"
    36  	"github.com/containerd/nerdctl/pkg/cioutil"
    37  	"github.com/containerd/nerdctl/pkg/consoleutil"
    38  	"github.com/containerd/nerdctl/pkg/infoutil"
    39  	"golang.org/x/term"
    40  )
    41  
    42  // NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
    43  func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container,
    44  	attachStreamOpt []string, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
    45  
    46  	var t containerd.Task
    47  	closer := func() {
    48  		if detachC != nil {
    49  			detachC <- struct{}{}
    50  		}
    51  		// t will be set by container.NewTask at the end of this function.
    52  		//
    53  		// We cannot use container.Task(ctx, cio.Load) to get the IO here
    54  		// because the `cancel` field of the returned `*cio` is nil. [1]
    55  		//
    56  		// [1] https://github.com/containerd/containerd/blob/8f756bc8c26465bd93e78d9cd42082b66f276e10/cio/io.go#L358-L359
    57  		io := t.IO()
    58  		if io == nil {
    59  			log.G(ctx).Errorf("got a nil io")
    60  			return
    61  		}
    62  		io.Cancel()
    63  	}
    64  	var ioCreator cio.Creator
    65  	if len(attachStreamOpt) != 0 {
    66  		log.G(ctx).Debug("attaching output instead of using the log-uri")
    67  		if flagT {
    68  			in, err := consoleutil.NewDetachableStdin(con, detachKeys, closer)
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  			ioCreator = cio.NewCreator(cio.WithStreams(in, con, nil), cio.WithTerminal)
    73  		} else {
    74  			streams := processAttachStreamsOpt(attachStreamOpt)
    75  			ioCreator = cio.NewCreator(cio.WithStreams(streams.stdIn, streams.stdOut, streams.stdErr))
    76  		}
    77  
    78  	} else if flagT && flagD {
    79  		u, err := url.Parse(logURI)
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  
    84  		var args []string
    85  		for k, vs := range u.Query() {
    86  			args = append(args, k)
    87  			if len(vs) > 0 {
    88  				args = append(args, vs[0])
    89  			}
    90  		}
    91  
    92  		// args[0]: _NERDCTL_INTERNAL_LOGGING
    93  		// args[1]: /var/lib/nerdctl/1935db59
    94  		if len(args) != 2 {
    95  			return nil, errors.New("parse logging path error")
    96  		}
    97  		ioCreator = cio.TerminalBinaryIO(u.Path, map[string]string{
    98  			args[0]: args[1],
    99  		})
   100  	} else if flagT && !flagD {
   101  
   102  		if con == nil {
   103  			return nil, errors.New("got nil con with flagT=true")
   104  		}
   105  		var in io.Reader
   106  		if flagI {
   107  			// FIXME: check IsTerminal on Windows too
   108  			if runtime.GOOS != "windows" && !term.IsTerminal(0) {
   109  				return nil, errors.New("the input device is not a TTY")
   110  			}
   111  			var err error
   112  			in, err = consoleutil.NewDetachableStdin(con, detachKeys, closer)
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  		}
   117  		ioCreator = cioutil.NewContainerIO(namespace, logURI, true, in, os.Stdout, os.Stderr)
   118  	} else if flagD && logURI != "" {
   119  		u, err := url.Parse(logURI)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  		ioCreator = cio.LogURI(u)
   124  	} else {
   125  		var in io.Reader
   126  		if flagI {
   127  			if sv, err := infoutil.ServerSemVer(ctx, client); err != nil {
   128  				log.G(ctx).Warn(err)
   129  			} else if sv.LessThan(semver.MustParse("1.6.0-0")) {
   130  				log.G(ctx).Warnf("`nerdctl (run|exec) -i` without `-t` expects containerd 1.6 or later, got containerd %v", sv)
   131  			}
   132  			var stdinC io.ReadCloser = &StdinCloser{
   133  				Stdin: os.Stdin,
   134  				Closer: func() {
   135  					if t, err := container.Task(ctx, nil); err != nil {
   136  						log.G(ctx).WithError(err).Debugf("failed to get task for StdinCloser")
   137  					} else {
   138  						t.CloseIO(ctx, containerd.WithStdinCloser)
   139  					}
   140  				},
   141  			}
   142  			in = stdinC
   143  		}
   144  		ioCreator = cioutil.NewContainerIO(namespace, logURI, false, in, os.Stdout, os.Stderr)
   145  	}
   146  	t, err := container.NewTask(ctx, ioCreator)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	return t, nil
   151  }
   152  
   153  // struct used to store streams specified with attachStreamOpt (-a, --attach)
   154  type streams struct {
   155  	stdIn  *os.File
   156  	stdOut *os.File
   157  	stdErr *os.File
   158  }
   159  
   160  func nullStream() *os.File {
   161  	devNull, err := os.Open(os.DevNull)
   162  	if err != nil {
   163  		return nil
   164  	}
   165  	defer devNull.Close()
   166  
   167  	return devNull
   168  }
   169  
   170  func processAttachStreamsOpt(streamsArr []string) streams {
   171  	stdIn := os.Stdin
   172  	stdOut := os.Stdout
   173  	stdErr := os.Stderr
   174  
   175  	for i, str := range streamsArr {
   176  		streamsArr[i] = strings.ToUpper(str)
   177  	}
   178  
   179  	if !slices.Contains(streamsArr, "STDIN") {
   180  		stdIn = nullStream()
   181  	}
   182  
   183  	if !slices.Contains(streamsArr, "STDOUT") {
   184  		stdOut = nullStream()
   185  	}
   186  
   187  	if !slices.Contains(streamsArr, "STDERR") {
   188  		stdErr = nullStream()
   189  	}
   190  
   191  	return streams{
   192  		stdIn:  stdIn,
   193  		stdOut: stdOut,
   194  		stdErr: stdErr,
   195  	}
   196  }
   197  
   198  // StdinCloser is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/exec.go#L181-L194
   199  type StdinCloser struct {
   200  	mu     sync.Mutex
   201  	Stdin  *os.File
   202  	Closer func()
   203  	closed bool
   204  }
   205  
   206  func (s *StdinCloser) Read(p []byte) (int, error) {
   207  	s.mu.Lock()
   208  	defer s.mu.Unlock()
   209  	if s.closed {
   210  		return 0, syscall.EBADF
   211  	}
   212  	n, err := s.Stdin.Read(p)
   213  	if err != nil {
   214  		if s.Closer != nil {
   215  			s.Closer()
   216  			s.closed = true
   217  		}
   218  	}
   219  	return n, err
   220  }
   221  
   222  // Close implements Closer
   223  func (s *StdinCloser) Close() error {
   224  	s.mu.Lock()
   225  	defer s.mu.Unlock()
   226  	if s.closed {
   227  		return nil
   228  	}
   229  	if s.Closer != nil {
   230  		s.Closer()
   231  	}
   232  	s.closed = true
   233  	return nil
   234  }