github.com/containerd/nerdctl@v1.7.7/pkg/containerutil/containerutil.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 containerutil
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"path"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/containerd/console"
    32  	"github.com/containerd/containerd"
    33  	"github.com/containerd/containerd/cio"
    34  	"github.com/containerd/containerd/containers"
    35  	"github.com/containerd/containerd/oci"
    36  	"github.com/containerd/containerd/runtime/restart"
    37  	"github.com/containerd/log"
    38  	"github.com/containerd/nerdctl/pkg/consoleutil"
    39  	"github.com/containerd/nerdctl/pkg/errutil"
    40  	"github.com/containerd/nerdctl/pkg/formatter"
    41  	"github.com/containerd/nerdctl/pkg/labels"
    42  	"github.com/containerd/nerdctl/pkg/nsutil"
    43  	"github.com/containerd/nerdctl/pkg/portutil"
    44  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    45  	"github.com/containerd/nerdctl/pkg/signalutil"
    46  	"github.com/containerd/nerdctl/pkg/taskutil"
    47  	"github.com/moby/sys/signal"
    48  	"github.com/opencontainers/runtime-spec/specs-go"
    49  )
    50  
    51  // PrintHostPort writes to `writer` the public (HostIP:HostPort) of a given `containerPort/protocol` in a container.
    52  // if `containerPort < 0`, it writes all public ports of the container.
    53  func PrintHostPort(ctx context.Context, writer io.Writer, container containerd.Container, containerPort int, proto string) error {
    54  	l, err := container.Labels(ctx)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	ports, err := portutil.ParsePortsLabel(l)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	if containerPort < 0 {
    64  		for _, p := range ports {
    65  			fmt.Fprintf(writer, "%d/%s -> %s:%d\n", p.ContainerPort, p.Protocol, p.HostIP, p.HostPort)
    66  		}
    67  		return nil
    68  	}
    69  
    70  	for _, p := range ports {
    71  		if p.ContainerPort == int32(containerPort) && strings.ToLower(p.Protocol) == proto {
    72  			fmt.Fprintf(writer, "%s:%d\n", p.HostIP, p.HostPort)
    73  			return nil
    74  		}
    75  	}
    76  	return fmt.Errorf("no public port %d/%s published for %q", containerPort, proto, container.ID())
    77  }
    78  
    79  // ContainerStatus returns the container's status from its task.
    80  func ContainerStatus(ctx context.Context, c containerd.Container) (containerd.Status, error) {
    81  	// Just in case, there is something wrong in server.
    82  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    83  	defer cancel()
    84  
    85  	task, err := c.Task(ctx, nil)
    86  	if err != nil {
    87  		return containerd.Status{}, err
    88  	}
    89  
    90  	return task.Status(ctx)
    91  }
    92  
    93  // ContainerNetNSPath returns the netns path of a container.
    94  func ContainerNetNSPath(ctx context.Context, c containerd.Container) (string, error) {
    95  	task, err := c.Task(ctx, nil)
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  	status, err := task.Status(ctx)
   100  	if err != nil {
   101  		return "", err
   102  	}
   103  	if status.Status != containerd.Running {
   104  		return "", fmt.Errorf("invalid target container: %s, should be running", c.ID())
   105  	}
   106  	return fmt.Sprintf("/proc/%d/ns/net", task.Pid()), nil
   107  }
   108  
   109  // UpdateStatusLabel updates the "containerd.io/restart.status"
   110  // label of the container according to the value of restart desired status.
   111  func UpdateStatusLabel(ctx context.Context, container containerd.Container, status containerd.ProcessStatus) error {
   112  	opt := containerd.WithAdditionalContainerLabels(map[string]string{
   113  		restart.StatusLabel: string(status),
   114  	})
   115  	return container.Update(ctx, containerd.UpdateContainerOpts(opt))
   116  }
   117  
   118  // UpdateExplicitlyStoppedLabel updates the "containerd.io/restart.explicitly-stopped"
   119  // label of the container according to the value of explicitlyStopped.
   120  func UpdateExplicitlyStoppedLabel(ctx context.Context, container containerd.Container, explicitlyStopped bool) error {
   121  	opt := containerd.WithAdditionalContainerLabels(map[string]string{
   122  		restart.ExplicitlyStoppedLabel: strconv.FormatBool(explicitlyStopped),
   123  	})
   124  	return container.Update(ctx, containerd.UpdateContainerOpts(opt))
   125  }
   126  
   127  // UpdateErrorLabel updates the "nerdctl/error"
   128  // label of the container according to the container error.
   129  func UpdateErrorLabel(ctx context.Context, container containerd.Container, err error) error {
   130  	opt := containerd.WithAdditionalContainerLabels(map[string]string{
   131  		labels.Error: err.Error(),
   132  	})
   133  	return container.Update(ctx, containerd.UpdateContainerOpts(opt))
   134  }
   135  
   136  // WithBindMountHostProcfs replaces procfs mount with rbind.
   137  // Required for --pid=host on rootless.
   138  //
   139  // https://github.com/moby/moby/pull/41893/files
   140  // https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257
   141  func WithBindMountHostProcfs(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
   142  	for i, m := range s.Mounts {
   143  		if path.Clean(m.Destination) == "/proc" {
   144  			newM := specs.Mount{
   145  				Destination: "/proc",
   146  				Type:        "bind",
   147  				Source:      "/proc",
   148  				Options:     []string{"rbind", "nosuid", "noexec", "nodev"},
   149  			}
   150  			s.Mounts[i] = newM
   151  		}
   152  	}
   153  
   154  	// Remove ReadonlyPaths for /proc/*
   155  	newROP := s.Linux.ReadonlyPaths[:0]
   156  	for _, x := range s.Linux.ReadonlyPaths {
   157  		x = path.Clean(x)
   158  		if !strings.HasPrefix(x, "/proc/") {
   159  			newROP = append(newROP, x)
   160  		}
   161  	}
   162  	s.Linux.ReadonlyPaths = newROP
   163  	return nil
   164  }
   165  
   166  // GenerateSharingPIDOpts returns the oci.SpecOpts that shares the host linux namespace from `targetCon`
   167  // If `targetCon` doesn't have a `PIDNamespace`, a new one is generated from its `Pid`.
   168  func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) ([]oci.SpecOpts, error) {
   169  	opts := make([]oci.SpecOpts, 0)
   170  
   171  	task, err := targetCon.Task(ctx, nil)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	status, err := task.Status(ctx)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if status.Status != containerd.Running {
   181  		return nil, fmt.Errorf("shared container is not running")
   182  	}
   183  
   184  	spec, err := targetCon.Spec(ctx)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	isHost := true
   190  	for _, n := range spec.Linux.Namespaces {
   191  		if n.Type == specs.PIDNamespace {
   192  			isHost = false
   193  		}
   194  	}
   195  	if isHost {
   196  		opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace))
   197  		if rootlessutil.IsRootless() {
   198  			opts = append(opts, WithBindMountHostProcfs)
   199  		}
   200  	} else {
   201  		ns := specs.LinuxNamespace{
   202  			Type: specs.PIDNamespace,
   203  			Path: fmt.Sprintf("/proc/%d/ns/pid", task.Pid()),
   204  		}
   205  		opts = append(opts, oci.WithLinuxNamespace(ns))
   206  	}
   207  	return opts, nil
   208  }
   209  
   210  // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio.
   211  func Start(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client, detachKeys string) (err error) {
   212  	// defer the storage of start error in the dedicated label
   213  	defer func() {
   214  		if err != nil {
   215  			UpdateErrorLabel(ctx, container, err)
   216  		}
   217  	}()
   218  	lab, err := container.Labels(ctx)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	if err := ReconfigNetContainer(ctx, container, client, lab); err != nil {
   224  		return err
   225  	}
   226  
   227  	if err := ReconfigPIDContainer(ctx, container, client, lab); err != nil {
   228  		return err
   229  	}
   230  
   231  	process, err := container.Spec(ctx)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	flagT := process.Process.Terminal
   236  	var con console.Console
   237  	if flagA && flagT {
   238  		con = console.Current()
   239  		defer con.Reset()
   240  		if err := con.SetRaw(); err != nil {
   241  			return err
   242  		}
   243  	}
   244  
   245  	logURI := lab[labels.LogURI]
   246  	namespace := lab[labels.Namespace]
   247  	cStatus := formatter.ContainerStatus(ctx, container)
   248  	if cStatus == "Up" {
   249  		log.G(ctx).Warnf("container %s is already running", container.ID())
   250  		return nil
   251  	}
   252  
   253  	_, restartPolicyExist := lab[restart.PolicyLabel]
   254  	if restartPolicyExist {
   255  		if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil {
   261  		return err
   262  	}
   263  	if oldTask, err := container.Task(ctx, nil); err == nil {
   264  		if _, err := oldTask.Delete(ctx); err != nil {
   265  			log.G(ctx).WithError(err).Debug("failed to delete old task")
   266  		}
   267  	}
   268  	detachC := make(chan struct{})
   269  	attachStreamOpt := []string{}
   270  	if flagA {
   271  		// In start, flagA attaches only STDOUT/STDERR
   272  		// source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start
   273  		attachStreamOpt = []string{"STDOUT", "STDERR"}
   274  	}
   275  	task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	if err := task.Start(ctx); err != nil {
   281  		return err
   282  	}
   283  	if !flagA {
   284  		return nil
   285  	}
   286  	if flagA && flagT {
   287  		if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil {
   288  			log.G(ctx).WithError(err).Error("console resize")
   289  		}
   290  	}
   291  	sigc := signalutil.ForwardAllSignals(ctx, task)
   292  	defer signalutil.StopCatch(sigc)
   293  
   294  	statusC, err := task.Wait(ctx)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	select {
   299  	// io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit.
   300  	//
   301  	// If we replace the `select` block with io.Wait() and
   302  	// directly use task.Status() to check the status of the container after io.Wait() returns,
   303  	// it can still be running even though the container is about to exit (somehow especially for Windows).
   304  	//
   305  	// As a result, we need a separate detachC to distinguish from the 2 cases mentioned above.
   306  	case <-detachC:
   307  		io := task.IO()
   308  		if io == nil {
   309  			return errors.New("got a nil IO from the task")
   310  		}
   311  		io.Wait()
   312  	case status := <-statusC:
   313  		code, _, err := status.Result()
   314  		if err != nil {
   315  			return err
   316  		}
   317  		if code != 0 {
   318  			return errutil.NewExitCoderErr(int(code))
   319  		}
   320  	}
   321  	return nil
   322  }
   323  
   324  // Stop stops `container` by sending SIGTERM. If the container is not stopped after `timeout`, it sends a SIGKILL.
   325  func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) (err error) {
   326  	// defer the storage of stop error in the dedicated label
   327  	defer func() {
   328  		if err != nil {
   329  			UpdateErrorLabel(ctx, container, err)
   330  		}
   331  	}()
   332  	if err := UpdateExplicitlyStoppedLabel(ctx, container, true); err != nil {
   333  		return err
   334  	}
   335  
   336  	l, err := container.Labels(ctx)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	if timeout == nil {
   342  		t, ok := l[labels.StopTimeout]
   343  		if !ok {
   344  			// Default is 10 seconds.
   345  			t = "10"
   346  		}
   347  		td, err := time.ParseDuration(t + "s")
   348  		if err != nil {
   349  			return err
   350  		}
   351  		timeout = &td
   352  	}
   353  
   354  	task, err := container.Task(ctx, cio.Load)
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	status, err := task.Status(ctx)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	paused := false
   365  
   366  	switch status.Status {
   367  	case containerd.Created, containerd.Stopped:
   368  		return nil
   369  	case containerd.Paused, containerd.Pausing:
   370  		paused = true
   371  	default:
   372  	}
   373  
   374  	// NOTE: ctx is main context so that it's ok to use for task.Wait().
   375  	exitCh, err := task.Wait(ctx)
   376  	if err != nil {
   377  		return err
   378  	}
   379  
   380  	if *timeout > 0 {
   381  		sig, err := signal.ParseSignal("SIGTERM")
   382  		if err != nil {
   383  			return err
   384  		}
   385  		if stopSignal, ok := l[containerd.StopSignalLabel]; ok {
   386  			sig, err = signal.ParseSignal(stopSignal)
   387  			if err != nil {
   388  				return err
   389  			}
   390  		}
   391  
   392  		if err := task.Kill(ctx, sig); err != nil {
   393  			return err
   394  		}
   395  
   396  		// signal will be sent once resume is finished
   397  		if paused {
   398  			if err := task.Resume(ctx); err != nil {
   399  				log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err)
   400  			} else {
   401  				// no need to do it again when send sigkill signal
   402  				paused = false
   403  			}
   404  		}
   405  
   406  		sigtermCtx, sigtermCtxCancel := context.WithTimeout(ctx, *timeout)
   407  		defer sigtermCtxCancel()
   408  
   409  		err = waitContainerStop(sigtermCtx, exitCh, container.ID())
   410  		if err == nil {
   411  			return nil
   412  		}
   413  
   414  		if ctx.Err() != nil {
   415  			return ctx.Err()
   416  		}
   417  	}
   418  
   419  	sig, err := signal.ParseSignal("SIGKILL")
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	if err := task.Kill(ctx, sig); err != nil {
   425  		return err
   426  	}
   427  
   428  	// signal will be sent once resume is finished
   429  	if paused {
   430  		if err := task.Resume(ctx); err != nil {
   431  			log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err)
   432  		}
   433  	}
   434  	return waitContainerStop(ctx, exitCh, container.ID())
   435  }
   436  
   437  func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error {
   438  	select {
   439  	case <-ctx.Done():
   440  		if err := ctx.Err(); err != nil {
   441  			return fmt.Errorf("wait container %v: %w", id, err)
   442  		}
   443  		return nil
   444  	case status := <-exitCh:
   445  		return status.Error()
   446  	}
   447  }
   448  
   449  // Pause pauses a container by its id.
   450  func Pause(ctx context.Context, client *containerd.Client, id string) error {
   451  	container, err := client.LoadContainer(ctx, id)
   452  	if err != nil {
   453  		return err
   454  	}
   455  
   456  	task, err := container.Task(ctx, cio.Load)
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	status, err := task.Status(ctx)
   462  	if err != nil {
   463  		return err
   464  	}
   465  
   466  	switch status.Status {
   467  	case containerd.Paused:
   468  		return fmt.Errorf("container %s is already paused", id)
   469  	case containerd.Created, containerd.Stopped:
   470  		return fmt.Errorf("container %s is not running", id)
   471  	default:
   472  		return task.Pause(ctx)
   473  	}
   474  }
   475  
   476  // Unpause unpauses a container by its id.
   477  func Unpause(ctx context.Context, client *containerd.Client, id string) error {
   478  	container, err := client.LoadContainer(ctx, id)
   479  	if err != nil {
   480  		return err
   481  	}
   482  
   483  	task, err := container.Task(ctx, cio.Load)
   484  	if err != nil {
   485  		return err
   486  	}
   487  
   488  	status, err := task.Status(ctx)
   489  	if err != nil {
   490  		return err
   491  	}
   492  
   493  	switch status.Status {
   494  	case containerd.Paused:
   495  		return task.Resume(ctx)
   496  	default:
   497  		return fmt.Errorf("container %s is not paused", id)
   498  	}
   499  }
   500  
   501  // ContainerStateDirPath returns the path to the Nerdctl-managed state directory for the container with the given ID.
   502  func ContainerStateDirPath(ns, dataStore, id string) (string, error) {
   503  	if err := nsutil.ValidateNamespaceName(ns); err != nil {
   504  		return "", fmt.Errorf("invalid namespace name %q for determining state dir of container %q: %s", ns, id, err)
   505  	}
   506  	return filepath.Join(dataStore, "containers", ns, id), nil
   507  }
   508  
   509  // ContainerVolume is a struct representing a volume in a container.
   510  type ContainerVolume struct {
   511  	Type        string
   512  	Name        string
   513  	Source      string
   514  	Destination string
   515  	Mode        string
   516  	RW          bool
   517  	Propagation string
   518  }
   519  
   520  // GetContainerVolumes is a function that returns a slice of containerVolume pointers.
   521  // It accepts a map of container labels as input, where key is the label name and value is its associated value.
   522  // The function iterates over the predefined volume labels (AnonymousVolumes and Mounts)
   523  // and for each, it checks if the labels exists in the provided container labels.
   524  // If yes, it decodes the label value from JSON format and appends the volumes to the result.
   525  // In case of error during decoding, it logs the error and continues to the next label.
   526  func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume {
   527  	var vols []*ContainerVolume
   528  	volLabels := []string{labels.AnonymousVolumes, labels.Mounts}
   529  	for _, volLabel := range volLabels {
   530  		names, ok := containerLabels[volLabel]
   531  		if !ok {
   532  			continue
   533  		}
   534  		var (
   535  			volumes []*ContainerVolume
   536  			err     error
   537  		)
   538  		if volLabel == labels.Mounts {
   539  			err = json.Unmarshal([]byte(names), &volumes)
   540  		}
   541  		if volLabel == labels.AnonymousVolumes {
   542  			var anonymous []string
   543  			err = json.Unmarshal([]byte(names), &anonymous)
   544  			for _, anony := range anonymous {
   545  				volumes = append(volumes, &ContainerVolume{Name: anony})
   546  			}
   547  
   548  		}
   549  		if err != nil {
   550  			log.L.Warn(err)
   551  		}
   552  		vols = append(vols, volumes...)
   553  	}
   554  	return vols
   555  }