github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/run_mount.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  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"runtime"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/containerd/containerd"
    31  	"github.com/containerd/containerd/containers"
    32  	"github.com/containerd/containerd/errdefs"
    33  	"github.com/containerd/containerd/leases"
    34  	"github.com/containerd/containerd/mount"
    35  	"github.com/containerd/containerd/oci"
    36  	"github.com/containerd/containerd/pkg/userns"
    37  	"github.com/containerd/continuity/fs"
    38  	"github.com/containerd/log"
    39  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    40  	"github.com/containerd/nerdctl/v2/pkg/cmd/volume"
    41  	"github.com/containerd/nerdctl/v2/pkg/idgen"
    42  	"github.com/containerd/nerdctl/v2/pkg/imgutil"
    43  	"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
    44  	"github.com/containerd/nerdctl/v2/pkg/labels"
    45  	"github.com/containerd/nerdctl/v2/pkg/mountutil"
    46  	"github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
    47  	"github.com/containerd/nerdctl/v2/pkg/strutil"
    48  	securejoin "github.com/cyphar/filepath-securejoin"
    49  	"github.com/opencontainers/image-spec/identity"
    50  	"github.com/opencontainers/runtime-spec/specs-go"
    51  )
    52  
    53  // copy from https://github.com/containerd/containerd/blob/v1.6.0-rc.1/pkg/cri/opts/spec_linux.go#L129-L151
    54  func withMounts(mounts []specs.Mount) oci.SpecOpts {
    55  	return func(ctx context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error {
    56  		// Copy all mounts from default mounts, except for
    57  		// - mounts overridden by supplied mount;
    58  		// - all mounts under /dev if a supplied /dev is present.
    59  		mountSet := make(map[string]struct{})
    60  		for _, m := range mounts {
    61  			mountSet[filepath.Clean(m.Destination)] = struct{}{}
    62  		}
    63  
    64  		defaultMounts := s.Mounts
    65  		s.Mounts = nil
    66  
    67  		for _, m := range defaultMounts {
    68  			dst := filepath.Clean(m.Destination)
    69  			if _, ok := mountSet[dst]; ok {
    70  				// filter out mount overridden by a supplied mount
    71  				continue
    72  			}
    73  			if _, mountDev := mountSet["/dev"]; mountDev && strings.HasPrefix(dst, "/dev/") {
    74  				// filter out everything under /dev if /dev is a supplied mount
    75  				continue
    76  			}
    77  			s.Mounts = append(s.Mounts, m)
    78  		}
    79  
    80  		s.Mounts = append(s.Mounts, mounts...)
    81  
    82  		sort.Slice(s.Mounts, func(i, j int) bool {
    83  			// Consistent with the less function in Docker.
    84  			// https://github.com/moby/moby/blob/0db417451313474133c5ed62bbf95e2d3c92444d/daemon/volumes.go#L34
    85  			return strings.Count(filepath.Clean(s.Mounts[i].Destination), string(os.PathSeparator)) < strings.Count(filepath.Clean(s.Mounts[j].Destination), string(os.PathSeparator))
    86  		})
    87  
    88  		return nil
    89  	}
    90  }
    91  
    92  // parseMountFlags parses --volume, --mount and --tmpfs.
    93  func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]*mountutil.Processed, error) {
    94  	var parsed []*mountutil.Processed //nolint:prealloc
    95  	for _, v := range strutil.DedupeStrSlice(options.Volume) {
    96  		// createDir=true for -v option to allow creation of directory on host if not found.
    97  		x, err := mountutil.ProcessFlagV(v, volStore, true)
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  		parsed = append(parsed, x)
   102  	}
   103  
   104  	for _, v := range strutil.DedupeStrSlice(options.Tmpfs) {
   105  		x, err := mountutil.ProcessFlagTmpfs(v)
   106  		if err != nil {
   107  			return nil, err
   108  		}
   109  		parsed = append(parsed, x)
   110  	}
   111  
   112  	for _, v := range strutil.DedupeStrSlice(options.Mount) {
   113  		x, err := mountutil.ProcessFlagMount(v, volStore)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  		parsed = append(parsed, x)
   118  	}
   119  
   120  	return parsed, nil
   121  }
   122  
   123  // generateMountOpts generates volume-related mount opts.
   124  // Other mounts such as procfs mount are not handled here.
   125  func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredImage *imgutil.EnsuredImage, options types.ContainerCreateOptions) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) {
   126  	// volume store is corresponds to a directory like `/var/lib/nerdctl/1935db59/volumes/default`
   127  	volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
   128  	if err != nil {
   129  		return nil, nil, nil, err
   130  	}
   131  
   132  	//nolint:golint,prealloc
   133  	var (
   134  		opts        []oci.SpecOpts
   135  		anonVolumes []string
   136  		userMounts  []specs.Mount
   137  		mountPoints []*mountutil.Processed
   138  	)
   139  	mounted := make(map[string]struct{})
   140  	var imageVolumes map[string]struct{}
   141  	var tempDir string
   142  	if ensuredImage != nil {
   143  		imageVolumes = ensuredImage.ImageConfig.Volumes
   144  
   145  		if err := ensuredImage.Image.Unpack(ctx, options.GOptions.Snapshotter); err != nil {
   146  			return nil, nil, nil, fmt.Errorf("error unpacking image: %w", err)
   147  		}
   148  
   149  		diffIDs, err := ensuredImage.Image.RootFS(ctx)
   150  		if err != nil {
   151  			return nil, nil, nil, err
   152  		}
   153  		chainID := identity.ChainID(diffIDs).String()
   154  
   155  		s := client.SnapshotService(options.GOptions.Snapshotter)
   156  		tempDir, err = os.MkdirTemp("", "initialC")
   157  		if err != nil {
   158  			return nil, nil, nil, err
   159  		}
   160  		// We use Remove here instead of RemoveAll.
   161  		// The RemoveAll will delete the temp dir and all children it contains.
   162  		// When the Unmount fails, RemoveAll will incorrectly delete data from the mounted dir
   163  		defer os.Remove(tempDir)
   164  
   165  		// Add a lease of 1 hour to the view so that it is not garbage collected
   166  		// Note(gsamfira): should we make this shorter?
   167  		ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   168  		if err != nil {
   169  			return nil, nil, nil, fmt.Errorf("failed to create lease: %w", err)
   170  		}
   171  		defer done(ctx)
   172  
   173  		var mounts []mount.Mount
   174  		mounts, err = s.View(ctx, tempDir, chainID)
   175  		if err != nil {
   176  			return nil, nil, nil, err
   177  		}
   178  
   179  		// windows has additional steps for mounting see
   180  		// https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b
   181  		unmounter := func(mountPath string) {
   182  			if uerr := mount.Unmount(mountPath, 0); uerr != nil {
   183  				log.G(ctx).Debugf("Failed to unmount snapshot %q", tempDir)
   184  				if err == nil {
   185  					err = uerr
   186  				}
   187  			}
   188  		}
   189  
   190  		if runtime.GOOS == "linux" {
   191  			defer unmounter(tempDir)
   192  			for _, m := range mounts {
   193  				m := m
   194  				if m.Type == "bind" && userns.RunningInUserNS() {
   195  					// For https://github.com/containerd/nerdctl/issues/2056
   196  					unpriv, err := mountutil.UnprivilegedMountFlags(m.Source)
   197  					if err != nil {
   198  						return nil, nil, nil, err
   199  					}
   200  					m.Options = strutil.DedupeStrSlice(append(m.Options, unpriv...))
   201  				}
   202  				if err := m.Mount(tempDir); err != nil {
   203  					if rmErr := s.Remove(ctx, tempDir); rmErr != nil && !errdefs.IsNotFound(rmErr) {
   204  						return nil, nil, nil, rmErr
   205  					}
   206  					return nil, nil, nil, fmt.Errorf("failed to mount %+v on %q: %w", m, tempDir, err)
   207  				}
   208  			}
   209  		} else {
   210  			defer unmounter(tempDir)
   211  			if err := mount.All(mounts, tempDir); err != nil {
   212  				if err := s.Remove(ctx, tempDir); err != nil && !errdefs.IsNotFound(err) {
   213  					return nil, nil, nil, err
   214  				}
   215  				return nil, nil, nil, err
   216  			}
   217  		}
   218  	}
   219  
   220  	if parsed, err := parseMountFlags(volStore, options); err != nil {
   221  		return nil, nil, nil, err
   222  	} else if len(parsed) > 0 {
   223  		ociMounts := make([]specs.Mount, len(parsed))
   224  		for i, x := range parsed {
   225  			ociMounts[i] = x.Mount
   226  			mounted[filepath.Clean(x.Mount.Destination)] = struct{}{}
   227  
   228  			target, err := securejoin.SecureJoin(tempDir, x.Mount.Destination)
   229  			if err != nil {
   230  				return nil, nil, nil, err
   231  			}
   232  
   233  			// Copying content in AnonymousVolume and namedVolume
   234  			if x.Type == "volume" {
   235  				if err := copyExistingContents(target, x.Mount.Source); err != nil {
   236  					return nil, nil, nil, err
   237  				}
   238  			}
   239  			if x.AnonymousVolume != "" {
   240  				anonVolumes = append(anonVolumes, x.AnonymousVolume)
   241  			}
   242  			opts = append(opts, x.Opts...)
   243  		}
   244  		userMounts = append(userMounts, ociMounts...)
   245  
   246  		// add parsed user specified bind-mounts/volume/tmpfs to mountPoints
   247  		mountPoints = append(mountPoints, parsed...)
   248  	}
   249  
   250  	// imageVolumes are defined in Dockerfile "VOLUME" instruction
   251  	for imgVolRaw := range imageVolumes {
   252  		imgVol := filepath.Clean(imgVolRaw)
   253  		switch imgVol {
   254  		case "/", "/dev", "/sys", "proc":
   255  			return nil, nil, nil, fmt.Errorf("invalid VOLUME: %q", imgVolRaw)
   256  		}
   257  		if _, ok := mounted[imgVol]; ok {
   258  			continue
   259  		}
   260  		anonVolName := idgen.GenerateID()
   261  
   262  		log.G(ctx).Debugf("creating anonymous volume %q, for \"VOLUME %s\"",
   263  			anonVolName, imgVolRaw)
   264  		anonVol, err := volStore.Create(anonVolName, []string{})
   265  		if err != nil {
   266  			return nil, nil, nil, err
   267  		}
   268  
   269  		target, err := securejoin.SecureJoin(tempDir, imgVol)
   270  		if err != nil {
   271  			return nil, nil, nil, err
   272  		}
   273  
   274  		//copying up initial contents of the mount point directory
   275  		if err := copyExistingContents(target, anonVol.Mountpoint); err != nil {
   276  			return nil, nil, nil, err
   277  		}
   278  
   279  		m := specs.Mount{
   280  			Type:        "none",
   281  			Source:      anonVol.Mountpoint,
   282  			Destination: imgVol,
   283  			Options:     []string{"rbind"},
   284  		}
   285  		userMounts = append(userMounts, m)
   286  		anonVolumes = append(anonVolumes, anonVolName)
   287  
   288  		mountPoint := &mountutil.Processed{
   289  			Type:            "volume",
   290  			AnonymousVolume: anonVolName,
   291  			Mount:           m,
   292  		}
   293  		mountPoints = append(mountPoints, mountPoint)
   294  	}
   295  
   296  	opts = append(opts, withMounts(userMounts))
   297  
   298  	containers, err := client.Containers(ctx)
   299  	if err != nil {
   300  		return nil, nil, nil, err
   301  	}
   302  
   303  	vfSet := strutil.SliceToSet(options.VolumesFrom)
   304  	var vfMountPoints []dockercompat.MountPoint
   305  	var vfAnonVolumes []string
   306  
   307  	for _, c := range containers {
   308  		ls, err := c.Labels(ctx)
   309  		if err != nil {
   310  			return nil, nil, nil, err
   311  		}
   312  		_, idMatch := vfSet[c.ID()]
   313  		nameMatch := false
   314  		if name, found := ls[labels.Name]; found {
   315  			_, nameMatch = vfSet[name]
   316  		}
   317  
   318  		if idMatch || nameMatch {
   319  			if av, found := ls[labels.AnonymousVolumes]; found {
   320  				err = json.Unmarshal([]byte(av), &vfAnonVolumes)
   321  				if err != nil {
   322  					return nil, nil, nil, err
   323  				}
   324  			}
   325  			if m, found := ls[labels.Mounts]; found {
   326  				err = json.Unmarshal([]byte(m), &vfMountPoints)
   327  				if err != nil {
   328  					return nil, nil, nil, err
   329  				}
   330  			}
   331  
   332  			ps := processeds(vfMountPoints)
   333  			s, err := c.Spec(ctx)
   334  			if err != nil {
   335  				return nil, nil, nil, err
   336  			}
   337  			opts = append(opts, withMounts(s.Mounts))
   338  			anonVolumes = append(anonVolumes, vfAnonVolumes...)
   339  			mountPoints = append(mountPoints, ps...)
   340  		}
   341  	}
   342  
   343  	return opts, anonVolumes, mountPoints, nil
   344  }
   345  
   346  // copyExistingContents copies from the source to the destination and
   347  // ensures the ownership is appropriately set.
   348  func copyExistingContents(source, destination string) error {
   349  	if _, err := os.Stat(source); os.IsNotExist(err) {
   350  		return nil
   351  	}
   352  	dstList, err := os.ReadDir(destination)
   353  	if err != nil {
   354  		return err
   355  	}
   356  	if len(dstList) != 0 {
   357  		log.L.Debugf("volume at %q is not initially empty, skipping copying", destination)
   358  		return nil
   359  	}
   360  	return fs.CopyDir(destination, source)
   361  }