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

     1  //go:build windows
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  /*
    20     Portions from https://github.com/moby/moby/blob/f5c7673ff8fcbd359f75fb644b1365ca9d20f176/volume/mounts/windows_parser.go#L26
    21     Copyright (C) Docker/Moby authors.
    22     Licensed under the Apache License, Version 2.0
    23     NOTICE: https://github.com/moby/moby/blob/master/NOTICE
    24  */
    25  
    26  package mountutil
    27  
    28  import (
    29  	"fmt"
    30  	"path/filepath"
    31  	"regexp"
    32  	"strings"
    33  
    34  	"github.com/containerd/containerd/oci"
    35  	"github.com/containerd/errdefs"
    36  	"github.com/containerd/log"
    37  	"github.com/containerd/nerdctl/pkg/mountutil/volumestore"
    38  )
    39  
    40  const (
    41  	// Defaults to an empty string
    42  	// https://github.com/microsoft/hcsshim/blob/5c75f29c1f5cb4d3498d66228637d07477bcb6a1/internal/hcsoci/resources_wcow.go#L140
    43  	DefaultMountType = ""
    44  
    45  	// DefaultPropagationMode is the default propagation of mounts
    46  	// where user doesn't specify mount propagation explicitly.
    47  	// See also: https://github.com/moby/moby/blob/v20.10.7/volume/mounts/windows_parser.go#L440-L442
    48  	DefaultPropagationMode = ""
    49  )
    50  
    51  func UnprivilegedMountFlags(path string) ([]string, error) {
    52  	m := []string{}
    53  	return m, nil
    54  }
    55  
    56  // parseVolumeOptions parses specified optsRaw with using information of
    57  // the volume type and the src directory when necessary.
    58  func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
    59  	var writeModeRawOpts []string
    60  	for _, opt := range strings.Split(optsRaw, ",") {
    61  		switch opt {
    62  		case "rw":
    63  			writeModeRawOpts = append(writeModeRawOpts, opt)
    64  		case "ro":
    65  			writeModeRawOpts = append(writeModeRawOpts, opt)
    66  		case "":
    67  			// NOP
    68  		default:
    69  			log.L.Warnf("unsupported volume option %q", opt)
    70  		}
    71  	}
    72  	var opts []string
    73  	if len(writeModeRawOpts) > 1 {
    74  		return nil, nil, fmt.Errorf("duplicated read/write volume option: %+v", writeModeRawOpts)
    75  	} else if len(writeModeRawOpts) > 0 && writeModeRawOpts[0] == "ro" {
    76  		opts = append(opts, "ro")
    77  	} // No need to return option when "rw"
    78  	return opts, nil, nil
    79  }
    80  
    81  func ProcessFlagTmpfs(s string) (*Processed, error) {
    82  	return nil, errdefs.ErrNotImplemented
    83  }
    84  
    85  func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) {
    86  	return nil, errdefs.ErrNotImplemented
    87  }
    88  
    89  func handleVolumeToMount(source string, dst string, volStore volumestore.VolumeStore, createDir bool) (volumeSpec, error) {
    90  	// Validate source and destination types
    91  	if _, err := (validateNamedPipeSpec(source, dst)); err != nil {
    92  		return volumeSpec{}, err
    93  	}
    94  
    95  	switch {
    96  	// Handle named volumes
    97  	case isNamedVolume(source):
    98  		return handleNamedVolumes(source, volStore)
    99  
   100  	// Handle named pipes
   101  	case isNamedPipe(source):
   102  		return handleNpipeToMount(source)
   103  
   104  	// Handle bind volumes (file paths)
   105  	default:
   106  		return handleBindMounts(source, createDir)
   107  	}
   108  }
   109  
   110  func handleNpipeToMount(source string) (volumeSpec, error) {
   111  	res := volumeSpec{
   112  		Type:   Npipe,
   113  		Source: source,
   114  	}
   115  	return res, nil
   116  }
   117  
   118  func splitVolumeSpec(raw string) ([]string, error) {
   119  	raw = strings.TrimSpace(raw)
   120  	raw = strings.TrimLeft(raw, ":")
   121  	if raw == "" {
   122  		return nil, fmt.Errorf("invalid empty volume specification")
   123  	}
   124  
   125  	const (
   126  		// Root drive or relative paths starting with .
   127  		rxHostDir = `(?:[a-zA-Z]:|\.)[\/\\]`
   128  
   129  		// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
   130  		// Windows UNC paths and DOS device paths (and namde pipes)
   131  		rxUNC  = `(?:\\{2}[a-zA-Z0-9_\-\.\?]+\\{1}[^\\*?"|\r\n]+)\\`
   132  		rxName = `[^\/\\:*?"<>|\r\n]+`
   133  
   134  		rxSource      = `((?P<source>((` + rxHostDir + `|` + rxUNC + `)` + `(` + rxName + `[\/\\]?)+` + `|` + rxName + `)):)?`
   135  		rxDestination = `(?P<destination>(` + rxHostDir + `|` + rxUNC + `)` + `(` + rxName + `[\/\\]?)+` + `|` + rxName + `)`
   136  		rxMode        = `(?::(?P<mode>(?i)\w+(,\w+)?))`
   137  
   138  		rxWindows = `^` + rxSource + rxDestination + `(?:` + rxMode + `)?$`
   139  	)
   140  
   141  	compiledRegex, err := regexp.Compile(rxWindows)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("error compiling regex: %s", err)
   144  	}
   145  	return splitRawSpec(raw, compiledRegex)
   146  }
   147  
   148  func isNamedPipe(s string) bool {
   149  	pattern := `^\\{2}.\\pipe\\[^\/\\:*?"<>|\r\n]+$`
   150  	matches, err := regexp.MatchString(pattern, s)
   151  	if err != nil {
   152  		log.L.Errorf("Invalid pattern %s", pattern)
   153  	}
   154  
   155  	return matches
   156  }
   157  
   158  func cleanMount(p string) string {
   159  	if isNamedPipe(p) {
   160  		return p
   161  	}
   162  	return filepath.Clean(p)
   163  }
   164  
   165  func isValidPath(s string) (bool, error) {
   166  	if isNamedPipe(s) || filepath.IsAbs(s) {
   167  		return true, nil
   168  	}
   169  
   170  	return false, fmt.Errorf("expected an absolute path or a named pipe, got %q", s)
   171  }
   172  
   173  /*
   174  For docker compatibility on Windows platforms:
   175  Docker only allows for absolute paths as anonymous volumes.
   176  Docker does not allows anonymous named volumes or anonymous named piped
   177  to be mounted into a container.
   178  */
   179  func validateAnonymousVolumeDestination(s string) (bool, error) {
   180  	if isNamedPipe(s) || isNamedVolume(s) {
   181  		return false, fmt.Errorf("invalid volume specification: %q. only directories can be mapped as anonymous volumes", s)
   182  	}
   183  
   184  	if filepath.IsAbs(s) {
   185  		return true, nil
   186  	}
   187  
   188  	return false, fmt.Errorf("expected an absolute path, got %q", s)
   189  }
   190  
   191  func splitRawSpec(raw string, splitRegexp *regexp.Regexp) ([]string, error) {
   192  	match := splitRegexp.FindStringSubmatch(raw)
   193  	if len(match) == 0 {
   194  		return nil, fmt.Errorf("invalid volume specification: '%s'", raw)
   195  	}
   196  
   197  	var split []string
   198  	matchgroups := make(map[string]string)
   199  	// Pull out the sub expressions from the named capture groups
   200  	for i, name := range splitRegexp.SubexpNames() {
   201  		matchgroups[name] = match[i]
   202  	}
   203  	if source, exists := matchgroups["source"]; exists {
   204  		if source == "." {
   205  			return nil, fmt.Errorf("invalid volume specification: %q", raw)
   206  		}
   207  
   208  		if source != "" {
   209  			split = append(split, source)
   210  		}
   211  	}
   212  
   213  	mode, modExists := matchgroups["mode"]
   214  
   215  	if destination, exists := matchgroups["destination"]; exists {
   216  		if destination == "." {
   217  			return nil, fmt.Errorf("invalid volume specification: %q", raw)
   218  		}
   219  
   220  		// If mode exists and destination is empty, set destination to an empty string
   221  		// source::ro
   222  		if destination != "" || modExists && mode != "" {
   223  			split = append(split, destination)
   224  		}
   225  	}
   226  
   227  	if mode, exists := matchgroups["mode"]; exists {
   228  		if mode != "" {
   229  			split = append(split, mode)
   230  		}
   231  	}
   232  	return split, nil
   233  }
   234  
   235  // Function to parse the source type
   236  func parseSourceType(source string) string {
   237  	switch {
   238  	case isNamedVolume(source):
   239  		return Volume
   240  	case isNamedPipe(source):
   241  		return Npipe
   242  	// Add more cases for different source types as needed
   243  	default:
   244  		return Bind
   245  	}
   246  }
   247  
   248  func validateNamedPipeSpec(source string, dst string) (bool, error) {
   249  	// Validate source and destination types
   250  	sourceType := parseSourceType(source)
   251  	destType := parseSourceType(dst)
   252  
   253  	if (destType == Npipe && sourceType != Npipe) || (sourceType == Npipe && destType != Npipe) {
   254  		return false, fmt.Errorf("invalid volume specification. named pipes can only be mapped to named pipes")
   255  	}
   256  	return true, nil
   257  }