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 }