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 }