github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/attach.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 container
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  
    24  	"github.com/containerd/console"
    25  	"github.com/containerd/containerd"
    26  	"github.com/containerd/containerd/cio"
    27  	"github.com/containerd/log"
    28  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    29  	"github.com/containerd/nerdctl/v2/pkg/consoleutil"
    30  	"github.com/containerd/nerdctl/v2/pkg/errutil"
    31  	"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
    32  	"github.com/containerd/nerdctl/v2/pkg/signalutil"
    33  )
    34  
    35  // Attach attaches stdin, stdout, and stderr to a running container.
    36  func Attach(ctx context.Context, client *containerd.Client, req string, options types.ContainerAttachOptions) error {
    37  	// Find the container.
    38  	var container containerd.Container
    39  	walker := &containerwalker.ContainerWalker{
    40  		Client: client,
    41  		OnFound: func(ctx context.Context, found containerwalker.Found) error {
    42  			container = found.Container
    43  			return nil
    44  		},
    45  	}
    46  	n, err := walker.Walk(ctx, req)
    47  	if err != nil {
    48  		return fmt.Errorf("error when trying to find the container: %w", err)
    49  	}
    50  	if n == 0 {
    51  		return fmt.Errorf("no container is found given the string: %s", req)
    52  	} else if n > 1 {
    53  		return fmt.Errorf("more than one containers are found given the string: %s", req)
    54  	}
    55  
    56  	// Attach to the container.
    57  	var task containerd.Task
    58  	detachC := make(chan struct{})
    59  	spec, err := container.Spec(ctx)
    60  	if err != nil {
    61  		return fmt.Errorf("failed to get the OCI runtime spec for the container: %w", err)
    62  	}
    63  	var (
    64  		opt cio.Opt
    65  		con console.Console
    66  	)
    67  	if spec.Process.Terminal {
    68  		con = console.Current()
    69  		defer con.Reset()
    70  		if err := con.SetRaw(); err != nil {
    71  			return fmt.Errorf("failed to set the console to raw mode: %w", err)
    72  		}
    73  		closer := func() {
    74  			detachC <- struct{}{}
    75  			// task will be set by container.Task later.
    76  			//
    77  			// We cannot use container.Task(ctx, cio.Load) to get the IO here
    78  			// because the `cancel` field of the returned `*cio` is nil. [1]
    79  			//
    80  			// [1] https://github.com/containerd/containerd/blob/8f756bc8c26465bd93e78d9cd42082b66f276e10/cio/io.go#L358-L359
    81  			io := task.IO()
    82  			if io == nil {
    83  				log.G(ctx).Errorf("got a nil io")
    84  				return
    85  			}
    86  			io.Cancel()
    87  		}
    88  		in, err := consoleutil.NewDetachableStdin(con, options.DetachKeys, closer)
    89  		if err != nil {
    90  			return err
    91  		}
    92  		opt = cio.WithStreams(in, con, nil)
    93  	} else {
    94  		opt = cio.WithStreams(options.Stdin, options.Stdout, options.Stderr)
    95  	}
    96  	task, err = container.Task(ctx, cio.NewAttach(opt))
    97  	if err != nil {
    98  		return fmt.Errorf("failed to attach to the container: %w", err)
    99  	}
   100  	if spec.Process.Terminal {
   101  		if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil {
   102  			log.G(ctx).WithError(err).Error("console resize")
   103  		}
   104  	}
   105  	sigC := signalutil.ForwardAllSignals(ctx, task)
   106  	defer signalutil.StopCatch(sigC)
   107  
   108  	// Wait for the container to exit.
   109  	statusC, err := task.Wait(ctx)
   110  	if err != nil {
   111  		return fmt.Errorf("failed to init an async wait for the container to exit: %w", err)
   112  	}
   113  	select {
   114  	// io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit.
   115  	//
   116  	// If we replace the `select` block with io.Wait() and
   117  	// directly use task.Status() to check the status of the container after io.Wait() returns,
   118  	// it can still be running even though the container is about to exit (somehow especially for Windows).
   119  	//
   120  	// As a result, we need a separate detachC to distinguish from the 2 cases mentioned above.
   121  	case <-detachC:
   122  		io := task.IO()
   123  		if io == nil {
   124  			return errors.New("got a nil IO from the task")
   125  		}
   126  		io.Wait()
   127  	case status := <-statusC:
   128  		code, _, err := status.Result()
   129  		if err != nil {
   130  			return err
   131  		}
   132  		if code != 0 {
   133  			return errutil.NewExitCoderErr(int(code))
   134  		}
   135  	}
   136  	return nil
   137  }