github.com/containerd/nerdctl@v1.7.7/pkg/mountutil/mountutil.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 mountutil
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  
    27  	"github.com/containerd/containerd/identifiers"
    28  	"github.com/containerd/containerd/oci"
    29  	"github.com/containerd/errdefs"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/idgen"
    32  	"github.com/containerd/nerdctl/pkg/mountutil/volumestore"
    33  	"github.com/containerd/nerdctl/pkg/strutil"
    34  	"github.com/moby/sys/userns"
    35  	"github.com/opencontainers/runtime-spec/specs-go"
    36  )
    37  
    38  const (
    39  	Bind          = "bind"
    40  	Volume        = "volume"
    41  	Tmpfs         = "tmpfs"
    42  	Npipe         = "npipe"
    43  	pathSeparator = string(os.PathSeparator)
    44  )
    45  
    46  type Processed struct {
    47  	Type            string
    48  	Mount           specs.Mount
    49  	Name            string // name
    50  	AnonymousVolume string // anonymous volume name
    51  	Mode            string
    52  	Opts            []oci.SpecOpts
    53  }
    54  
    55  type volumeSpec struct {
    56  	Type            string
    57  	Name            string
    58  	Source          string
    59  	AnonymousVolume string
    60  }
    61  
    62  func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (*Processed, error) {
    63  	var (
    64  		res      *Processed
    65  		volSpec  volumeSpec
    66  		src, dst string
    67  		options  []string
    68  	)
    69  
    70  	split, err := splitVolumeSpec(s)
    71  	if err != nil {
    72  		return nil, fmt.Errorf("failed to split volume mount specification: %v", err)
    73  	}
    74  
    75  	switch len(split) {
    76  	case 1:
    77  		// validate destination
    78  		dst = split[0]
    79  		if _, err := validateAnonymousVolumeDestination(dst); err != nil {
    80  			return nil, err
    81  		}
    82  
    83  		// create anonymous volume
    84  		volSpec, err = handleAnonymousVolumes(dst, volStore)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  
    89  		src = volSpec.Source
    90  		res = &Processed{
    91  			Type:            volSpec.Type,
    92  			AnonymousVolume: volSpec.AnonymousVolume,
    93  		}
    94  	case 2, 3:
    95  		// Vaildate destination
    96  		dst = split[1]
    97  		dst = strings.TrimLeft(dst, ":")
    98  		if _, err := isValidPath(dst); err != nil {
    99  			return nil, err
   100  		}
   101  
   102  		// Get volume spec
   103  		src = split[0]
   104  		volSpec, err = handleVolumeToMount(src, dst, volStore, createDir)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  
   109  		src = volSpec.Source
   110  		res = &Processed{
   111  			Type:            volSpec.Type,
   112  			Name:            volSpec.Name,
   113  			AnonymousVolume: volSpec.AnonymousVolume,
   114  		}
   115  
   116  		// Parse volume options
   117  		if len(split) == 3 {
   118  			res.Mode = split[2]
   119  
   120  			rawOpts := res.Mode
   121  
   122  			options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts)
   123  			if err != nil {
   124  				return nil, err
   125  			}
   126  		}
   127  	default:
   128  		return nil, fmt.Errorf("failed to parse %q", s)
   129  	}
   130  
   131  	fstype := DefaultMountType
   132  	if runtime.GOOS != "freebsd" {
   133  		found := false
   134  		for _, opt := range options {
   135  			switch opt {
   136  			case "rbind", "bind":
   137  				fstype = "bind"
   138  				found = true
   139  			}
   140  			if found {
   141  				break
   142  			}
   143  		}
   144  		if !found {
   145  			options = append(options, "rbind")
   146  		}
   147  	}
   148  	res.Mount = specs.Mount{
   149  		Type:        fstype,
   150  		Source:      cleanMount(src),
   151  		Destination: cleanMount(dst),
   152  		Options:     options,
   153  	}
   154  	if userns.RunningInUserNS() {
   155  		unpriv, err := UnprivilegedMountFlags(src)
   156  		if err != nil {
   157  			return nil, fmt.Errorf("failed to get unprivileged mount flags for %q: %w", src, err)
   158  		}
   159  		res.Mount.Options = strutil.DedupeStrSlice(append(res.Mount.Options, unpriv...))
   160  	}
   161  
   162  	return res, nil
   163  }
   164  
   165  func handleBindMounts(source string, createDir bool) (volumeSpec, error) {
   166  	var res volumeSpec
   167  	res.Type = Bind
   168  	res.Source = source
   169  
   170  	// Handle relative paths
   171  	if !filepath.IsAbs(source) {
   172  		log.L.Warnf("expected an absolute path, got a relative path %q (allowed for nerdctl, but disallowed for Docker, so unrecommended)", source)
   173  		absPath, err := filepath.Abs(source)
   174  		if err != nil {
   175  			return res, fmt.Errorf("failed to get the absolute path of %q: %w", source, err)
   176  		}
   177  		res.Source = absPath
   178  	}
   179  
   180  	// Create dir if it does not exist
   181  	if err := createDirOnHost(source, createDir); err != nil {
   182  		return res, err
   183  	}
   184  
   185  	return res, nil
   186  }
   187  
   188  func handleAnonymousVolumes(s string, volStore volumestore.VolumeStore) (volumeSpec, error) {
   189  	var res volumeSpec
   190  	res.AnonymousVolume = idgen.GenerateID()
   191  
   192  	log.L.Debugf("creating anonymous volume %q, for %q", res.AnonymousVolume, s)
   193  	anonVol, err := volStore.Create(res.AnonymousVolume, []string{})
   194  	if err != nil {
   195  		return res, fmt.Errorf("failed to create an anonymous volume %q: %w", res.AnonymousVolume, err)
   196  	}
   197  
   198  	res.Type = Volume
   199  	res.Source = anonVol.Mountpoint
   200  	return res, nil
   201  }
   202  
   203  func handleNamedVolumes(source string, volStore volumestore.VolumeStore) (volumeSpec, error) {
   204  	var res volumeSpec
   205  	res.Name = source
   206  	vol, err := volStore.Get(res.Name, false)
   207  	if err != nil {
   208  		if errors.Is(err, errdefs.ErrNotFound) {
   209  			vol, err = volStore.Create(res.Name, nil)
   210  			if err != nil {
   211  				return res, fmt.Errorf("failed to create volume %q: %w", res.Name, err)
   212  			}
   213  		} else {
   214  			return res, fmt.Errorf("failed to get volume %q: %w", res.Name, err)
   215  		}
   216  	}
   217  	// src is now an absolute path
   218  	res.Type = Volume
   219  	res.Source = vol.Mountpoint
   220  
   221  	return res, nil
   222  }
   223  
   224  func getVolumeOptions(src string, vType string, rawOpts string) ([]string, []oci.SpecOpts, error) {
   225  	// always call parseVolumeOptions for bind mount to allow the parser to add some default options
   226  	var err error
   227  	var specOpts []oci.SpecOpts
   228  	options, specOpts, err := parseVolumeOptions(vType, src, rawOpts)
   229  	if err != nil {
   230  		return nil, nil, fmt.Errorf("failed to parse volume options (%q, %q, %q): %w", vType, src, rawOpts, err)
   231  	}
   232  
   233  	specOpts = append(specOpts, specOpts...)
   234  	return options, specOpts, nil
   235  }
   236  
   237  func createDirOnHost(src string, createDir bool) error {
   238  	_, err := os.Stat(src)
   239  	if err == nil {
   240  		return nil
   241  	}
   242  
   243  	if !createDir {
   244  
   245  		/**
   246  		* In pkg\mountutil\mountutil_linux.go:432, we disallow creating directories on host if not found
   247  		* The user gets an error if the directory does not exist:
   248  		*	  error mounting "/foo" to rootfs at "/foo": stat /foo: no such file or directory: unknown.
   249  		* We log this error to give the user a hint that they may need to create the directory on the host.
   250  		* https://docs.docker.com/storage/bind-mounts/
   251  		 */
   252  		if os.IsNotExist(err) {
   253  			log.L.Warnf("mount source %q does not exist. Please make sure to create the directory on the host.", src)
   254  			return nil
   255  		}
   256  		return fmt.Errorf("failed to stat %q: %w", src, err)
   257  	}
   258  
   259  	if !os.IsNotExist(err) {
   260  		return fmt.Errorf("failed to stat %q: %w", src, err)
   261  	}
   262  	if err := os.MkdirAll(src, 0o755); err != nil {
   263  		return fmt.Errorf("failed to mkdir %q: %w", src, err)
   264  	}
   265  	return nil
   266  }
   267  
   268  func isNamedVolume(s string) bool {
   269  	err := identifiers.Validate(s)
   270  
   271  	// If the volume name is invalid, we assume it is a path
   272  	return err == nil
   273  }